장르 선택 로직 개선

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} onAnalyze={handleStartAnalysis}
onAutocomplete={handleAutocomplete} onAutocomplete={handleAutocomplete}
onManualInput={handleManualInput} onManualInput={handleManualInput}
onTestData={handleTestData}
onNext={() => scrollToSection(1)} onNext={() => scrollToSection(1)}
error={error} error={error}
scrollProgress={scrollProgress} scrollProgress={scrollProgress}

View File

@ -94,7 +94,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
const date = new Date(dateString); 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) => { const formatCommentDate = (dateString: string) => {
@ -120,12 +120,12 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
kakao.Share.sendDefault({ kakao.Share.sendDefault({
objectType: 'feed', objectType: 'feed',
content: { content: {
title: video?.store_name ?? 'ADO2 영상', title: video?.store_name ?? t('videoDetail.kakaoDefaultTitle'),
description: `${video?.region ?? ''} · ADO2 AI 마케팅 영상`, description: t('videoDetail.kakaoDescription', { region: video?.region ?? '' }),
imageUrl: 'https://demo.castad.net/favicon_48.svg', imageUrl: 'https://demo.castad.net/favicon_48.svg',
link: { mobileWebUrl: shareUrl, webUrl: shareUrl }, 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) { } else if (navigator.share) {
navigator.share({ url: shareUrl }).catch(() => {}); navigator.share({ url: shareUrl }).catch(() => {});
@ -222,7 +222,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
}; };
const renderCommentContent = (content: string | null, isDeleted: boolean) => { 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; return content;
}; };
@ -231,7 +231,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
{/* 헤더 */} {/* 헤더 */}
<div className="video-detail-header"> <div className="video-detail-header">
{isModal ? ( {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"> <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"/> <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg> </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"/> <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"/> <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> </svg>
{copied ? '복사됨!' : '공유하기'} {copied ? t('videoDetail.copied') : t('videoDetail.share')}
</button> </button>
{shareMenuOpen && ( {shareMenuOpen && (
<div className="video-detail-share-menu"> <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"/> <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"/> <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> </svg>
{t('videoDetail.shareKakao')}
</button> </button>
{/* 페이스북 */} {/* 페이스북 */}
<button className="video-detail-share-item" onClick={handleFacebookShare}> <button className="video-detail-share-item" onClick={handleFacebookShare}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="#1877F2"> <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"/> <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> </svg>
{t('videoDetail.shareFacebook')}
</button> </button>
{/* X (트위터) */} {/* X (트위터) */}
<button className="video-detail-share-item" onClick={handleTwitterShare}> <button className="video-detail-share-item" onClick={handleTwitterShare}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> <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"/> <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> </svg>
X () {t('videoDetail.shareTwitter')}
</button> </button>
{/* URL 복사 */} {/* URL 복사 */}
<button className="video-detail-share-item" onClick={() => { handleCopyLink(); setShareMenuOpen(false); }}> <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"/> <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"/> <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg> </svg>
{copied ? '복사됨!' : 'URL 복사'} {copied ? t('videoDetail.copied') : t('videoDetail.copyUrl')}
</button> </button>
</div> </div>
)} )}
@ -335,7 +335,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
{/* 댓글 섹션 */} {/* 댓글 섹션 */}
<div className="video-detail-comments"> <div className="video-detail-comments">
<div className="video-detail-comments-header"> <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> <span className="video-detail-comments-count">{commentsTotal}</span>
</div> </div>
@ -344,16 +344,16 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
<div className="video-detail-comment-profile"> <div className="video-detail-comment-profile">
<img <img
src={`https://api.dicebear.com/9.x/pixel-art/svg?seed=${commentAvatarSeed}`} src={`https://api.dicebear.com/9.x/pixel-art/svg?seed=${commentAvatarSeed}`}
alt="아바타 변경" alt={t('videoDetail.changeAvatarTitle')}
className="video-detail-comment-avatar" className="video-detail-comment-avatar"
onClick={handleChangeAvatar} onClick={handleChangeAvatar}
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
title="클릭하여 아바타 변경" title={t('videoDetail.changeAvatarTitle')}
/> />
<input <input
className="video-detail-nickname-input" className="video-detail-nickname-input"
type="text" type="text"
placeholder="작성자 이름" placeholder={t('videoDetail.nicknamePlaceholder')}
value={commentNickname} value={commentNickname}
onChange={(e) => setCommentNickname(e.target.value)} onChange={(e) => setCommentNickname(e.target.value)}
maxLength={20} maxLength={20}
@ -365,7 +365,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
<textarea <textarea
ref={commentTextareaRef} ref={commentTextareaRef}
className="video-detail-comment-input" className="video-detail-comment-input"
placeholder={authed ? '댓글을 입력하세요...' : '로그인 후 댓글을 작성할 수 있습니다'} placeholder={authed ? t('videoDetail.commentPlaceholder') : t('videoDetail.commentLoginRequired')}
maxLength={500} maxLength={500}
rows={1} rows={1}
value={commentInput} value={commentInput}
@ -388,12 +388,12 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
onClick={handleCommentSubmit} onClick={handleCommentSubmit}
disabled={!authed || !commentInput.trim() || commentSubmitting} disabled={!authed || !commentInput.trim() || commentSubmitting}
> >
{commentSubmitting ? '작성 중' : '작성'} {commentSubmitting ? t('videoDetail.commentSubmitting') : t('videoDetail.commentSubmit')}
</button> </button>
</div> </div>
{comments.length === 0 && !commentsLoading ? ( {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"> <ul className="video-detail-comment-list">
{comments.map((c) => ( {comments.map((c) => (
@ -405,7 +405,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
/> />
<div className="video-detail-comment-body"> <div className="video-detail-comment-body">
<span className="video-detail-comment-nickname"> <span className="video-detail-comment-nickname">
{c.nickname || '익명'} {c.nickname || t('videoDetail.anonymous')}
</span> </span>
<p className="video-detail-comment-text"> <p className="video-detail-comment-text">
{renderCommentContent(c.content, c.is_deleted)} {renderCommentContent(c.content, c.is_deleted)}
@ -417,7 +417,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
className="video-detail-comment-delete" className="video-detail-comment-delete"
onClick={() => handleDeleteComment(c.id)} onClick={() => handleDeleteComment(c.id)}
> >
{t('videoDetail.deleteComment')}
</button> </button>
)} )}
</div> </div>
@ -434,7 +434,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
/> />
<div className="video-detail-comment-body"> <div className="video-detail-comment-body">
<span className="video-detail-comment-nickname"> <span className="video-detail-comment-nickname">
{r.nickname || '익명'} {r.nickname || t('videoDetail.anonymous')}
</span> </span>
<p className="video-detail-comment-text"> <p className="video-detail-comment-text">
{renderCommentContent(r.content, r.is_deleted)} {renderCommentContent(r.content, r.is_deleted)}
@ -446,7 +446,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
className="video-detail-comment-delete" className="video-detail-comment-delete"
onClick={() => handleDeleteComment(r.id)} onClick={() => handleDeleteComment(r.id)}
> >
{t('videoDetail.deleteComment')}
</button> </button>
)} )}
</div> </div>
@ -467,7 +467,7 @@ const VideoDetailContent: React.FC<VideoDetailContentProps> = ({ videoId, isModa
onClick={() => fetchComments(commentsPage + 1, true)} onClick={() => fetchComments(commentsPage + 1, true)}
disabled={commentsLoading} disabled={commentsLoading}
> >
{commentsLoading ? '불러오는 중...' : '댓글 더 보기'} {commentsLoading ? t('videoDetail.loadingComments') : t('videoDetail.loadMoreComments')}
</button> </button>
)} )}
</div> </div>

View File

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

View File

@ -54,7 +54,7 @@
"video": { "title": "Generate Video", "desc": "Click the button to start generating your video." } "video": { "title": "Generate Video", "desc": "Click the button to start generating your video." }
}, },
"completion": { "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." }, "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?" }, "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." } "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": { "hero": {
"searchTypeLabel": "Business Name | URL", "searchTypeLabel": "Business Name | URL",
"placeholderCombined": "Enter a business name or Naver Maps/Place 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.", "errorInputRequired": "Please enter a business name or URL.",
"errorInvalidUrl": "Invalid URL format. (e.g., https://naver.me/abcdef)", "errorInvalidUrl": "Invalid URL format. (e.g., https://naver.me/abcdef)",
"analyzeButton": "Brand Analysis", "analyzeButton": "Brand Analysis",
"scrollMore": "Scroll to see more", "scrollMore": "Scroll to see more",
"searching": "Searching...", "searching": "Searching...",
"searchTypeManual": "Not on Naver? Enter manually", "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", "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", "manualAnalyzeButton": "Get Started",
"manualLabelName": "Business Name", "manualLabelName": "Business Name",
"manualLabelAddress": "Address", "manualLabelAddress": "Address",
@ -278,7 +278,7 @@
"genreLabel": "Select Genre", "genreLabel": "Select Genre",
"genreAuto": "Auto Select", "genreAuto": "Auto Select",
"genreBallad": "Ballad", "genreBallad": "Ballad",
"languageLabel": "Language", "languageLabel": "Select Language",
"languageKorean": "Korean", "languageKorean": "Korean",
"lyricsColumn": "Lyrics", "lyricsColumn": "Lyrics",
"lyricsHint": "Select the lyrics area to edit", "lyricsHint": "Select the lyrics area to edit",
@ -307,7 +307,7 @@
"songRegenerationError": "An error occurred during music regeneration.", "songRegenerationError": "An error occurred during music regeneration.",
"creditsRemaining": "{{count}} left", "creditsRemaining": "{{count}} left",
"creditsExhausted": "Not enough credits.", "creditsExhausted": "Not enough credits.",
"chargeCredits": "Purchase credits" "chargeCredits": "Top Up Credits"
}, },
"completion": { "completion": {
"back": "Go Back", "back": "Go Back",
@ -549,9 +549,9 @@
"marketPositioning": "Market Positioning", "marketPositioning": "Market Positioning",
"coreValue": "Core Value", "coreValue": "Core Value",
"categoryDefinition": "Category Definition", "categoryDefinition": "Category Definition",
"targetPersona": "Target Persona", "targetPersona": "Key Customer Type",
"ageSuffix": "years old", "ageSuffix": "years old",
"sellingPoints": "Unique Selling Points (USP)", "sellingPoints": "Key Selling Points",
"recommendedKeywords": "Recommended Target Keywords", "recommendedKeywords": "Recommended Target Keywords",
"generateContent": "Generate Content", "generateContent": "Generate Content",
"pageDescBefore": " reveals ", "pageDescBefore": " reveals ",
@ -592,7 +592,7 @@
"loading": "Loading...", "loading": "Loading...",
"noResults": "No recent results", "noResults": "No recent results",
"noResultsDesc": "Upload your content to social channels", "noResultsDesc": "Upload your content to social channels",
"ado2Contents": "ADO2 Contents", "myContents": "My Contents",
"cancel": "Cancel", "cancel": "Cancel",
"retry": "Retry", "retry": "Retry",
"cancelConfirm": "Are you sure you want to cancel this reservation?", "cancelConfirm": "Are you sure you want to cancel this reservation?",
@ -604,7 +604,33 @@
}, },
"loginPrompt": { "loginPrompt": {
"title": "Login Required", "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": { "app": {
"loginProcessing": "Processing login...", "loginProcessing": "Processing login...",

View File

@ -591,7 +591,7 @@
"loading": "불러오는 중...", "loading": "불러오는 중...",
"noResults": "최근 결과 없음", "noResults": "최근 결과 없음",
"noResultsDesc": "제작한 콘텐츠를 소셜 채널에 업로드해 보세요", "noResultsDesc": "제작한 콘텐츠를 소셜 채널에 업로드해 보세요",
"ado2Contents": "ADO2 콘텐츠", "myContents": "내 콘텐츠",
"cancel": "취소", "cancel": "취소",
"retry": "재시도", "retry": "재시도",
"cancelConfirm": "예약을 취소하시겠습니까?", "cancelConfirm": "예약을 취소하시겠습니까?",
@ -605,6 +605,32 @@
"title": "로그인이 필요합니다.", "title": "로그인이 필요합니다.",
"loginBtn": "로그인" "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": { "app": {
"loginProcessing": "로그인 처리 중...", "loginProcessing": "로그인 처리 중...",
"loginFailed": "로그인 처리에 실패했습니다. 다시 시도해주세요.", "loginFailed": "로그인 처리에 실패했습니다. 다시 시도해주세요.",

View File

@ -476,11 +476,11 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
<div className="comp2-info-header"> <div className="comp2-info-header">
<span className="comp2-info-label">{t('completion.contentInfo')}</span> <span className="comp2-info-label">{t('completion.contentInfo')}</span>
</div> </div>
<div className="comp2-info-content">
<div className="comp2-file-info"> <div className="comp2-file-info">
<h3 className="comp2-filename">{getFileName()}</h3> <h3 className="comp2-filename">{getFileName()}</h3>
{/* <p className="comp2-filesize">19.6MB</p> */} {/* <p className="comp2-filesize">19.6MB</p> */}
</div> </div>
<div className="comp2-info-content">
<div className="comp2-meta-grid"> <div className="comp2-meta-grid">
<div className="comp2-meta-item"> <div className="comp2-meta-item">
<span className="comp2-meta-label">{t('completion.genre')} : {songCompletionData?.genre || 'K-POP'}</span> <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> </p>
</div> </div>
<button <button
onClick={() => onNavigate?.('ADO2 콘텐츠')} onClick={() => onNavigate?.(' 콘텐츠')}
style={{ style={{
height: 34, padding: '0 10px', borderRadius: 8, border: 'none', height: 34, padding: '0 10px', borderRadius: 8, border: 'none',
backgroundColor: '#a65eff', cursor: 'pointer', backgroundColor: '#a65eff', cursor: 'pointer',
@ -653,7 +653,7 @@ const ContentCalendarContent: React.FC<ContentCalendarContentProps> = ({ onNavig
color: '#ffffff', color: '#ffffff',
}} }}
> >
{t('contentCalendar.ado2Contents')} {t('contentCalendar.myContents')}
</button> </button>
</div> </div>
); );
@ -717,7 +717,7 @@ const ContentCalendarContent: React.FC<ContentCalendarContentProps> = ({ onNavig
</div> </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={{ <div style={{
backgroundColor: '#01393b', backgroundColor: '#01393b',
borderTop: '1px solid #046266', borderTop: '1px solid #046266',

View File

@ -301,7 +301,7 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
.then(res => res.json()) .then(res => res.json())
.then((data: CrawlingResponse) => { .then((data: CrawlingResponse) => {
// 언어 변경 시에는 기존 m_id 유지 // 언어 변경 시에는 기존 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); setAnalysisData(tagged);
localStorage.setItem(ANALYSIS_DATA_KEY, JSON.stringify(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())} /> <Sidebar activeItem={activeItem} onNavigate={handleNavigate} onHome={handleHome} userInfo={userInfo} onLogout={handleLogout} credits={credits} tutorialAvailable={!!getCurrentTutorialKey()} tutorialEnabled={tutorial.isEnabled} onToggleTutorial={() => tutorial.toggleTutorial(getCurrentTutorialKey())} />
)} )}
{tutorialUI} {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()} {renderContent()}
</div> </div>
</div> </div>

View File

@ -31,6 +31,27 @@ const MAX_RETRY_COUNT = 3;
// GenerationFlow의 clearAllProjectStorage가 이 키를 포함해 정리함 // GenerationFlow의 clearAllProjectStorage가 이 키를 포함해 정리함
const SONG_GENERATION_KEY = 'castad_song_generation'; 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 => { const sanitizeError = (message: string, fallback: string): string => {
if ( if (
message.includes('Traceback') || message.includes('Traceback') ||
@ -69,6 +90,7 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
const [selectedType, setSelectedType] = useState('보컬'); const [selectedType, setSelectedType] = useState('보컬');
const [selectedLang, setSelectedLang] = useState('한국어'); const [selectedLang, setSelectedLang] = useState('한국어');
const [selectedGenre, setSelectedGenre] = useState('자동 선택'); const [selectedGenre, setSelectedGenre] = useState('자동 선택');
const [usedGenre, setUsedGenre] = useState('');
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [status, setStatus] = useState<GenerationStatus>('idle'); const [status, setStatus] = useState<GenerationStatus>('idle');
@ -88,7 +110,7 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
const saveSongCompletionData = () => { const saveSongCompletionData = () => {
const completionData = { const completionData = {
businessName: businessInfo?.customer_name || '알 수 없음', businessName: businessInfo?.customer_name || '알 수 없음',
genre: selectedGenre === '자동 선택' ? 'K-POP' : selectedGenre, genre: selectedGenre,
lyrics: lyrics, lyrics: lyrics,
timestamp: Date.now(), timestamp: Date.now(),
}; };
@ -211,20 +233,13 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
try { try {
const language = LANGUAGE_MAP[selectedLang] || 'Korean'; 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 isInstrumental = selectedType === '배경음악';
const effectiveGenre = getEffectiveGenre(selectedGenre);
setUsedGenre(effectiveGenre);
const songResponse = await generateSong(imageTaskId, { const songResponse = await generateSong(imageTaskId, {
genre: genreMap[selectedGenre] || 'pop', genre: effectiveGenre,
...(isInstrumental ? { instrumental: true, lyrics: '' } : { language, lyrics: currentLyrics }), ...(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')); 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; let songLyrics: string | undefined;
if (!isInstrumental) { if (!isInstrumental) {
@ -412,8 +416,11 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
setStatus('generating_song'); setStatus('generating_song');
setStatusMessage(t('soundStudio.generatingSong')); setStatusMessage(t('soundStudio.generatingSong'));
const effectiveGenre = getEffectiveGenre(selectedGenre);
setUsedGenre(effectiveGenre);
const songResponse = await generateSong(imageTaskId, { const songResponse = await generateSong(imageTaskId, {
genre: genreMap[selectedGenre] || 'pop', genre: effectiveGenre,
...(isInstrumental ? { instrumental: true, lyrics: '' } : { language, lyrics: songLyrics }), ...(isInstrumental ? { instrumental: true, lyrics: '' } : { language, lyrics: songLyrics }),
}); });
@ -552,7 +559,7 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
))} ))}
</div> </div>
<div className="genre-row"> <div className="genre-row">
{['JAZZ', 'ROCK'].map(genre => ( {['JAZZ', 'ROCK', 'POP'].map(genre => (
<button <button
key={genre} key={genre}
onClick={() => setSelectedGenre(genre)} onClick={() => setSelectedGenre(genre)}
@ -562,7 +569,6 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
{genre} {genre}
</button> </button>
))} ))}
<div className="genre-btn-placeholder"></div>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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