From f8bebdac525634a811ce1da52871c5eb85a8d79c Mon Sep 17 00:00:00 2001 From: hbyang Date: Thu, 8 Jan 2026 17:27:04 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AA=A8=EB=93=A0=20ui=20/=20ux=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EC=9E=91=EC=97=85=20.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.css | 374 +++++++++--------- src/App.tsx | 19 + src/pages/Analysis/AnalysisResultSection.tsx | 122 ++++-- .../Dashboard/AssetManagementContent.tsx | 43 +- src/pages/Dashboard/CompletionContent.tsx | 19 +- src/pages/Dashboard/GenerationFlow.tsx | 173 +++----- src/pages/Dashboard/SoundStudioContent.tsx | 38 +- src/pages/Landing/DisplaySection.tsx | 22 +- src/pages/Landing/HeroSection.tsx | 140 ++++++- 9 files changed, 505 insertions(+), 445 deletions(-) diff --git a/index.css b/index.css index 8c0eada..6255a74 100644 --- a/index.css +++ b/index.css @@ -130,6 +130,16 @@ } } +/* Wizard Page Container - 단계별 페이지 전환용 */ +.wizard-page-container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + background-color: #0d1416; + overflow-y: auto; +} + /* Main Content Area */ .main-content { flex: 1; @@ -251,23 +261,21 @@ /* Section Title (Mint color uppercase) */ .section-title { - color: var(--color-mint); - font-size: var(--text-sm); - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 1rem; + color: #94FBE0; + font-size: 16px; + font-weight: 600; + letter-spacing: -0.006em; + margin-bottom: 0; flex-shrink: 0; } /* Section Title Purple */ .section-title-purple { - color: var(--color-purple); - font-size: var(--text-sm); - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 1.25rem; + color: #AE72F9; + font-size: 16px; + font-weight: 600; + letter-spacing: -0.006em; + margin-bottom: 0; display: block; } @@ -493,19 +501,14 @@ /* Back Button Container */ .back-button-container { + width: 100%; + max-width: 1040px; display: flex; justify-content: flex-start; margin-bottom: 1.5rem; - margin-left: 2.5rem; flex-shrink: 0; } -@media (min-width: 768px) { - .back-button-container { - margin-left: 0; - } -} - /* Bottom Button Container */ .bottom-button-container { display: flex; @@ -776,7 +779,7 @@ .mobile-menu-btn { position: fixed; top: 1rem; - left: 1rem; + right: 1rem; z-index: 40; padding: 0.625rem; background-color: var(--color-bg-card); @@ -1797,7 +1800,8 @@ color: var(--color-text-white); display: flex; flex-direction: column; - padding: var(--spacing-page); + align-items: center; + padding: 1.5rem 1rem; background-color: var(--color-bg-dark); overflow-y: auto; overflow-x: hidden; @@ -1806,18 +1810,27 @@ @media (min-width: 768px) { .analysis-container { - padding: var(--spacing-page-md); + padding: 2rem 2rem; } } @media (min-width: 1024px) { .analysis-container { - padding: 2.5rem; + padding: 2.5rem 5%; + } +} + +@media (min-width: 1440px) { + .analysis-container { + /* 피그마: 1440px 화면에서 양쪽 200px 패딩 = 약 13.9% */ + padding: 2.5rem calc((100% - 1040px) / 2); } } /* Analysis Header */ .analysis-header { + width: 100%; + max-width: 1040px; display: flex; flex-direction: column; align-items: center; @@ -1839,85 +1852,140 @@ /* Analysis Grid */ .analysis-grid { - max-width: 72rem; - margin-left: auto; - margin-right: auto; width: 100%; + max-width: 1040px; display: flex; flex-direction: column; - gap: 1.5rem; + gap: 16px; flex-shrink: 0; + box-sizing: border-box; } -@media (min-width: 1024px) { +@media (min-width: 768px) { .analysis-grid { display: grid; - grid-template-columns: repeat(2, 1fr); + grid-template-columns: 1fr 1fr; + align-items: stretch; + min-height: 453px; } } /* Brand Identity Card */ .brand-identity-card { - background-color: var(--color-bg-card); - border-radius: var(--radius-3xl); - padding: var(--spacing-page); + background-color: #01393B; + border-radius: 40px; + padding: 24px; display: flex; flex-direction: column; - border: 1px solid var(--color-border-white-5); - box-shadow: var(--shadow-xl); + gap: 20px; + border: none; + box-shadow: none; + width: 100%; + box-sizing: border-box; } @media (min-width: 768px) { .brand-identity-card { - padding: var(--spacing-page-md); + padding: 32px; } } .brand-header { display: flex; align-items: center; - gap: 0.75rem; - margin-bottom: 1rem; + gap: 8px; + margin-bottom: 0; } .brand-name { - font-size: var(--text-3xl); - font-weight: 700; - margin-bottom: 0.5rem; + font-size: 28px; + font-weight: 600; + color: #E5F1F2; + letter-spacing: -0.006em; + margin-bottom: 0; } @media (min-width: 768px) { .brand-name { - font-size: var(--text-4xl); + font-size: 32px; } } .brand-location { - color: var(--color-text-gray-400); - font-size: var(--text-base); - margin-bottom: 1.5rem; + color: #6AB0B3; + font-size: 14px; + letter-spacing: -0.006em; + margin-bottom: 0; +} + +.brand-subtitle { + color: #6AB0B3; + font-size: 14px; + font-weight: 400; + letter-spacing: -0.006em; +} + +.brand-info { + display: flex; + flex-direction: column; + gap: 4px; } /* Report Section */ +.report-section { + flex: 1; + display: flex; + flex-direction: column; + gap: 8px; + min-height: 0; +} + .report-toggle { - font-size: var(--text-sm); - color: var(--color-text-gray-400); + font-size: 14px; + color: #6AB0B3; background: none; border: none; cursor: pointer; - transition: color var(--transition-normal); + transition: color 0.2s ease; + padding: 0; + text-align: left; } .report-toggle:hover { - color: var(--color-purple); + color: #AE72F9; } .report-content { - color: var(--color-text-gray-300); - font-size: var(--text-base); - line-height: 1.625; + color: #E5F1F2; + font-size: 17px; + line-height: 1.5; + letter-spacing: -0.006em; overflow-y: auto; - max-height: 300px; + flex: 1; + min-height: 0; +} + +.report-sections { + display: flex; + flex-direction: column; + gap: 16px; +} + +.report-content::-webkit-scrollbar { + width: 6px; +} + +.report-content::-webkit-scrollbar-track { + background: transparent; +} + +.report-content::-webkit-scrollbar-thumb { + background: #379599; + border-radius: 3px; +} + +.report-content::-webkit-scrollbar-thumb:hover { + background: #4AABAF; } .report-section-title { @@ -1929,18 +1997,17 @@ /* Image Preview */ .image-preview-section { - margin-top: 1.5rem; - padding-top: 1.5rem; - border-top: 1px solid var(--color-border-white-10); + margin-top: auto; + padding-top: 1rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); } .image-preview-title { - color: var(--color-text-gray-400); - font-size: var(--text-sm); - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 0.75rem; + color: #6AB0B3; + font-size: 14px; + font-weight: 600; + letter-spacing: -0.006em; + margin-bottom: 12px; display: block; } @@ -1979,21 +2046,28 @@ .analysis-cards-column { display: flex; flex-direction: column; - gap: 1.5rem; + gap: 16px; + width: 100%; } -/* Feature Card (Selling Points & Keywords) */ -.feature-card { - background-color: var(--color-bg-card); - border-radius: var(--radius-3xl); - padding: var(--spacing-page); - border: 1px solid var(--color-border-white-5); - box-shadow: var(--shadow-xl); +/* Feature Card for Analysis Page (Selling Points & Keywords) */ +.analysis-cards-column .feature-card { + background-color: #01393B; + border-radius: 40px; + padding: 24px; + border: none; + box-shadow: none; + flex: 1; + display: flex; + flex-direction: column; + gap: 20px; + width: 100%; + box-sizing: border-box; } @media (min-width: 768px) { - .feature-card { - padding: var(--spacing-page-md); + .analysis-cards-column .feature-card { + padding: 32px; } } @@ -2001,21 +2075,24 @@ .tags-wrapper { display: flex; flex-wrap: wrap; - gap: 0.75rem; + gap: 8px; } /* Feature Tag */ .feature-tag { - padding: 0.625rem 1.25rem; - background-color: var(--color-bg-card-inner); - border-radius: var(--radius-full); - font-size: var(--text-base); - color: #e5e7eb; - border: 1px solid var(--color-border-white-10); + padding: 8px 16px; + background-color: #046266; + border-radius: 999px; + font-size: 17px; + font-weight: 400; + color: #FFFFFF; + border: none; } /* Analysis Bottom Button */ .analysis-bottom { + width: 100%; + max-width: 1040px; margin-top: 2rem; padding-bottom: 1rem; display: flex; @@ -2077,122 +2154,14 @@ z-index: 0; } -.hero-orb { +/* JavaScript controlled random orbs */ +.hero-orb-random { position: absolute; border-radius: 50%; filter: blur(80px); - opacity: 0.6; -} - -/* Purple orb - top left */ -.hero-orb-purple-1 { - width: 400px; - height: 400px; - background: #AE72F9; - top: -100px; - left: 0; - animation: float-1 20s ease-in-out infinite; -} - -/* Mint orb - top right */ -.hero-orb-mint-1 { - width: 450px; - height: 450px; - background: #94FBE0; - top: -50px; - right: -100px; - animation: float-2 25s ease-in-out infinite; -} - -/* Purple orb - bottom left */ -.hero-orb-purple-2 { - width: 500px; - height: 500px; - background: #AE72F9; - bottom: -100px; - left: -100px; - animation: float-3 22s ease-in-out infinite; -} - -/* Mint orb - bottom right */ -.hero-orb-mint-2 { - width: 400px; - height: 400px; - background: #94FBE0; - bottom: -50px; - right: 5%; - animation: float-4 18s ease-in-out infinite; -} - -@keyframes float-1 { - 0% { - transform: translate(0, 0); - } - 33% { - transform: translate(200px, 150px); - } - 66% { - transform: translate(100px, 250px); - } - 100% { - transform: translate(0, 0); - } -} - -@keyframes float-2 { - 0% { - transform: translate(0, 0); - } - 33% { - transform: translate(-180px, 200px); - } - 66% { - transform: translate(-250px, 100px); - } - 100% { - transform: translate(0, 0); - } -} - -@keyframes float-3 { - 0% { - transform: translate(0, 0); - } - 33% { - transform: translate(220px, -180px); - } - 66% { - transform: translate(150px, -280px); - } - 100% { - transform: translate(0, 0); - } -} - -@keyframes float-4 { - 0% { - transform: translate(0, 0); - } - 33% { - transform: translate(-200px, -220px); - } - 66% { - transform: translate(-280px, -120px); - } - 100% { - transform: translate(0, 0); - } -} - -.hero-bg-blur { - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 200px; - background: linear-gradient(180deg, rgba(0, 34, 36, 0) 0%, rgba(0, 34, 36, 1) 100%); + opacity: 0.65; pointer-events: none; - z-index: 2; + will-change: left, top, transform; } .hero-content { @@ -2606,7 +2575,7 @@ @media (min-width: 1024px) { .feature-card { - max-width: 360px; + max-width: 500px; min-height: 440px; padding: 24px 40px; gap: 24px; @@ -2997,6 +2966,14 @@ object-fit: cover; } +.display-video { + width: 100%; + height: 100%; + border: none; + border-radius: 2.5rem; + pointer-events: none; +} + .display-frame-hidden-mobile { display: none; } @@ -4328,6 +4305,8 @@ font-weight: 600; cursor: pointer; transition: all var(--transition-normal); + width: fit-content; + flex-shrink: 0; } .btn-back-new:hover { @@ -4508,6 +4487,14 @@ cursor: not-allowed; } +.sound-type-btn.permanently-disabled { + opacity: 0.35; + cursor: not-allowed; + background-color: rgba(255, 255, 255, 0.03); + border-color: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.4); +} + /* Genre Grid */ .genre-grid { display: flex; @@ -4523,7 +4510,7 @@ /* Genre Button */ .genre-btn { - padding: 0.5rem 1rem; + padding: 1rem; background-color: #002224; border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 8px; @@ -5136,6 +5123,8 @@ .asset-column-left { flex: 1; min-width: 0; + min-height: 0; + overflow: hidden; } .asset-column-right { @@ -5164,6 +5153,7 @@ flex: 1; min-height: 0; overflow-y: auto; + overscroll-behavior: contain; background-color: #002224; border-radius: 8px; padding: 0.5rem; diff --git a/src/App.tsx b/src/App.tsx index c06ae1d..66b4863 100755 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,12 +31,30 @@ const App: React.FC = () => { savedAnalysisData ? JSON.parse(savedAnalysisData) : null ); const [error, setError] = useState(null); + const [scrollProgress, setScrollProgress] = useState(0); // viewMode 변경 시 localStorage에 저장 useEffect(() => { localStorage.setItem(VIEW_MODE_KEY, viewMode); }, [viewMode]); + // 스크롤 이벤트 핸들러 - 첫 번째 섹션에서 두 번째 섹션으로 넘어갈 때 0~1 값 계산 + useEffect(() => { + const container = containerRef.current; + if (!container || viewMode !== 'landing') return; + + const handleScroll = () => { + const scrollTop = container.scrollTop; + const sectionHeight = container.clientHeight; + // 첫 번째 섹션 스크롤 진행률 (0 ~ 1) + const progress = Math.min(1, Math.max(0, scrollTop / sectionHeight)); + setScrollProgress(progress); + }; + + container.addEventListener('scroll', handleScroll); + return () => container.removeEventListener('scroll', handleScroll); + }, [viewMode]); + const scrollToSection = (index: number) => { if (containerRef.current) { const h = containerRef.current.clientHeight; @@ -120,6 +138,7 @@ const App: React.FC = () => { onAnalyze={handleStartAnalysis} onNext={() => scrollToSection(1)} error={error} + scrollProgress={scrollProgress} />
diff --git a/src/pages/Analysis/AnalysisResultSection.tsx b/src/pages/Analysis/AnalysisResultSection.tsx index f3bee1a..23362f6 100755 --- a/src/pages/Analysis/AnalysisResultSection.tsx +++ b/src/pages/Analysis/AnalysisResultSection.tsx @@ -8,12 +8,62 @@ interface AnalysisResultSectionProps { data: CrawlingResponse; } +// 텍스트를 포맷팅 ([] 기준 줄바꿈, 해시태그 스타일링) +const formatReportText = (text: string): React.ReactNode[] => { + if (!text) return []; + + // [제목] 패턴을 기준으로 분리 + const parts = text.split(/(\[[^\]]+\])/g).filter(Boolean); + + return parts.map((part, idx) => { + // [제목] 패턴인 경우 + if (part.match(/^\[[^\]]+\]$/)) { + const title = part.slice(1, -1); // [] 제거 + return ( + + {idx > 0 &&
} +
+ [{title}] +
+
+ ); + } + + // 해시태그 처리 + const hashtagParts = part.split(/(#[^\s#]+)/g); + return ( + + {hashtagParts.map((segment, segIdx) => { + if (segment.startsWith('#')) { + return ( + + {segment} + + ); + } + return segment; + })} + + ); + }); +}; + // 마크다운 report를 섹션별로 파싱 const parseReport = (report: string) => { + if (!report || report.trim() === '') { + return []; + } + const sections: { title: string; content: string }[] = []; const lines = report.split('\n'); let currentTitle = ''; let currentContent: string[] = []; + let hasMarkdownHeaders = report.includes('## '); + + // 마크다운 헤더가 없는 경우 전체 텍스트를 하나의 섹션으로 반환 + if (!hasMarkdownHeaders) { + return [{ title: '분석 결과', content: report.trim() }]; + } lines.forEach((line) => { if (line.startsWith('## ')) { @@ -31,11 +81,11 @@ const parseReport = (report: string) => { sections.push({ title: currentTitle, content: currentContent.join('\n').trim() }); } - return sections.filter(s => s.title && s.content && !s.title.includes('JSON')); + return sections.filter(s => s.content && !s.title.includes('JSON')); }; const AnalysisResultSection: React.FC = ({ onBack, onGenerate, data }) => { - const { processed_info, marketing_analysis, image_list } = data; + const { processed_info, marketing_analysis } = data; const tags = marketing_analysis.tags || []; const facilities = marketing_analysis.facilities || []; const [showFullReport, setShowFullReport] = useState(false); @@ -71,57 +121,53 @@ const AnalysisResultSection: React.FC = ({ onBack, o {/* Brand Identity */}
- 브랜드 정체성 - AI 마케팅 분석 요약 + 브랜드 정체성 + AI 마케팅 분석 요약
-

