장르 선택 로직 개선

main
김성경 2026-06-26 11:50:08 +09:00
parent 888ac4fa5e
commit 6c499ae061
15 changed files with 167 additions and 112 deletions

View File

@ -507,7 +507,6 @@ const App: React.FC = () => {
onAnalyze={handleStartAnalysis}
onAutocomplete={handleAutocomplete}
onManualInput={handleManualInput}
onTestData={handleTestData}
onNext={() => scrollToSection(1)}
error={error}
scrollProgress={scrollProgress}

View File

@ -94,7 +94,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}`;
return t('videoDetail.dateFormat', { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() });
};
const formatCommentDate = (dateString: string) => {
@ -120,12 +120,12 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
kakao.Share.sendDefault({
objectType: 'feed',
content: {
title: video?.store_name ?? 'ADO2 영상',
description: `${video?.region ?? ''} · ADO2 AI 마케팅 영상`,
title: video?.store_name ?? t('videoDetail.kakaoDefaultTitle'),
description: t('videoDetail.kakaoDescription', { region: video?.region ?? '' }),
imageUrl: 'https://demo.castad.net/favicon_48.svg',
link: { mobileWebUrl: shareUrl, webUrl: shareUrl },
},
buttons: [{ title: '영상 보기', link: { mobileWebUrl: shareUrl, webUrl: shareUrl } }],
buttons: [{ title: t('videoDetail.kakaoButtonTitle'), link: { mobileWebUrl: shareUrl, webUrl: shareUrl } }],
});
} else if (navigator.share) {
navigator.share({ url: shareUrl }).catch(() => {});
@ -222,7 +222,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
};
const renderCommentContent = (content: string | null, isDeleted: boolean) => {
if (isDeleted) return <span style={{ color: '#6B9EA0', fontStyle: 'italic' }}>( .)</span>;
if (isDeleted) return <span style={{ color: '#6B9EA0', fontStyle: 'italic' }}>{t('videoDetail.deletedComment')}</span>;
return content;
};
@ -231,7 +231,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
{/* 헤더 */}
<div className="video-detail-header">
{isModal ? (
<button className="video-detail-close-btn" onClick={handleHeaderAction} aria-label="닫기">
<button className="video-detail-close-btn" onClick={handleHeaderAction} aria-label={t('videoDetail.closeAriaLabel')}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
@ -293,7 +293,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>
{copied ? '복사됨!' : '공유하기'}
{copied ? t('videoDetail.copied') : t('videoDetail.share')}
</button>
{shareMenuOpen && (
<div className="video-detail-share-menu">
@ -303,21 +303,21 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
<rect width="20" height="20" rx="4" fill="#FEE500"/>
<path fillRule="evenodd" clipRule="evenodd" d="M10 3.5C6.134 3.5 3 6.01 3 9.1c0 1.98 1.2 3.72 3.01 4.76l-.74 2.75a.19.19 0 0 0 .28.21l3.37-2.23c.34.04.69.06 1.06.06 3.866 0 7-2.51 7-5.6S13.866 3.5 10 3.5z" fill="#3C1E1E"/>
</svg>
{t('videoDetail.shareKakao')}
</button>
{/* 페이스북 */}
<button className="video-detail-share-item" onClick={handleFacebookShare}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="#1877F2">
<path d="M24 12.073C24 5.405 18.627 0 12 0S0 5.405 0 12.073C0 18.1 4.388 23.094 10.125 24v-8.437H7.078v-3.49h3.047V9.41c0-3.025 1.792-4.697 4.533-4.697 1.312 0 2.686.235 2.686.235v2.97h-1.513c-1.491 0-1.956.93-1.956 1.887v2.268h3.328l-.532 3.49h-2.796V24C19.612 23.094 24 18.1 24 12.073z"/>
</svg>
{t('videoDetail.shareFacebook')}
</button>
{/* X (트위터) */}
<button className="video-detail-share-item" onClick={handleTwitterShare}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
X ()
{t('videoDetail.shareTwitter')}
</button>
{/* URL 복사 */}
<button className="video-detail-share-item" onClick={() => { handleCopyLink(); setShareMenuOpen(false); }}>
@ -325,7 +325,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
<rect x="9" y="9" width="13" height="13" rx="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
{copied ? '복사됨!' : 'URL 복사'}
{copied ? t('videoDetail.copied') : t('videoDetail.copyUrl')}
</button>
</div>
)}
@ -335,7 +335,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
{/* 댓글 섹션 */}
<div className="video-detail-comments">
<div className="video-detail-comments-header">
<h3 className="video-detail-comments-title"></h3>
<h3 className="video-detail-comments-title">{t('videoDetail.commentsTitle')}</h3>
<span className="video-detail-comments-count">{commentsTotal}</span>
</div>
@ -344,16 +344,16 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
<div className="video-detail-comment-profile">
<img
src={`https://api.dicebear.com/9.x/pixel-art/svg?seed=${commentAvatarSeed}`}
alt="아바타 변경"
alt={t('videoDetail.changeAvatarTitle')}
className="video-detail-comment-avatar"
onClick={handleChangeAvatar}
style={{ cursor: 'pointer' }}
title="클릭하여 아바타 변경"
title={t('videoDetail.changeAvatarTitle')}
/>
<input
className="video-detail-nickname-input"
type="text"
placeholder="작성자 이름"
placeholder={t('videoDetail.nicknamePlaceholder')}
value={commentNickname}
onChange={(e) => setCommentNickname(e.target.value)}
maxLength={20}
@ -365,7 +365,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
<textarea
ref={commentTextareaRef}
className="video-detail-comment-input"
placeholder={authed ? '댓글을 입력하세요...' : '로그인 후 댓글을 작성할 수 있습니다'}
placeholder={authed ? t('videoDetail.commentPlaceholder') : t('videoDetail.commentLoginRequired')}
maxLength={500}
rows={1}
value={commentInput}
@ -388,12 +388,12 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
onClick={handleCommentSubmit}
disabled={!authed || !commentInput.trim() || commentSubmitting}
>
{commentSubmitting ? '작성 중' : '작성'}
{commentSubmitting ? t('videoDetail.commentSubmitting') : t('videoDetail.commentSubmit')}
</button>
</div>
{comments.length === 0 && !commentsLoading ? (
<p className="video-detail-comments-empty"> .</p>
<p className="video-detail-comments-empty">{t('videoDetail.noComments')}</p>
) : (
<ul className="video-detail-comment-list">
{comments.map((c) => (
@ -405,7 +405,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
/>
<div className="video-detail-comment-body">
<span className="video-detail-comment-nickname">
{c.nickname || '익명'}
{c.nickname || t('videoDetail.anonymous')}
</span>
<p className="video-detail-comment-text">
{renderCommentContent(c.content, c.is_deleted)}
@ -417,7 +417,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
className="video-detail-comment-delete"
onClick={() => handleDeleteComment(c.id)}
>
{t('videoDetail.deleteComment')}
</button>
)}
</div>
@ -434,7 +434,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
/>
<div className="video-detail-comment-body">
<span className="video-detail-comment-nickname">
{r.nickname || '익명'}
{r.nickname || t('videoDetail.anonymous')}
</span>
<p className="video-detail-comment-text">
{renderCommentContent(r.content, r.is_deleted)}
@ -446,7 +446,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
className="video-detail-comment-delete"
onClick={() => handleDeleteComment(r.id)}
>
{t('videoDetail.deleteComment')}
</button>
)}
</div>
@ -467,7 +467,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
onClick={() => fetchComments(commentsPage + 1, true)}
disabled={commentsLoading}
>
{commentsLoading ? '불러오는 중...' : '댓글 더 보기'}
{commentsLoading ? t('videoDetail.loadingComments') : t('videoDetail.loadMoreComments')}
</button>
)}
</div>

