fix: [sound-logo] UX 개선 - 중앙 toast 안내 시스템 + transport bar 레이아웃 수정
- toast를 화면 중앙에 표시하고 info/warn/error 유형별 색상 분리 - 모든 기능에 조건 미충족 시 가이드 메시지 추가 (음표/음성/배경음악 미생성 안내) - 에러 발생 시 console 대신 사용자 친화적 toast로 알림 - transport bar 하단 잘림 수정 (height 계산 + margin 보정)
This commit is contained in:
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user