{processed_info.customer_name}

-

{processed_info.region} · {processed_info.detail_region_info}

+
+

{processed_info.customer_name}

+

{processed_info.region} · {processed_info.detail_region_info}

+
{/* Marketing Analysis Summary */} -
-
- -
+
+
- {showFullReport ? ( -
+ {reportSections.length === 0 ? ( +
+ {marketing_analysis.report + ? formatReportText(marketing_analysis.report) + : '분석 결과가 없습니다.'} +
+ ) : showFullReport ? ( +
{reportSections.map((section, idx) => (
-

{section.title}

-

{section.content}

+ {section.title &&

{section.title}

} +
+ {formatReportText(section.content)} +
))}
) : ( -

- {reportSections[0]?.content.slice(0, 150)}... -

+
+ {formatReportText( + reportSections[0]?.content.length > 150 + ? `${reportSections[0].content.slice(0, 150)}...` + : reportSections[0]?.content || '' + )} +
)}
- {/* Image Preview */} - {image_list.length > 0 && ( -
- 수집된 이미지 ({image_list.length}장) -
- {image_list.slice(0, 8).map((img, idx) => ( -
- {`이미지 -
- ))} -
- {image_list.length > 8 && ( -

+{image_list.length - 8}장 더 있음

- )} -
- )}
{/* Right Cards */} diff --git a/src/pages/Dashboard/AssetManagementContent.tsx b/src/pages/Dashboard/AssetManagementContent.tsx index 186ed68..05c4229 100755 --- a/src/pages/Dashboard/AssetManagementContent.tsx +++ b/src/pages/Dashboard/AssetManagementContent.tsx @@ -4,7 +4,6 @@ import { ImageItem, ImageUrlItem } from '../../types/api'; import { uploadImages } from '../../utils/api'; interface AssetManagementContentProps { - onBack: () => void; onNext: (imageTaskId: string) => void; imageList: ImageItem[]; onRemoveImage: (index: number) => void; @@ -14,7 +13,6 @@ interface AssetManagementContentProps { type VideoRatio = 'vertical' | 'horizontal'; const AssetManagementContent: React.FC = ({ - onBack, onNext, imageList, onRemoveImage, @@ -74,20 +72,8 @@ const AssetManagementContent: React.FC = ({ }; const handleImageListWheel = (e: React.WheelEvent) => { - const el = imageListRef.current; - if (!el) return; - - const { scrollTop, scrollHeight, clientHeight } = el; - const hasScrollableContent = scrollHeight > clientHeight; - - if (!hasScrollableContent) return; - - const atTop = scrollTop <= 0; - const atBottom = scrollTop + clientHeight >= scrollHeight - 1; - - if ((e.deltaY < 0 && !atTop) || (e.deltaY > 0 && !atBottom)) { - e.stopPropagation(); - } + // 이 영역 안에서는 항상 스크롤 이벤트 전파 차단 + e.stopPropagation(); }; const handleDragOver = (e: React.DragEvent) => { @@ -122,30 +108,6 @@ const AssetManagementContent: React.FC = ({ return (
- {/* Header with Back Button and Progress */} -
- - -
-
- 단계 -
- 1 - / - 2 -
-
-
-
-
-
-
- {/* Title */}

브랜드 에셋

@@ -158,7 +120,6 @@ const AssetManagementContent: React.FC = ({ ref={imageListRef} onWheel={handleImageListWheel} className="asset-image-list" - style={{ overscrollBehavior: 'contain' }} > {imageList.length > 0 ? (
diff --git a/src/pages/Dashboard/CompletionContent.tsx b/src/pages/Dashboard/CompletionContent.tsx index 368bb8c..660037e 100755 --- a/src/pages/Dashboard/CompletionContent.tsx +++ b/src/pages/Dashboard/CompletionContent.tsx @@ -282,7 +282,7 @@ const CompletionContent: React.FC = ({ return (
- {/* Header with Back Button and Progress */} + {/* Header with Back Button */}
- -
-
- 단계 -
- 2 - / - 2 -
-
-
-
-
-
{/* Title */} diff --git a/src/pages/Dashboard/GenerationFlow.tsx b/src/pages/Dashboard/GenerationFlow.tsx index ee336f4..20d994f 100755 --- a/src/pages/Dashboard/GenerationFlow.tsx +++ b/src/pages/Dashboard/GenerationFlow.tsx @@ -1,5 +1,5 @@ -import React, { useRef, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import Sidebar from '../../components/Sidebar'; import AssetManagementContent from './AssetManagementContent'; import SoundStudioContent from './SoundStudioContent'; @@ -40,8 +40,6 @@ interface GenerationFlowProps { } const GenerationFlow: React.FC = ({ onHome, initialActiveItem = '대시보드', initialImageList = [], businessInfo }) => { - const scrollContainerRef = useRef(null); - // localStorage에서 저장된 상태 복원 const savedActiveItem = localStorage.getItem(ACTIVE_ITEM_KEY); const savedWizardStep = localStorage.getItem(WIZARD_STEP_KEY); @@ -49,7 +47,8 @@ const GenerationFlow: React.FC = ({ onHome, initialActiveIt const savedImageTaskId = localStorage.getItem(IMAGE_TASK_ID_KEY); const [activeItem, setActiveItem] = useState(savedActiveItem || initialActiveItem); - const [maxWizardIndex, setMaxWizardIndex] = useState(savedWizardStep ? parseInt(savedWizardStep, 10) : 0); + // 현재 위저드 단계 (0: Asset, 1: Sound Studio, 2: Completion) + const [wizardStep, setWizardStep] = useState(savedWizardStep ? parseInt(savedWizardStep, 10) : 0); const [songTaskId, setSongTaskId] = useState(savedSongTaskId); const [imageTaskId, setImageTaskId] = useState(savedImageTaskId); const [videoGenerationStatus, setVideoGenerationStatus] = useState<'idle' | 'generating' | 'complete' | 'error'>('idle'); @@ -84,22 +83,17 @@ const GenerationFlow: React.FC = ({ onHome, initialActiveIt // 홈 버튼(로고) 클릭 시 모든 상태 초기화 후 홈으로 이동 const handleHome = () => { clearAllProjectStorage(); - setMaxWizardIndex(0); + setWizardStep(0); setSongTaskId(null); setImageTaskId(null); setImageList(initialImageList.map(url => ({ type: 'url', url }))); onHome(); }; - const scrollToWizardSection = (index: number) => { - if (scrollContainerRef.current) { - const sections = scrollContainerRef.current.querySelectorAll('.flow-section'); - if (sections[index]) { - setMaxWizardIndex(prev => Math.max(prev, index)); - localStorage.setItem(WIZARD_STEP_KEY, index.toString()); - sections[index].scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - } + // 위저드 단계 이동 + const goToWizardStep = (step: number) => { + setWizardStep(step); + localStorage.setItem(WIZARD_STEP_KEY, step.toString()); }; // activeItem 변경 시 localStorage에 저장 @@ -107,60 +101,55 @@ const GenerationFlow: React.FC = ({ onHome, initialActiveIt localStorage.setItem(ACTIVE_ITEM_KEY, activeItem); }, [activeItem]); - // 새로고침 시 저장된 위저드 단계로 스크롤 - useEffect(() => { - if (activeItem === '새 프로젝트 만들기' && savedWizardStep) { - const stepIndex = parseInt(savedWizardStep, 10); - // 약간의 딜레이 후 스크롤 (DOM이 준비될 때까지) - setTimeout(() => { - if (scrollContainerRef.current) { - const sections = scrollContainerRef.current.querySelectorAll('.flow-section'); - if (sections[stepIndex]) { - sections[stepIndex].scrollIntoView({ behavior: 'auto', block: 'start' }); - } - } - }, 100); + // 새 프로젝트 만들기 - 단계별 컨텐츠 렌더링 + const renderWizardContent = () => { + switch (wizardStep) { + case 0: + return ( + { + // Clear video generation state to start fresh + localStorage.removeItem(VIDEO_GENERATION_KEY); + setVideoGenerationStatus('idle'); + setVideoGenerationProgress(0); + + setImageTaskId(taskId); + localStorage.setItem(IMAGE_TASK_ID_KEY, taskId); + goToWizardStep(1); + }} + imageList={imageList} + onRemoveImage={handleRemoveImage} + onAddImages={handleAddImages} + /> + ); + case 1: + return ( + goToWizardStep(0)} + onNext={(taskId: string) => { + setSongTaskId(taskId); + localStorage.setItem(SONG_TASK_ID_KEY, taskId); + goToWizardStep(2); + }} + businessInfo={businessInfo} + imageTaskId={imageTaskId} + videoGenerationStatus={videoGenerationStatus} + videoGenerationProgress={videoGenerationProgress} + /> + ); + case 2: + return ( + goToWizardStep(1)} + songTaskId={songTaskId} + onVideoStatusChange={setVideoGenerationStatus} + onVideoProgressChange={setVideoGenerationProgress} + /> + ); + default: + return null; } - }, []); - - useEffect(() => { - const container = scrollContainerRef.current; - if (!container || activeItem !== '새 프로젝트 만들기') return; - - const handleWheel = (e: WheelEvent) => { - // 스크롤 가능한 자식 요소 내부에서 발생한 이벤트인지 확인 - const target = e.target as HTMLElement; - - // custom-scrollbar 클래스를 가진 요소 또는 그 자식에서 발생한 이벤트인지 확인 - const scrollableParent = target.closest('.custom-scrollbar') as HTMLElement; - if (scrollableParent) { - const { scrollTop, scrollHeight, clientHeight } = scrollableParent; - const hasScrollableContent = scrollHeight > clientHeight; - const atTop = scrollTop <= 0; - const atBottom = scrollTop + clientHeight >= scrollHeight - 1; - - // 스크롤 가능한 콘텐츠가 있는 경우 - if (hasScrollableContent) { - // 위로 스크롤하고 맨 위가 아니거나, 아래로 스크롤하고 맨 아래가 아니면 허용 - if ((e.deltaY < 0 && !atTop) || (e.deltaY > 0 && !atBottom)) { - return; // 이미지 리스트 스크롤 허용 (preventDefault 하지 않음) - } - } - } - - const h = container.clientHeight; - const currentIdx = Math.round(container.scrollTop / h); - - if (e.deltaY > 0) { // Down - if (currentIdx >= maxWizardIndex) { - e.preventDefault(); - } - } - }; - - container.addEventListener('wheel', handleWheel, { passive: false }); - return () => container.removeEventListener('wheel', handleWheel); - }, [maxWizardIndex, activeItem]); + }; const renderContent = () => { switch (activeItem) { @@ -170,54 +159,8 @@ const GenerationFlow: React.FC = ({ onHome, initialActiveIt return ; case '새 프로젝트 만들기': return ( -
- {/* Step 0: Asset Management (AnalysisResultSection removed) */} -
- setActiveItem('대시보드')} - onNext={(taskId: string) => { - // Clear video generation state to start fresh - localStorage.removeItem(VIDEO_GENERATION_KEY); - setVideoGenerationStatus('idle'); - setVideoGenerationProgress(0); - - setImageTaskId(taskId); - localStorage.setItem(IMAGE_TASK_ID_KEY, taskId); - scrollToWizardSection(1); - }} - imageList={imageList} - onRemoveImage={handleRemoveImage} - onAddImages={handleAddImages} - /> -
- {/* Step 1: Sound Studio */} -
- scrollToWizardSection(0)} - onNext={(taskId: string) => { - setSongTaskId(taskId); - localStorage.setItem(SONG_TASK_ID_KEY, taskId); - scrollToWizardSection(2); - }} - businessInfo={businessInfo} - imageTaskId={imageTaskId} - videoGenerationStatus={videoGenerationStatus} - videoGenerationProgress={videoGenerationProgress} - /> -
- {/* Step 2: Completion */} -
- scrollToWizardSection(1)} - songTaskId={songTaskId} - onVideoStatusChange={setVideoGenerationStatus} - onVideoProgressChange={setVideoGenerationProgress} - /> -
+
+ {renderWizardContent()}
); default: diff --git a/src/pages/Dashboard/SoundStudioContent.tsx b/src/pages/Dashboard/SoundStudioContent.tsx index 8945219..6b96a75 100755 --- a/src/pages/Dashboard/SoundStudioContent.tsx +++ b/src/pages/Dashboard/SoundStudioContent.tsx @@ -430,7 +430,7 @@ const SoundStudioContent: React.FC = ({