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