From c993826fdc2dd0abb39d9b1025e08c680c03e421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sun, 8 Mar 2026 13:53:24 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20[sound-logo]=20UX=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=20-=20=EC=A4=91=EC=95=99=20toast=20=EC=95=88=EB=82=B4=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20+=20transport=20bar=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - toast를 화면 중앙에 표시하고 info/warn/error 유형별 색상 분리 - 모든 기능에 조건 미충족 시 가이드 메시지 추가 (음표/음성/배경음악 미생성 안내) - 에러 발생 시 console 대신 사용자 친화적 toast로 알림 - transport bar 하단 잘림 수정 (height 계산 + margin 보정) --- resources/views/rd/sound-logo/index.blade.php | 69 +++++++++++++------ 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/resources/views/rd/sound-logo/index.blade.php b/resources/views/rd/sound-logo/index.blade.php index 7239a9d8..6585bc22 100644 --- a/resources/views/rd/sound-logo/index.blade.php +++ b/resources/views/rd/sound-logo/index.blade.php @@ -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 @@ -
@@ -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); }, }; }