diff --git a/index.css b/index.css index 28087ec..dbea805 100644 --- a/index.css +++ b/index.css @@ -511,10 +511,24 @@ /* Bottom Button Container */ .bottom-button-container { + position: fixed; + bottom: 32px; + left: 0; + right: 0; display: flex; justify-content: center; - padding: 1.5rem; - flex-shrink: 0; + z-index: 30; + pointer-events: none; +} + +.bottom-button-container > * { + pointer-events: all; +} + +@media (min-width: 768px) { + .bottom-button-container { + left: 240px; + } } /* ===================================================== @@ -7336,13 +7350,45 @@ Sound Studio Styles ===================================================== */ +/* Sound Studio Page */ +.sound-studio-page { + height: 100%; + background-color: #002224; + overflow-y: auto; + overflow-x: hidden; + padding: 80px 1rem 120px; +} + +@media (min-width: 768px) { + .sound-studio-page { + padding: 80px 2rem 120px; + left: 240px; + } +} + /* Sound Studio Header */ .sound-studio-header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 64px; display: flex; - justify-content: space-between; align-items: center; - padding: 0.5rem 2rem; - gap: 1rem; + padding: 0 1rem; + z-index: 30; + pointer-events: none; +} + +.sound-studio-header > * { + pointer-events: all; +} + +@media (min-width: 768px) { + .sound-studio-header { + left: 240px; + padding: 0 2rem; + } } .btn-back-new { @@ -7426,28 +7472,35 @@ .sound-studio-title { font-size: 2rem; font-weight: 700; - color: #E5F1F2; - margin: 1rem auto 1.5rem auto; - padding: 0 2rem; - max-width: 1200px; + color: #ffffff; + text-align: center; width: 100%; + margin: 0 auto 1rem; + padding: 0; line-height: 1.19; letter-spacing: -0.006em; } /* Sound Studio Container */ .sound-studio-container { + height: 96%; + min-height: 600px; background-color: #01393B; - border-radius: 40px; - padding: 2rem; - margin: 0 auto 1.5rem auto; - max-width: 1136px; - width: calc(100% - 4rem); + border-radius: 24px; + padding: 1.25rem 1rem; + margin: 0 auto; + max-width: 1440px; + width: 100%; display: flex; flex-direction: column; gap: 1.25rem; - flex: 1; - min-height: 0; +} + +@media (min-width: 768px) { + .sound-studio-container { + border-radius: 40px; + padding: 2rem; + } } /* Sound Studio Columns */ @@ -7466,10 +7519,15 @@ width: 100%; display: flex; flex-direction: column; + align-items: center; gap: 1.25rem; flex-shrink: 0; } +.sound-column > *:not(.btn-generate-sound):not(.error-message-new):not(.status-message-new) { + width: 100%; +} + /* Lyrics Column */ .lyrics-column { width: 100%; @@ -7794,12 +7852,12 @@ background-color: #002224; border-radius: 8px; padding: 1rem; - min-height: 200px; overflow: hidden; } .lyrics-textarea { width: 100%; + height: 200px; background: transparent; border: none; color: var(--color-text-white); @@ -7825,12 +7883,13 @@ /* Generate Sound Button */ .btn-generate-sound { - width: 100%; - padding: 0.625rem 2.5rem; - background-color: #01393B; - border: 1px solid var(--color-mint); + height: 48px; + min-width: 120px; + padding: 0.625rem 1.25rem; + background-color: #94FBE0; + border: none; border-radius: var(--radius-full); - color: var(--color-mint); + color: #000000; font-size: var(--text-sm); font-weight: 600; cursor: pointer; @@ -7844,8 +7903,7 @@ } .btn-generate-sound:hover:not(.disabled) { - background-color: var(--color-mint); - color: #000000; + background-color: #6ef5ca; } .btn-generate-sound.disabled { @@ -7942,27 +8000,10 @@ /* Responsive Styles for Sound Studio */ @media (max-width: 639px) { - .sound-studio-header { - padding: 0.5rem 1rem; - flex-direction: column; - align-items: stretch; - } - .progress-indicator { width: 100%; } - .sound-studio-title { - font-size: 1.5rem; - padding: 0 1rem; - margin: 0.75rem auto 1rem auto; - } - - .sound-studio-container { - padding: 1.25rem; - width: calc(100% - 2rem); - } - .sound-type-grid { gap: 0.5rem; } @@ -7980,28 +8021,9 @@ padding: 0.5rem 0.75rem; font-size: var(--text-xs); } - - .lyrics-display { - min-height: 150px; - } } @media (min-width: 640px) and (max-width: 767px) { - .sound-studio-header { - padding: 0.5rem 1.5rem; - } - - .sound-studio-title { - font-size: 1.75rem; - padding: 0 1.5rem; - margin: 0.875rem auto 1.25rem auto; - } - - .sound-studio-container { - padding: 1.5rem; - width: calc(100% - 3rem); - } - .sound-studio-columns { gap: 2rem; } @@ -8014,21 +8036,15 @@ } @media (min-width: 1024px) { - .sound-studio-container { - padding: 2.5rem; - } - .sound-studio-columns { flex-direction: row; gap: 3rem; - overflow: hidden; } .sound-column { flex: 1; width: auto; min-height: 0; - overflow-y: auto; } .lyrics-column { @@ -8046,81 +8062,113 @@ } } -/* Height-based responsive adjustments */ -@media (max-height: 800px) { - .sound-studio-container { - gap: 1rem; - } - - .sound-studio-columns { - gap: 2rem; - } -} - -@media (max-height: 700px) { - .sound-studio-title { - font-size: 1.75rem; - margin: 0.75rem 2rem 1rem 2rem; - } - - .sound-studio-container { - padding: 1.5rem; - gap: 1rem; - } - - .sound-studio-columns { - gap: 1.5rem; - } - - .sound-studio-section { - gap: 0.5rem; - } - - .lyrics-display { - min-height: 180px; - } -} - -@media (max-height: 600px) { - .sound-studio-header { - padding: 0.375rem 2rem; - } - - .sound-studio-title { - font-size: 1.5rem; - margin: 0.5rem 2rem 0.75rem 2rem; - } - - .sound-studio-container { - padding: 1.25rem; - gap: 0.875rem; - } - - .sound-studio-columns { - gap: 1.25rem; - } - - .sound-type-btn, - .genre-btn { - padding: 0.625rem 0.75rem; - } - - .lyrics-display { - min-height: 150px; - } -} - /* ==================================== Asset Management (Brand Asset) Styles ==================================== */ -/* Asset Header */ -.asset-header { +/* Asset Page Layout */ +.asset-page { + height: 100%; + background-color: #002224; + position: relative; +} + +/* Fixed Header - 뒤로가기 */ +.asset-sticky-header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 64px; display: flex; - justify-content: space-between; align-items: center; - padding: 0.5rem 2rem; - margin-bottom: 1rem; + padding: 0 1rem; + z-index: 30; + pointer-events: none; +} + +.asset-sticky-header > * { + pointer-events: all; +} + +@media (min-width: 768px) { + .asset-sticky-header { + left: 240px; + padding: 0 2rem; + } +} + +/* 뒤로가기 버튼 */ +.asset-back-btn { + display: flex; + align-items: center; + gap: 4px; + background-color: #462E64; + color: #CFABFB; + font-size: 0.875rem; + font-weight: 600; + padding: 0 1.25rem 0 0.5rem; + height: 36px; + border: 1px solid #694596; + border-radius: 999px; + cursor: pointer; + white-space: nowrap; + transition: background-color 0.2s; + line-height: 1.19; + letter-spacing: -0.006em; +} + +.asset-back-btn:hover { + background-color: #5a3a80; +} + +/* Single Scroll Area */ +.asset-scroll-area { + height: 100%; + overflow-y: auto; + overflow-x: hidden; + padding: 80px 2rem 120px; +} + +.asset-scroll-area::-webkit-scrollbar { + width: 6px; +} + +.asset-scroll-area::-webkit-scrollbar-track { + background: transparent; +} + +.asset-scroll-area::-webkit-scrollbar-thumb { + background: #379599; + border-radius: 3px; +} + +.asset-scroll-area::-webkit-scrollbar-thumb:hover { + background: #4AABAF; +} + +/* Fixed Footer - 다음 단계 */ +.asset-sticky-footer { + position: fixed; + bottom: 32px; + left: 0; + right: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + z-index: 30; + pointer-events: none; +} + +.asset-sticky-footer > * { + pointer-events: all; +} + +@media (min-width: 768px) { + .asset-sticky-footer { + left: 240px; + } } /* Asset Title */ @@ -8128,35 +8176,26 @@ font-size: 2rem; font-weight: 700; color: #E5F1F2; - margin: 1rem auto 1.5rem auto; - padding: 0 2rem; - max-width: 1200px; - width: 100%; + padding: 0 0 1.5rem; + text-align: center; line-height: 1.19; letter-spacing: -0.006em; + margin: 0; } /* Asset Container */ .asset-container { - background-color: #01393B; - border-radius: 40px; - padding: 2rem; - margin: 0 auto 1.5rem auto; + margin: 0 auto; max-width: 1136px; - width: calc(100% - 4rem); + width: 100%; display: flex; flex-direction: column; - gap: 1.5rem; - flex: 1; - min-height: 0; - overflow-y: auto; - overflow-x: hidden; + gap: 1rem; } @media (min-width: 1024px) { .asset-container { flex-direction: row; - overflow: hidden; } } @@ -8164,18 +8203,24 @@ .asset-column { display: flex; flex-direction: column; - gap: 1.25rem; + gap: 1rem; } .asset-column-left { width: 100%; min-width: 0; flex-shrink: 0; + background-color: #01393B; + border-radius: 24px; + padding: 1.25rem 1rem; } .asset-column-right { width: 100%; flex-shrink: 0; + gap: 1rem; + display: flex; + flex-direction: column; } @media (min-width: 1024px) { @@ -8183,6 +8228,8 @@ flex: 1; min-height: 0; overflow: hidden; + border-radius: 40px; + padding: 2rem; } .asset-column-right { @@ -8190,50 +8237,91 @@ } } +/* Asset Section Header */ +.asset-section-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + flex-shrink: 0; +} + +.asset-section-header-left { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.asset-section-subtitle { + font-size: 0.875rem; + font-weight: 400; + color: #6AB0B3; + line-height: 1.19; + letter-spacing: -0.006em; +} + /* Asset Section Title */ .asset-section-title { - font-size: 1.125rem; + font-size: 1.5rem; font-weight: 600; color: #94FBE0; line-height: 1.19; - letter-spacing: -0.006em; + letter-spacing: -0.009em; margin: 0; } +/* Mobile Upload Button (visible only on mobile/tablet) */ +.asset-mobile-upload-btn { + display: flex; + align-items: center; + gap: 4px; + background-color: #94FBE0; + color: #000; + font-size: 0.75rem; + font-weight: 600; + padding: 0 10px; + height: 24px; + border: none; + border-radius: 999px; + cursor: pointer; + white-space: nowrap; + flex-shrink: 0; +} + +@media (min-width: 1024px) { + .asset-mobile-upload-btn { + display: none; + } +} + /* Asset Image List */ .asset-image-list { - min-height: 120px; - max-height: 200px; - overflow-y: auto; - overscroll-behavior: contain; background-color: #002224; border-radius: 8px; padding: 0.5rem; + max-height: 1080px; + overflow-y: auto; } -@media (min-width: 1024px) { - .asset-image-list { - flex: 1; - min-height: 0; - max-height: none; - } +/* Load More Button */ +.asset-load-more { + width: 100%; + padding: 0.625rem 1rem; + background-color: #002224; + color: #9BCACC; + font-size: 0.875rem; + font-weight: 600; + border: 1px solid #379599; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.2s, border-color 0.2s; + line-height: 1.19; + letter-spacing: -0.006em; } -.asset-image-list::-webkit-scrollbar { - width: 6px; -} - -.asset-image-list::-webkit-scrollbar-track { - background: transparent; -} - -.asset-image-list::-webkit-scrollbar-thumb { - background: #379599; - border-radius: 3px; -} - -.asset-image-list::-webkit-scrollbar-thumb:hover { - background: #4AABAF; +.asset-load-more:hover { + background-color: #003A3C; + border-color: #4AABAF; } /* Asset Image Grid */ @@ -8294,10 +8382,19 @@ /* Asset Upload Section */ .asset-upload-section { - display: flex; + display: none; flex-direction: column; - gap: 1.25rem; + gap: 1rem; flex: 1; + background-color: #01393B; + border-radius: 40px; + padding: 2rem; +} + +@media (min-width: 1024px) { + .asset-upload-section { + display: flex; + } } .asset-upload-zone { @@ -8332,20 +8429,31 @@ .asset-ratio-section { display: flex; flex-direction: column; - gap: 1.25rem; + gap: 1rem; + background-color: #01393B; + border-radius: 24px; + padding: 1.25rem 1rem; +} + +@media (min-width: 1024px) { + .asset-ratio-section { + border-radius: 40px; + padding: 2rem; + } } .asset-ratio-buttons { display: flex; flex-direction: column; - gap: 0.75rem; + gap: 0.5rem; } .asset-ratio-button { display: flex; align-items: center; gap: 1rem; - padding: 1rem; + padding: 0 1rem; + height: 48px; background-color: #002224; border: 1px solid transparent; border-radius: 8px; @@ -8356,6 +8464,7 @@ font-weight: 600; line-height: 1.19; letter-spacing: -0.006em; + width: 100%; } .asset-ratio-button:hover { @@ -8367,6 +8476,17 @@ background-color: #002224; } +.asset-ratio-label { + font-weight: 600; + color: #E5F1F2; +} + +.asset-ratio-subtitle { + font-size: 0.875rem; + font-weight: 400; + color: #9BCACC; +} + .asset-ratio-icon { width: 32px; height: 32px; @@ -8392,14 +8512,7 @@ border-radius: 4px; } -/* Asset Bottom Button */ -.asset-bottom { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.5rem; - padding: 0 2rem 1.5rem 2rem; -} +/* Asset Next Button */ .asset-next-button { background-color: #AE72F9; @@ -8426,83 +8539,36 @@ /* Responsive Styles */ @media (max-width: 639px) { - .asset-header { - padding: 0.5rem 1rem; - } - .asset-title { font-size: 1.5rem; - padding: 0 1rem; - margin: 0.75rem auto 1rem auto; + padding-bottom: 1rem; } - .asset-container { - padding: 1.25rem; - width: calc(100% - 2rem); - gap: 1.25rem; + .asset-scroll-area { + padding: 80px 1rem 120px; } .asset-image-grid { grid-template-columns: repeat(2, 1fr); } - - .asset-bottom { - padding: 0 1rem 1rem 1rem; - } } @media (min-width: 640px) and (max-width: 767px) { - .asset-header { - padding: 0.5rem 1.5rem; - } - .asset-title { font-size: 1.75rem; - padding: 0 1.5rem; - margin: 0.875rem auto 1.25rem auto; + padding-bottom: 1.25rem; } - .asset-container { - padding: 1.5rem; - width: calc(100% - 3rem); - } - - .asset-bottom { - padding: 0 1.5rem 1.25rem 1.5rem; - } -} - -@media (min-width: 768px) and (max-width: 1023px) { - .asset-container { - padding: 1.75rem; - } -} - -@media (min-width: 1024px) { - .asset-container { - padding: 2.5rem; + .asset-scroll-area { + padding: 80px 1.5rem 120px; } } /* Height-based responsive adjustments */ -@media (max-height: 800px) { - .asset-container { - padding: 1.75rem; - } - - .asset-upload-zone { - min-height: 120px; - } -} - @media (max-height: 700px) { - .asset-container { - padding: 1.5rem; - } - .asset-title { font-size: 1.75rem; - margin: 0.75rem auto 1rem auto; + padding-bottom: 0.75rem; } .asset-upload-zone { @@ -8511,20 +8577,6 @@ } @media (max-height: 600px) { - .asset-container { - padding: 1.25rem; - gap: 0.875rem; - } - - .asset-column { - gap: 1rem; - } - - .asset-upload-section, - .asset-ratio-section { - gap: 1rem; - } - .asset-upload-zone { min-height: 80px; } diff --git a/src/locales/en.json b/src/locales/en.json index c2c8d90..8f88625 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -129,6 +129,11 @@ "imageUpload": "Image Upload", "dragAndDrop": "Drag and drop\nimages to upload", "videoRatio": "Video Ratio", + "minImages": "Min. 5 images", + "youtubeShorts": "YouTube Shorts", + "youtubeVideo": "YouTube Video", + "back": "Go Back", + "loadMore": "Load more", "uploadFailed": "Image upload failed.", "uploading": "Uploading...", "nextStep": "Next Step" diff --git a/src/locales/ko.json b/src/locales/ko.json index 7532f45..14a02ac 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -129,6 +129,11 @@ "imageUpload": "이미지 업로드", "dragAndDrop": "이미지를 드래그하여\n업로드", "videoRatio": "영상 비율", + "minImages": "최소 5장", + "youtubeShorts": "유튜브 쇼츠", + "youtubeVideo": "유튜브 일반", + "back": "뒤로가기", + "loadMore": "더보기", "uploadFailed": "이미지 업로드에 실패했습니다.", "uploading": "업로드 중...", "nextStep": "다음 단계" diff --git a/src/pages/Dashboard/AssetManagementContent.tsx b/src/pages/Dashboard/AssetManagementContent.tsx index 6bb6ad3..6ce9388 100755 --- a/src/pages/Dashboard/AssetManagementContent.tsx +++ b/src/pages/Dashboard/AssetManagementContent.tsx @@ -6,6 +6,7 @@ import { uploadImages } from '../../utils/api'; interface AssetManagementContentProps { onNext: (imageTaskId: string) => void; + onBack?: () => void; imageList: ImageItem[]; onRemoveImage: (index: number) => void; onAddImages: (files: File[]) => void; @@ -13,20 +14,22 @@ interface AssetManagementContentProps { type VideoRatio = 'vertical' | 'horizontal'; +const IMAGES_PER_PAGE = 12; + const AssetManagementContent: React.FC = ({ onNext, + onBack, imageList, onRemoveImage, onAddImages, }) => { const { t } = useTranslation(); const fileInputRef = useRef(null); - const imageListRef = useRef(null); const [isUploading, setIsUploading] = useState(false); const [uploadError, setUploadError] = useState(null); const [videoRatio, setVideoRatio] = useState('vertical'); + const [displayCount, setDisplayCount] = useState(IMAGES_PER_PAGE); - // Load video ratio from localStorage on mount useEffect(() => { const savedRatio = localStorage.getItem('castad_video_ratio') as VideoRatio; if (savedRatio === 'vertical' || savedRatio === 'horizontal') { @@ -34,7 +37,6 @@ const AssetManagementContent: React.FC = ({ } }, []); - // Save video ratio to localStorage when it changes const handleVideoRatioChange = (ratio: VideoRatio) => { setVideoRatio(ratio); localStorage.setItem('castad_video_ratio', ratio); @@ -51,7 +53,6 @@ const AssetManagementContent: React.FC = ({ setUploadError(null); try { - // URL 이미지와 파일 이미지 분리 const urlImages: ImageUrlItem[] = imageList .filter((item: ImageItem): item is ImageItem & { type: 'url' } => item.type === 'url') .map((item) => ({ url: item.url })); @@ -60,7 +61,6 @@ const AssetManagementContent: React.FC = ({ .filter((item: ImageItem): item is ImageItem & { type: 'file' } => item.type === 'file') .map((item) => item.file); - // 이미지가 하나라도 있으면 업로드 if (urlImages.length > 0 || fileImages.length > 0) { const response = await uploadImages(urlImages, fileImages); onNext(response.task_id); @@ -73,11 +73,6 @@ const AssetManagementContent: React.FC = ({ } }; - const handleImageListWheel = (e: React.WheelEvent) => { - // 이 영역 안에서는 항상 스크롤 이벤트 전파 차단 - e.stopPropagation(); - }; - const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); @@ -86,14 +81,10 @@ const AssetManagementContent: React.FC = ({ const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); - - const files = Array.from(e.dataTransfer.files).filter(file => + const files = Array.from(e.dataTransfer.files).filter((file: File) => file.type.startsWith('image/') ); - - if (files.length > 0) { - onAddImages(files); - } + if (files.length > 0) onAddImages(files); }; const handleFileSelect = () => { @@ -108,104 +99,132 @@ const AssetManagementContent: React.FC = ({ } }; + const visibleImages = imageList.slice(0, displayCount); + const hasMore = imageList.length > displayCount; + return ( -
- {/* Title */} -

{t('assetManagement.title')}

+
+ {/* Fixed Header - 뒤로가기 버튼 */} +
+ {onBack && ( + + )} +
- {/* Main Content Container */} -
- {/* Left Column - Selected Images */} -
-

{t('assetManagement.selectedImages')}

-
- {imageList.length > 0 ? ( -
- {imageList.map((item, i) => ( -
- {`${t('assetManagement.imageAlt')} - {item.type === 'file' && ( -
{t('assetManagement.uploadBadge')}
- )} - -
- ))} + {/* Single Scrollable Content Area */} +
+

{t('assetManagement.title')}

+ +
+ {/* Left Column - Selected Images */} +
+
+
+

{t('assetManagement.selectedImages')}

+ {t('assetManagement.minImages')}
- ) : null} -
-
- - {/* Right Column - Upload and Video Ratio */} -
- {/* Image Upload Section */} -
-

{t('assetManagement.imageUpload')}

-
-

- {t('assetManagement.dragAndDrop').split('\n').map((line, i) => ( - {i > 0 &&
}{line}
- ))} -

+
- + +
+ {visibleImages.length > 0 && ( +
+ {visibleImages.map((item, i) => ( +
+ {`${t('assetManagement.imageAlt')} + {item.type === 'file' && ( +
{t('assetManagement.uploadBadge')}
+ )} + +
+ ))} +
+ )} +
+ + {hasMore && ( + + )}
- {/* Video Ratio Section */} -
-

{t('assetManagement.videoRatio')}

-
- - +

+ {t('assetManagement.dragAndDrop').split('\n').map((line, i) => ( + {i > 0 &&
}{line}
+ ))} +

+
+
+ + {/* Video Ratio Section */} +
+

{t('assetManagement.videoRatio')}

+
+ + +
- {/* Bottom Button */} -
+ {/* Fixed Footer - 다음 단계 버튼 */} +
{uploadError && (

{uploadError}

)} @@ -217,6 +236,15 @@ const AssetManagementContent: React.FC = ({ {isUploading ? t('assetManagement.uploading') : t('assetManagement.nextStep')}
+ +
); }; diff --git a/src/pages/Dashboard/GenerationFlow.tsx b/src/pages/Dashboard/GenerationFlow.tsx index f386151..4bb72a4 100755 --- a/src/pages/Dashboard/GenerationFlow.tsx +++ b/src/pages/Dashboard/GenerationFlow.tsx @@ -363,6 +363,7 @@ const GenerationFlow: React.FC = ({ case 1: return ( goToWizardStep(0)} onNext={(taskId: string) => { // Clear video generation state to start fresh localStorage.removeItem(VIDEO_GENERATION_KEY); diff --git a/src/pages/Dashboard/SoundStudioContent.tsx b/src/pages/Dashboard/SoundStudioContent.tsx index 1ba0f00..c62e367 100755 --- a/src/pages/Dashboard/SoundStudioContent.tsx +++ b/src/pages/Dashboard/SoundStudioContent.tsx @@ -386,7 +386,7 @@ const SoundStudioContent: React.FC = ({ const isGenerating = status === 'generating_lyric' || status === 'generating_song' || status === 'polling'; return ( -
+
{audioUrl && (