View File

@ -41,7 +41,7 @@ const VideoDetailModal: React.FC<VideoDetailModalProps> = ({ videoId, onClose })
background: '#01282A',
borderRadius: '16px',
width: '100%',
maxWidth: '900px',
maxWidth: '800px',
margin: 'auto',
position: 'relative',
}}

View File

@ -54,7 +54,7 @@
"video": { "title": "Generate Video", "desc": "Click the button to start generating your video." }
},
"completion": {
"contentInfo": { "title": "Content Info", "desc": "Check the title, genre, resolution, and lyrics of the generated content." },
"contentInfo": { "title": "Content Info", "desc": "Check the file name, genre, resolution, and lyrics of the generated content." },
"generating": { "title": "Generating Video", "desc": "AI is creating your video.\nPlease wait a moment." },
"completion": { "title": "Video Complete!", "desc": "Your video is ready. Want to take a look?" },
"myInfo": { "title": "Connect Social Account", "desc": "To upload your video to YouTube, connect your social account in My Info. Click to go there." }
@ -188,16 +188,16 @@
"hero": {
"searchTypeLabel": "Business Name | URL",
"placeholderCombined": "Enter a business name or Naver Maps/Place URL",
"guideCombined": "Enter a business name, or paste a Naver Maps share URL (naver.me/...) to automatically retrieve information.",
"guideCombined": "Paste a Naver Maps share URL, or enter a business name to automatically retrieve information.",
"errorInputRequired": "Please enter a business name or URL.",
"errorInvalidUrl": "Invalid URL format. (e.g., https://naver.me/abcdef)",
"analyzeButton": "Brand Analysis",
"scrollMore": "Scroll to see more",
"searching": "Searching...",
"searchTypeManual": "Not on Naver? Enter manually",
"manualButtonDesc": "Even businesses not listed on Naver Maps/Place can get\na full brand analysis, lyrics, music, and video\njust by entering the name, region, and address.",
"manualButtonDesc": "Even businesses not listed on Naver Maps/Place\ncan get lyrics, music, and video production\njust by entering the name, region, and address.",
"manualModalTitle": "Enter Business Info Directly",
"manualModalSubtitle": "Even businesses not on Naver can start brand analysis by filling in the details below.",
"manualModalSubtitle": "Even businesses not on Naver can get lyrics, music, and video production by filling in the details below.",
"manualAnalyzeButton": "Get Started",
"manualLabelName": "Business Name",
"manualLabelAddress": "Address",
@ -278,7 +278,7 @@
"genreLabel": "Select Genre",
"genreAuto": "Auto Select",
"genreBallad": "Ballad",
"languageLabel": "Language",
"languageLabel": "Select Language",
"languageKorean": "Korean",
"lyricsColumn": "Lyrics",
"lyricsHint": "Select the lyrics area to edit",
@ -307,7 +307,7 @@
"songRegenerationError": "An error occurred during music regeneration.",
"creditsRemaining": "{{count}} left",
"creditsExhausted": "Not enough credits.",
"chargeCredits": "Purchase credits"
"chargeCredits": "Top Up Credits"
},
"completion": {
"back": "Go Back",
@ -549,9 +549,9 @@
"marketPositioning": "Market Positioning",
"coreValue": "Core Value",
"categoryDefinition": "Category Definition",
"targetPersona": "Target Persona",
"targetPersona": "Key Customer Type",
"ageSuffix": "years old",
"sellingPoints": "Unique Selling Points (USP)",
"sellingPoints": "Key Selling Points",
"recommendedKeywords": "Recommended Target Keywords",
"generateContent": "Generate Content",
"pageDescBefore": " reveals ",
@ -592,7 +592,7 @@
"loading": "Loading...",
"noResults": "No recent results",
"noResultsDesc": "Upload your content to social channels",
"ado2Contents": "ADO2 Contents",
"myContents": "My Contents",
"cancel": "Cancel",
"retry": "Retry",
"cancelConfirm": "Are you sure you want to cancel this reservation?",
@ -604,7 +604,33 @@
},
"loginPrompt": {
"title": "Login Required",
"loginBtn": "Login with Kakao"
"loginBtn": "Login"
},
"videoDetail": {
"dateFormat": "{{month}}/{{day}}/{{year}}",
"kakaoDefaultTitle": "ADO2 Video",
"kakaoDescription": "{{region}} · ADO2 AI Marketing Video",
"kakaoButtonTitle": "Watch Video",
"deletedComment": "(This comment has been deleted.)",
"closeAriaLabel": "Close",
"share": "Share",
"copied": "Copied!",
"shareKakao": "KakaoTalk",
"shareFacebook": "Facebook",
"shareTwitter": "X (Twitter)",
"copyUrl": "Copy URL",
"commentsTitle": "Comments",
"changeAvatarTitle": "Click to change avatar",
"nicknamePlaceholder": "Author name",
"commentPlaceholder": "Write a comment...",
"commentLoginRequired": "Please log in to write a comment",
"commentSubmitting": "Submitting",
"commentSubmit": "Submit",
"noComments": "No comments yet.",
"anonymous": "Anonymous",
"deleteComment": "Delete",
"loadMoreComments": "Load more comments",
"loadingComments": "Loading..."
},
"app": {
"loginProcessing": "Processing login...",

View File

@ -591,7 +591,7 @@
"loading": "불러오는 중...",
"noResults": "최근 결과 없음",
"noResultsDesc": "제작한 콘텐츠를 소셜 채널에 업로드해 보세요",
"ado2Contents": "ADO2 콘텐츠",
"myContents": "내 콘텐츠",
"cancel": "취소",
"retry": "재시도",
"cancelConfirm": "예약을 취소하시겠습니까?",
@ -605,6 +605,32 @@
"title": "로그인이 필요합니다.",
"loginBtn": "로그인"
},
"videoDetail": {
"dateFormat": "{{year}}년 {{month}}월 {{day}}일",
"kakaoDefaultTitle": "ADO2 영상",
"kakaoDescription": "{{region}} · ADO2 AI 마케팅 영상",
"kakaoButtonTitle": "영상 보기",
"deletedComment": "(삭제된 댓글입니다.)",
"closeAriaLabel": "닫기",
"share": "공유하기",
"copied": "복사됨!",
"shareKakao": "카카오톡",
"shareFacebook": "페이스북",
"shareTwitter": "X (트위터)",
"copyUrl": "URL 복사",
"commentsTitle": "댓글",
"changeAvatarTitle": "클릭하여 아바타 변경",
"nicknamePlaceholder": "작성자 이름",
"commentPlaceholder": "댓글을 입력하세요...",
"commentLoginRequired": "로그인 후 댓글을 작성할 수 있습니다",
"commentSubmitting": "작성 중",
"commentSubmit": "작성",
"noComments": "아직 댓글이 없습니다.",
"anonymous": "익명",
"deleteComment": "삭제",
"loadMoreComments": "댓글 더 보기",
"loadingComments": "불러오는 중..."
},
"app": {
"loginProcessing": "로그인 처리 중...",
"loginFailed": "로그인 처리에 실패했습니다. 다시 시도해주세요.",

View File

@ -476,11 +476,11 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
<div className="comp2-info-header">
<span className="comp2-info-label">{t('completion.contentInfo')}</span>
</div>
<div className="comp2-info-content">
<div className="comp2-file-info">
<h3 className="comp2-filename">{getFileName()}</h3>
{/* <p className="comp2-filesize">19.6MB</p> */}
</div>
<div className="comp2-info-content">
<div className="comp2-meta-grid">
<div className="comp2-meta-item">
<span className="comp2-meta-label">{t('completion.genre')} : {songCompletionData?.genre || 'K-POP'}</span>

View File

@ -645,7 +645,7 @@ const ContentCalendarContent: React.FC<ContentCalendarContentProps> = ({ onNavig
</p>
</div>
<button
onClick={() => onNavigate?.('ADO2 콘텐츠')}
onClick={() => onNavigate?.(' 콘텐츠')}
style={{
height: 34, padding: '0 10px', borderRadius: 8, border: 'none',
backgroundColor: '#a65eff', cursor: 'pointer',
@ -653,7 +653,7 @@ const ContentCalendarContent: React.FC<ContentCalendarContentProps> = ({ onNavig
color: '#ffffff',
}}
>
{t('contentCalendar.ado2Contents')}
{t('contentCalendar.myContents')}
</button>
</div>
);
@ -717,7 +717,7 @@ const ContentCalendarContent: React.FC<ContentCalendarContentProps> = ({ onNavig
</div>
{/* 바텀시트 */}
<div style={{ position: 'fixed', bottom: 0, left: 0, right: 0, zIndex: 100, display: 'flex', flexDirection: 'column' }}>
<div style={{ position: 'fixed', bottom: 0, left: 0, right: 0, zIndex: 10, display: 'flex', flexDirection: 'column' }}>
<div style={{
backgroundColor: '#01393b',
borderTop: '1px solid #046266',

View File

@ -301,7 +301,7 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
.then(res => res.json())
.then((data: CrawlingResponse) => {
// 언어 변경 시에는 기존 m_id 유지
const tagged = { ...data, m_id: parsed.m_id || generateRandomMId(), _isTestData: true };
const tagged = { ...data, m_id: parsed.m_id, _isTestData: true };
setAnalysisData(tagged);
localStorage.setItem(ANALYSIS_DATA_KEY, JSON.stringify(tagged));
})
@ -646,7 +646,7 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
<Sidebar activeItem={activeItem} onNavigate={handleNavigate} onHome={handleHome} userInfo={userInfo} onLogout={handleLogout} credits={credits} tutorialAvailable={!!getCurrentTutorialKey()} tutorialEnabled={tutorial.isEnabled} onToggleTutorial={() => tutorial.toggleTutorial(getCurrentTutorialKey())} />
)}
{tutorialUI}
<div className={`flex-1 relative pl-0 md:pl-0 ${needsScroll ? 'overflow-auto' : 'h-full overflow-hidden'}`}>
<div className={`flex-1 relative pl-0 md:pl-0 bg-[#001a1c] ${needsScroll ? 'overflow-auto' : 'h-full overflow-hidden'}`}>
{renderContent()}
</div>
</div>

View File

@ -31,6 +31,27 @@ const MAX_RETRY_COUNT = 3;
// GenerationFlow의 clearAllProjectStorage가 이 키를 포함해 정리함
const SONG_GENERATION_KEY = 'castad_song_generation';
const GENRE_VALUES = ['kpop', 'pop', 'ballad', 'hip-hop', 'rnb', 'edm', 'jazz', 'rock'];
const genreMap: Record<string, string> = {
'자동 선택': '', // 런타임에 랜덤 결정
'K-POP': 'kpop',
'POP': 'pop',
'발라드': 'ballad',
'Hip-Hop': 'hip-hop',
'R&B': 'rnb',
'EDM': 'edm',
'JAZZ': 'jazz',
'ROCK': 'rock',
};
const getEffectiveGenre = (selectedGenre: string): string => {
if (selectedGenre === '자동 선택') {
return GENRE_VALUES[Math.floor(Math.random() * GENRE_VALUES.length)];
}
return genreMap[selectedGenre] || 'pop';
};
const sanitizeError = (message: string, fallback: string): string => {
if (
message.includes('Traceback') ||
@ -69,6 +90,7 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
const [selectedType, setSelectedType] = useState('보컬');
const [selectedLang, setSelectedLang] = useState('한국어');
const [selectedGenre, setSelectedGenre] = useState('자동 선택');
const [usedGenre, setUsedGenre] = useState('');
const [progress, setProgress] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const [status, setStatus] = useState<GenerationStatus>('idle');
@ -88,7 +110,7 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
const saveSongCompletionData = () => {
const completionData = {
businessName: businessInfo?.customer_name || '알 수 없음',
genre: selectedGenre === '자동 선택' ? 'K-POP' : selectedGenre,
genre: selectedGenre,
lyrics: lyrics,
timestamp: Date.now(),
};
@ -211,20 +233,13 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
try {
const language = LANGUAGE_MAP[selectedLang] || 'Korean';
const genreMap: Record<string, string> = {
'자동 선택': 'pop',
'K-POP': 'kpop',
'발라드': 'ballad',
'Hip-Hop': 'hip-hop',
'R&B': 'rnb',
'EDM': 'edm',
'JAZZ': 'jazz',
'ROCK': 'rock',
};
const isInstrumental = selectedType === '배경음악';
const effectiveGenre = getEffectiveGenre(selectedGenre);
setUsedGenre(effectiveGenre);
const songResponse = await generateSong(imageTaskId, {
genre: genreMap[selectedGenre] || 'pop',
genre: effectiveGenre,
...(isInstrumental ? { instrumental: true, lyrics: '' } : { language, lyrics: currentLyrics }),
});
@ -371,17 +386,6 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
throw new Error(lyricResponse.error_message || t('soundStudio.lyricGenerationFailed'));
}
const genreMap: Record<string, string> = {
'자동 선택': 'pop',
'K-POP': 'kpop',
'발라드': 'ballad',
'Hip-Hop': 'hip-hop',
'R&B': 'rnb',
'EDM': 'edm',
'JAZZ': 'jazz',
'ROCK': 'rock',
};
let songLyrics: string | undefined;
if (!isInstrumental) {
@ -412,8 +416,11 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
setStatus('generating_song');
setStatusMessage(t('soundStudio.generatingSong'));
const effectiveGenre = getEffectiveGenre(selectedGenre);
setUsedGenre(effectiveGenre);
const songResponse = await generateSong(imageTaskId, {
genre: genreMap[selectedGenre] || 'pop',
genre: effectiveGenre,
...(isInstrumental ? { instrumental: true, lyrics: '' } : { language, lyrics: songLyrics }),
});
@ -552,7 +559,7 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
))}
</div>
<div className="genre-row">
{['JAZZ', 'ROCK'].map(genre => (
{['JAZZ', 'ROCK', 'POP'].map(genre => (
<button
key={genre}
onClick={() => setSelectedGenre(genre)}
@ -562,7 +569,6 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
{genre}
</button>
))}
<div className="genre-btn-placeholder"></div>
</div>
</div>
</div>

View File

@ -310,6 +310,7 @@
color: #379599;
letter-spacing: -0.006em;
line-height: 1.19;
text-align: center;
}
.footer-links {

View File

@ -626,7 +626,7 @@
border: none;
color: #9BCACC;
cursor: pointer;
padding: 4px;
padding-bottom: 8px;
margin-left: auto;
transition: color 0.2s;
}
@ -647,7 +647,7 @@
}
.video-detail-header {
margin-bottom: 24px;
/* margin-bottom: 24px; */
}
.video-detail-back-btn {
@ -685,7 +685,7 @@
display: block;
width: 100%;
height: auto;
max-width: 450px;
max-width: 360px;
border-radius: 12px;
flex-shrink: 0;
}

View File

@ -9,7 +9,7 @@
padding: 0.75rem;
display: flex;
flex-direction: column;
background-color: var(--color-bg-dark);
background-color: var(--color-bg-darker);
color: var(--color-text-white);
}

View File

@ -563,7 +563,7 @@
}
.comp2-title-row {
padding: 16px;
padding: 10px;
text-align: center;
flex-shrink: 0;
}
@ -2389,7 +2389,7 @@
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #002224;
background-color: var(--color-bg-darker);
color: var(--color-text-white);
padding: 80px 1rem 100px;
position: relative;
@ -2405,6 +2405,7 @@
position: relative;
margin-top: 50px;
width: 100%;
max-width: 375px;
}
.url-input-icon {
@ -2418,9 +2419,28 @@
.url-input-logo img {
width: 350px;
max-width: 100%;
height: auto;
}
@media (min-width: 640px) {
.url-input-logo img {
width: 400px;
}
}
@media (min-width: 768px) {
.url-input-logo img {
width: 500px;
}
}
@media (min-width: 1024px) {
.url-input-logo img {
width: 554px;
}
}
.url-input-title {
font-family: 'Pretendard', sans-serif;
font-size: 40px;

View File

@ -63,10 +63,11 @@
z-index: 20;
position: relative;
margin-top: 50px;
width: 100%;
}
.hero-logo {
width: 280px;
width: 350px;
height: auto;
margin-bottom: 1rem;
}
@ -396,7 +397,7 @@
.hero-manual-card {
width: 100%;
padding: 16px 20px;
padding: 10px 16px;
border-radius: 16px;
border: 1.5px solid var(--color-mint);
background: rgba(15, 60, 60, 0.5);
@ -451,7 +452,7 @@
/* Scroll Indicator */
.scroll-indicator {
position: absolute;
bottom: 2rem;
bottom: 1rem;
display: flex;
flex-direction: column;
align-items: center;
@ -466,7 +467,7 @@
@media (min-width: 640px) {
.scroll-indicator {
bottom: 3rem;
bottom: 1rem;
}
}
@ -779,7 +780,7 @@
}
.feature-card-description {
font-size: 12px;
font-size: 14px;
font-weight: 400;
color: #E5F1F2;
letter-spacing: -0.006em;
@ -1486,24 +1487,12 @@
max-width: 600px;
}
@media (min-width: 768px) {
.loading-content {
gap: 4rem;
}
}
/* Loading Logo */
.loading-logo {
width: 200px;
width: 300px;
height: auto;
}
@media (min-width: 768px) {
.loading-logo {
width: 308px;
}
}
.loading-logo img {
width: 100%;
height: auto;
@ -1520,7 +1509,7 @@
}
.loading-title {
font-size: 1.5rem;
font-size: 2rem;
font-weight: 700;
letter-spacing: -0.006em;
line-height: 1.19;
@ -1528,12 +1517,6 @@
text-align: center;
}
@media (min-width: 768px) {
.loading-title {
font-size: 2rem;
}
}
/* Loading Progress Bar */
.loading-progress-wrapper {
display: flex;
@ -1567,19 +1550,13 @@
/* Loading Spinner Wrapper */
.loading-spinner-wrapper {
width: 120px;
height: 120px;
width: 200px;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
@media (min-width: 768px) {
.loading-spinner-wrapper {
width: 200px;
height: 200px;
}
}
.loading-spinner-icon {
width: 100%;

View File

@ -1315,7 +1315,7 @@
}
.asset-scroll-area {
padding: 80px 1rem 120px;
padding: 1rem 1rem 120px;
}
.asset-image-grid {
@ -1330,7 +1330,7 @@
}
.asset-scroll-area {
padding: 80px 1.5rem 120px;
padding: 1.5rem 1.5rem 120px;
}
}