fix: [sound-logo] UX 개선 - 중앙 toast 안내 시스템 + transport bar 레이아웃 수정

- toast를 화면 중앙에 표시하고 info/warn/error 유형별 색상 분리
- 모든 기능에 조건 미충족 시 가이드 메시지 추가 (음표/음성/배경음악 미생성 안내)
- 에러 발생 시 console 대신 사용자 친화적 toast로 알림
- transport bar 하단 잘림 수정 (height 계산 + margin 보정)
This commit is contained in:
김보곤
2026-03-08 13:53:24 +09:00
parent 904cde62cf
commit 584961ca18

View File

@@ -21,9 +21,10 @@
}
.sl-wrap {
display: flex; flex-direction: column; height: calc(100vh - 56px);
display: flex; flex-direction: column; height: calc(100vh - 64px);
background: var(--sl-bg); overflow: hidden;
font-family: 'Pretendard', -apple-system, sans-serif; color: var(--sl-text);
margin: -24px;
}
/* Toolbar */
@@ -64,7 +65,7 @@
/* Main Layout */
.sl-main {
display: flex; flex: 1; overflow: hidden;
display: flex; flex: 1; overflow: hidden; min-height: 0;
}
.sl-sidebar {
width: 240px; flex-shrink: 0; background: var(--sl-card);
@@ -174,11 +175,14 @@
/* Toast */
.sl-toast {
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
background: var(--sl-indigo); color: #fff; padding: 10px 20px;
border-radius: 8px; font-size: 13px; z-index: 9999;
box-shadow: 0 4px 12px rgba(0,0,0,.3);
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
color: #fff; padding: 14px 28px; max-width: 420px; text-align: center;
border-radius: 10px; font-size: 13px; font-weight: 500; z-index: 9999;
box-shadow: 0 8px 24px rgba(0,0,0,.5); line-height: 1.5;
}
.sl-toast.info { background: var(--sl-indigo); }
.sl-toast.warn { background: #d97706; }
.sl-toast.error { background: var(--sl-red); }
/* Note picker dropdown */
.sl-note-picker {
@@ -978,7 +982,7 @@
</template>
<!-- Toast -->
<div class="sl-toast" x-show="toastMsg" x-text="toastMsg"
<div class="sl-toast" :class="toastType" x-show="toastMsg" x-text="toastMsg"
x-transition.opacity.duration.300ms x-cloak></div>
</div>
@@ -1000,6 +1004,7 @@ function soundLogo() {
playingIdx: -1,
isPlaying: false,
toastMsg: '',
toastType: 'info',
savedSounds: [],
currentSoundId: null,
presetIdx: -1,
@@ -1370,7 +1375,13 @@ function soundLogo() {
},
async playAll() {
if (this.isPlaying || this.notes.length === 0) return;
if (this.isPlaying) return;
if (this.notes.length === 0 && !this.voiceBuffer && !this.bgmBuffer) {
return this.toast('재생할 콘텐츠가 없습니다.\n음표를 추가하거나 음성/배경음악을 먼저 생성해 주세요.', 'warn');
}
if (this.notes.length === 0 && !this.voiceBuffer && this.bgmBuffer) {
// BGM만 있는 경우 — 허용하되 안내
}
this.isPlaying = true;
this.pickerIdx = -1;
@@ -1480,7 +1491,9 @@ function soundLogo() {
// ===== Export WAV =====
async exportWav() {
if (this.notes.length === 0) return this.toast('음표를 추가해 주세요');
if (this.notes.length === 0 && !this.voiceBuffer && !this.bgmBuffer) {
return this.toast('저장할 콘텐츠가 없습니다.\n음표, 음성, 배경음악 중 하나 이상을 먼저 만들어 주세요.', 'warn');
}
const sampleRate = 44100;
const synthDur = this.getTotalDuration() + this.adsr.release / 1000 + 0.5;
@@ -1594,7 +1607,8 @@ function soundLogo() {
// ===== 음성 오버레이 (TTS) =====
async generateVoice() {
if (this.voiceLoading || !this.voiceText.trim()) return;
if (this.voiceLoading) return;
if (!this.voiceText.trim()) return this.toast('음성으로 변환할 텍스트를 입력해 주세요.', 'warn');
this.voiceLoading = true;
this.voiceBuffer = null;
@@ -1618,10 +1632,10 @@ function soundLogo() {
this.voiceBuffer = this.decodeL16(data.audio_data, sampleRate);
this.toast('음성 생성 완료: "' + this.voiceText + '"');
} else {
this.toast(data.error || '음성 생성 실패');
this.toast(data.error || '음성 생성 실패했습니다. 다시 시도해 주세요.', 'error');
}
} catch (e) {
this.toast('음성 생성 중 오류 발생');
this.toast('음성 생성 중 네트워크 오류 발생했습니다.', 'error');
} finally {
this.voiceLoading = false;
}
@@ -1645,7 +1659,7 @@ function soundLogo() {
},
async playVoiceOnly() {
if (!this.voiceBuffer) return;
if (!this.voiceBuffer) return this.toast('음성을 먼저 생성해 주세요.\n음성 탭에서 텍스트를 입력하고 생성 버튼을 눌러주세요.', 'warn');
const ctx = this.getAudioCtx();
if (ctx.state === 'suspended') await ctx.resume();
const src = ctx.createBufferSource();
@@ -1665,7 +1679,8 @@ function soundLogo() {
// ===== 배경음악 (Lyria RealTime) =====
async generateBgm() {
if (this.bgmLoading || !this.bgmPrompt.trim()) return;
if (this.bgmLoading) return;
if (!this.bgmPrompt.trim()) return this.toast('배경음악 프롬프트를 입력해 주세요.\n원하는 분위기나 장르를 설명해 주세요.', 'warn');
this.bgmLoading = true;
this.bgmError = '';
this.bgmProgress = '연결 중...';
@@ -1747,6 +1762,7 @@ function soundLogo() {
if (msg.filteredPrompt) {
this.bgmError = '프롬프트가 안전 필터에 의해 거부되었습니다.';
this.bgmLoading = false;
this.toast('입력한 프롬프트가 안전 필터에 거부되었습니다.\n다른 표현으로 변경해 주세요.', 'error');
ws.close();
}
};
@@ -1754,6 +1770,7 @@ function soundLogo() {
ws.onerror = () => {
this.bgmError = 'WebSocket 연결 오류가 발생했습니다.';
this.bgmLoading = false;
this.toast('배경음악 서버 연결에 실패했습니다.\n잠시 후 다시 시도해 주세요.', 'error');
};
ws.onclose = () => {
@@ -1764,6 +1781,7 @@ function soundLogo() {
} else if (!this.bgmError) {
this.bgmError = '오디오 데이터를 받지 못했습니다.';
this.bgmLoading = false;
this.toast('배경음악 생성에 실패했습니다.\n다른 프롬프트로 다시 시도해 주세요.', 'error');
}
};
@@ -1782,6 +1800,7 @@ function soundLogo() {
} catch (e) {
this.bgmError = e.message || '배경음악 생성 중 오류 발생';
this.bgmLoading = false;
this.toast(this.bgmError, 'error');
}
},
@@ -1827,13 +1846,14 @@ function soundLogo() {
this.toast('배경음악 생성 완료 (' + buffer.duration.toFixed(1) + '초)');
} catch (e) {
this.bgmError = '오디오 디코딩 실패: ' + e.message;
this.toast('배경음악 오디오 처리에 실패했습니다.', 'error');
} finally {
this.bgmLoading = false;
}
},
async playBgmOnly() {
if (!this.bgmBuffer) return;
if (!this.bgmBuffer) return this.toast('배경음악을 먼저 생성해 주세요.\nAI 배경음악 탭에서 프롬프트를 입력하고 생성 버튼을 눌러주세요.', 'warn');
const ctx = this.getAudioCtx();
if (ctx.state === 'suspended') await ctx.resume();
const src = ctx.createBufferSource();
@@ -1845,7 +1865,7 @@ function soundLogo() {
},
async exportBgmWav() {
if (!this.bgmBuffer) return;
if (!this.bgmBuffer) return this.toast('배경음악을 먼저 생성해 주세요.', 'warn');
const wav = this.bufferToWav(this.bgmBuffer);
const blob = new Blob([wav], { type: 'audio/wav' });
const url = URL.createObjectURL(blob);
@@ -1869,7 +1889,8 @@ function soundLogo() {
// ===== AI 어시스트 =====
async generateWithAi() {
if (this.aiLoading || !this.aiPrompt.trim()) return;
if (this.aiLoading) return;
if (!this.aiPrompt.trim()) return this.toast('AI 생성 프롬프트를 입력해 주세요.\n원하는 사운드 로고를 설명해 주세요.', 'warn');
this.aiLoading = true;
this.aiResult = null;
this.aiError = '';
@@ -1891,18 +1912,21 @@ function soundLogo() {
const data = await res.json();
if (data.success && data.data) {
this.aiResult = data.data;
this.toast('AI 사운드 로고가 생성되었습니다.');
} else {
this.aiError = data.error || 'AI 생성에 실패했습니다.';
this.toast(this.aiError, 'error');
}
} catch (e) {
this.aiError = '네트워크 오류가 발생했습니다.';
this.toast(this.aiError, 'error');
} finally {
this.aiLoading = false;
}
},
loadAiResult() {
if (!this.aiResult) return;
if (!this.aiResult) return this.toast('적용할 AI 결과가 없습니다.\n먼저 AI 생성을 실행해 주세요.', 'warn');
const r = this.aiResult;
if (r.synth) this.synth = r.synth;
if (r.adsr) this.adsr = { ...r.adsr };
@@ -1914,7 +1938,8 @@ function soundLogo() {
},
async previewAiResult() {
if (!this.aiResult || this.isPlaying) return;
if (this.isPlaying) return;
if (!this.aiResult) return this.toast('AI 생성 결과가 없습니다.\nAI 생성 탭에서 먼저 사운드를 생성해 주세요.', 'warn');
// 임시로 AI 결과를 현재 설정에 적용 후 재생
const backup = { synth: this.synth, adsr: { ...this.adsr }, volume: this.volume, reverb: this.reverb, notes: JSON.parse(JSON.stringify(this.notes)) };
this.loadAiResult();
@@ -2022,9 +2047,11 @@ function soundLogo() {
input.click();
},
toast(msg) {
toast(msg, type = 'info') {
this.toastMsg = msg;
setTimeout(() => { this.toastMsg = ''; }, 2500);
this.toastType = type;
const dur = type === 'error' ? 3500 : type === 'warn' ? 3000 : 2500;
setTimeout(() => { this.toastMsg = ''; }, dur);
},
};
}