diff --git a/index.css b/index.css index 6916a1d..31a5adb 100644 --- a/index.css +++ b/index.css @@ -5691,6 +5691,37 @@ } } +/* Loading Progress Bar */ +.loading-progress-wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + margin-top: 24px; + width: 240px; +} + +.loading-progress-bar { + width: 100%; + height: 4px; + background: rgba(255, 255, 255, 0.1); + border-radius: 2px; + overflow: hidden; +} + +.loading-progress-fill { + height: 100%; + background: #a6ffea; + border-radius: 2px; + transition: width 0.1s linear; +} + +.loading-progress-text { + font-size: 13px; + color: rgba(255, 255, 255, 0.5); + font-variant-numeric: tabular-nums; +} + /* Loading Spinner Wrapper */ .loading-spinner-wrapper { width: 120px; @@ -7972,7 +8003,7 @@ /* Generate Sound Button */ .btn-generate-sound { - height: 48px; + height: 40px; min-width: 120px; padding: 0.625rem 1.25rem; background-color: #94FBE0; @@ -7989,6 +8020,7 @@ gap: 0.5rem; line-height: 1.19; letter-spacing: -0.006em; + margin-top: 10px; } .btn-generate-sound:hover:not(.disabled) { @@ -8000,6 +8032,12 @@ cursor: not-allowed; } +.regenerate-hint { + font-size: 14px; + color: #CEE5E6; + text-align: center; +} + /* Video Generate Button */ .btn-video-generate { padding: 0.625rem 2.5rem; @@ -8007,6 +8045,7 @@ border: none; border-radius: var(--radius-full); color: #FFFFFF; + height: 40px; font-size: var(--text-sm); font-weight: 600; cursor: pointer; @@ -8086,6 +8125,7 @@ align-items: center; justify-content: center; gap: 0.5rem; + margin-top: 10px; } /* Responsive Styles for Sound Studio */ @@ -10621,13 +10661,29 @@ pointer-events: all; } +@keyframes tutorial-pulse { + 0%, 100% { + border-color: rgba(166, 255, 234, 0.9); + box-shadow: 0 0 0 2px rgba(166, 255, 234, 0.3), + 0 0 16px 4px rgba(166, 255, 234, 0.35), + 0 0 40px 8px rgba(166, 255, 234, 0.15); + } + 50% { + border-color: rgba(166, 255, 234, 0.5); + box-shadow: 0 0 0 5px rgba(166, 255, 234, 0.12), + 0 0 32px 10px rgba(166, 255, 234, 0.5), + 0 0 64px 20px rgba(166, 255, 234, 0.2); + } +} + .tutorial-spotlight-ring { position: fixed; - border: 2px solid rgba(166, 255, 234, 0.85); + border: 4px solid rgba(166, 255, 234, 0.85); border-radius: 12px; - box-shadow: 0 0 0 1px rgba(166, 255, 234, 0.2); + box-shadow: 0 0 0 1px rgba(166, 255, 234, 0.2), 0 0 12px 2px rgba(166, 255, 234, 0.15); pointer-events: none; z-index: 10001; + animation: tutorial-pulse 2s ease-in-out infinite; } .tutorial-tooltip { @@ -10914,3 +10970,52 @@ border-color: rgba(166, 255, 234, 0.5); color: #a6ffea; } + +/* 튜토리얼 토글 버튼 */ +.tutorial-toggle-fab { + position: fixed; + top: 16px; + right: 20px; + z-index: 900; + display: flex; + align-items: center; + gap: 8px; + padding: 7px 14px; + background: rgba(0, 34, 36, 0.85); + border: 1px solid rgba(166, 255, 234, 0.25); + border-radius: 20px; + color: rgba(255, 255, 255, 0.5); + cursor: pointer; + font-size: 13px; + backdrop-filter: blur(6px); + transition: background 0.15s, color 0.15s, border-color 0.15s; +} + +.tutorial-toggle-fab:hover { + background: rgba(166, 255, 234, 0.12); + border-color: rgba(166, 255, 234, 0.5); + color: #a6ffea; +} + +.tutorial-toggle-fab.active { + color: #a6ffea; + border-color: rgba(166, 255, 234, 0.4); +} + +.tutorial-toggle-badge { + font-size: 10px; + font-weight: 700; + padding: 2px 6px; + border-radius: 4px; + transition: background 0.15s, color 0.15s; +} + +.tutorial-toggle-badge.on { + background: #a6ffea; + color: #002224; +} + +.tutorial-toggle-badge.off { + background: rgba(255, 255, 255, 0.12); + color: rgba(255, 255, 255, 0.4); +} diff --git a/index.html b/index.html index f52d80c..afa6666 100755 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - CASTAD + ADO2 diff --git a/src/components/Tutorial/tutorialSteps.ts b/src/components/Tutorial/tutorialSteps.ts index accfeaf..c993419 100644 --- a/src/components/Tutorial/tutorialSteps.ts +++ b/src/components/Tutorial/tutorialSteps.ts @@ -46,14 +46,6 @@ export const tutorialSteps: TutorialStepDef[] = [ { key: TUTORIAL_KEYS.LANDING, hints: [ - // { - // targetSelector: '.tutorial-center-anchor', - // titleKey: 'tutorial.landing.intro.title', - // descriptionKey: 'tutorial.landing.intro.desc', - // position: 'bottom', - // clickToAdvance: false, - // noSpotlight: true, - // }, { targetSelector: '.hero-dropdown-trigger', titleKey: 'tutorial.landing.dropdown.title', @@ -83,46 +75,6 @@ export const tutorialSteps: TutorialStepDef[] = [ }, ], }, - // { - // key: TUTORIAL_KEYS.ANALYSIS, - // hints: [ - // { - // targetSelector: '.bi2-identity-card', - // titleKey: 'tutorial.analysis.identity.title', - // descriptionKey: 'tutorial.analysis.identity.desc', - // position: 'top', - // clickToAdvance: false, - // }, - // { - // targetSelector: '.bi2-selling-card', - // titleKey: 'tutorial.analysis.selling.title', - // descriptionKey: 'tutorial.analysis.selling.desc', - // position: 'top', - // clickToAdvance: false, - // }, - // { - // targetSelector: '.bi2-persona-grid', - // titleKey: 'tutorial.analysis.persona.title', - // descriptionKey: 'tutorial.analysis.persona.desc', - // position: 'top', - // clickToAdvance: false, - // }, - // { - // targetSelector: '.bi2-keyword-tags', - // titleKey: 'tutorial.analysis.keywords.title', - // descriptionKey: 'tutorial.analysis.keywords.desc', - // position: 'top', - // clickToAdvance: false, - // }, - // { - // targetSelector: '.bi2-generate-btn', - // titleKey: 'tutorial.analysis.generate.title', - // descriptionKey: 'tutorial.analysis.generate.desc', - // position: 'right', - // clickToAdvance: true, - // }, - // ], - // }, { key: TUTORIAL_KEYS.ASSET, hints: [ @@ -167,7 +119,7 @@ export const tutorialSteps: TutorialStepDef[] = [ clickToAdvance: false, }, { - targetSelector: '.language-selector-wrapper', + targetSelector: '.language-grid', titleKey: 'tutorial.sound.language.title', descriptionKey: 'tutorial.sound.language.desc', position: 'top', @@ -393,7 +345,7 @@ export const tutorialSteps: TutorialStepDef[] = [ titleKey: 'tutorial.contentCalendar.grid.title', descriptionKey: 'tutorial.contentCalendar.grid.desc', position: 'top', - clickToAdvance: true, + clickToAdvance: false, }, { targetSelector: '.calendar-side-panel', diff --git a/src/components/Tutorial/useTutorial.ts b/src/components/Tutorial/useTutorial.ts index dce64a9..478da87 100644 --- a/src/components/Tutorial/useTutorial.ts +++ b/src/components/Tutorial/useTutorial.ts @@ -19,6 +19,7 @@ function getGroupProgress(key: string, currentIndex: number): { groupTotal: numb const SEEN_KEY = 'ado2_tutorial_seen'; const PROGRESS_KEY = 'ado2_tutorial_progress'; +const ENABLED_KEY = 'ado2_tutorial_enabled'; // 전역 단일 활성 튜토리얼 관리 — 새 튜토리얼 시작 시 이전 것을 skip 처리 let globalSkip: (() => void) | null = null; @@ -66,6 +67,7 @@ function clearProgress(key: string) { interface UseTutorialReturn { isActive: boolean; + isEnabled: boolean; isRestartPopupVisible: boolean; currentHintIndex: number; hints: TutorialHint[]; @@ -75,6 +77,7 @@ interface UseTutorialReturn { nextHint: () => void; prevHint: () => void; skipTutorial: () => void; + toggleTutorial: (currentKey: string | null) => void; showRestartPopup: (key: string) => void; confirmRestart: () => void; cancelRestart: () => void; @@ -83,6 +86,7 @@ interface UseTutorialReturn { export function useTutorial(): UseTutorialReturn { const [isActive, setIsActive] = useState(false); + const [isEnabled, setIsEnabled] = useState(() => localStorage.getItem(ENABLED_KEY) !== 'false'); const [isRestartPopupVisible, setIsRestartPopupVisible] = useState(false); const [pendingRestartKey, setPendingRestartKey] = useState(null); const [currentHintIndex, setCurrentHintIndex] = useState(0); @@ -91,6 +95,7 @@ export function useTutorial(): UseTutorialReturn { const onCompleteRef = React.useRef<(() => void) | undefined>(undefined); const startTutorial = useCallback((key: string, onComplete?: () => void, forceFromStart?: boolean) => { + if (localStorage.getItem(ENABLED_KEY) === 'false') return; const step = tutorialSteps.find(s => s.key === key); if (!step || step.hints.length === 0) return; // 다른 인스턴스에서 활성화된 튜토리얼이 있으면 skip 처리 @@ -139,6 +144,24 @@ export function useTutorial(): UseTutorialReturn { setCurrentHintIndex(0); }, [tutorialKey, currentHintIndex]); + const toggleTutorial = useCallback((currentKey: string | null) => { + if (isEnabled) { + // off: 튜토리얼 중단 + 비활성화 + setIsActive(false); + setIsEnabled(false); + localStorage.setItem(ENABLED_KEY, 'false'); + } else { + // on: seen/progress 초기화 + 현재 화면 튜토리얼 시작 + localStorage.removeItem(SEEN_KEY); + localStorage.removeItem(PROGRESS_KEY); + localStorage.setItem(ENABLED_KEY, 'true'); + setIsEnabled(true); + if (currentKey) { + startTutorial(currentKey, undefined, true); + } + } + }, [isEnabled, startTutorial]); + // 튜토리얼 다시 보기: 팝업 표시만 const showRestartPopup = useCallback((key: string) => { setPendingRestartKey(key); @@ -168,6 +191,7 @@ export function useTutorial(): UseTutorialReturn { return { isActive, + isEnabled, isRestartPopupVisible, currentHintIndex, hints, @@ -177,6 +201,7 @@ export function useTutorial(): UseTutorialReturn { nextHint, prevHint, skipTutorial, + toggleTutorial, showRestartPopup, confirmRestart, cancelRestart, diff --git a/src/locales/en.json b/src/locales/en.json index a7299bc..4b955ea 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -14,7 +14,10 @@ "defaultUser": "User", "loggingOut": "Logging out...", "logout": "Log Out", - "tutorialRestart": "Restart Tutorial" + "tutorialRestart": "Restart Tutorial", + "tutorial": "Tutorial", + "tutorialOn": "Enable Tutorial", + "tutorialOff": "Disable Tutorial" }, "tutorial": { "skip": "Skip", @@ -27,13 +30,6 @@ "field": { "title": "Enter Search Term", "desc": "For URL, paste a Naver Maps share URL.\nFor business name, type the name and select from the list." }, "button": { "title": "Start Brand Analysis", "desc": "Click the button to let AI start analyzing your brand." } }, - "analysis": { - "identity": { "title": "Brand Identity", "desc": "Check the core values and market positioning AI analyzed for your brand." }, - "selling": { "title": "Key Selling Points", "desc": "See your brand's strengths." }, - "persona": { "title": "Target Customer Types", "desc": "AI analyzed what kind of customers visit this brand." }, - "keywords": { "title": "Recommended Keywords", "desc": "Keywords your customers are likely to search for." }, - "generate": { "title": "Generate Content", "desc": "Create video content based on the analysis.\nClick to sign in with Kakao and continue." } - }, "asset": { "image": { "title": "Image List", "desc": "Photos from Naver Place. Tap 'Show more' to see the rest, or X to remove any." }, "upload": { "title": "Add Images", "desc": "You can freely add more images." }, @@ -234,6 +230,8 @@ "lyricsHint": "Select the lyrics area to edit", "lyricsPlaceholder": "Lyrics will be displayed when sound is generated.", "generateButton": "Generate Sound", + "regenerateButton": "Regenerate", + "regenerateHint": "Press the regenerate button to create new lyrics and music.", "generating": "Generating...", "generateVideo": "Generate Video", "videoGenerating": "Generating Video", diff --git a/src/locales/ko.json b/src/locales/ko.json index 82c0845..cfa2ab0 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -14,7 +14,10 @@ "defaultUser": "사용자", "loggingOut": "로그아웃 중...", "logout": "로그아웃", - "tutorialRestart": "튜토리얼 다시 보기" + "tutorialRestart": "튜토리얼 다시 보기", + "tutorial": "튜토리얼", + "tutorialOn": "튜토리얼 켜기", + "tutorialOff": "튜토리얼 끄기" }, "tutorial": { "skip": "건너뛰기", @@ -27,13 +30,6 @@ "field": { "title": "입력하기", "desc": "URL 방식이라면 네이버 지도 공유 URL,\n업체명 방식이라면 업체명을 입력하고 목록에서 선택하세요." }, "button": { "title": "브랜드 분석 시작", "desc": "버튼을 누르면 AI가 브랜드를 분석하기 시작해요." } }, - "analysis": { - "identity": { "title": "브랜드 정체성", "desc": "AI가 분석한 펜션의 핵심 가치와 시장 포지셔닝을 확인하세요." }, - "selling": { "title": "주요 셀링 포인트", "desc": "펜션의 강점을 확인할 수 있어요." }, - "persona": { "title": "주요 고객 유형", "desc": "어떤 고객이 이 펜션을 찾는지 분석했어요." }, - "keywords": { "title": "추천 타겟 키워드", "desc": "고객이 검색할 가능성이 높은 키워드들이에요." }, - "generate": { "title": "콘텐츠 생성", "desc": "분석 결과를 바탕으로 영상을 만들어 보세요.\n클릭하면 카카오 로그인으로 이동합니다." } - }, "asset": { "image": { "title": "이미지 목록", "desc": "네이버 Place에서 가져 온 사진이에요. \n더보기를 누르면 나머지 사진도 볼 수 있고 X를 눌러 삭제 할 수 있어요." }, "upload": { "title": "이미지 추가", "desc": "이미지를 자유롭게 추가 할 수 있어요." }, @@ -50,7 +46,7 @@ "video": { "title": "영상 생성", "desc": "버튼을 클릭해서 영상 생성을 시작하세요." } }, "completion": { - "contentInfo": { "title": "콘텐츠 정보", "desc": "콘텐츠의 제목,장르,규격,가사를 확인하세요." }, + "contentInfo": { "title": "콘텐츠 정보", "desc": "콘텐츠의 파일명, 장르, 규격, 가사를 확인하세요." }, "generating": { "title": "영상 제작 중", "desc": "AI가 영상을 만들고 있어요. \n잠시만 기다려 주세요." }, "completion": { "title": "영상 완성!", "desc": "영상 제작이 완료되었어요. \n영상을 확인해 볼까요?" }, "myInfo": { "title": "소셜 계정 연동", "desc": "영상을 유튜브에 업로드하려면 내 정보에서 소셜 계정을 연동해야 해요. \n클릭해서 이동하세요." } @@ -228,12 +224,14 @@ "genreLabel": "장르 선택", "genreAuto": "자동 선택", "genreBallad": "발라드", - "languageLabel": "언어", + "languageLabel": "언어 선택", "languageKorean": "한국어", "lyricsColumn": "가사", "lyricsHint": "가사 영역을 선택해서 수정 가능해요", "lyricsPlaceholder": "사운드 생성 시 가사 표시됩니다.", "generateButton": "사운드 생성", + "regenerateButton": "재생성 하기", + "regenerateHint": "재생성 버튼을 누르면 가사와 음악을 다시 만들 수 있어요.", "generating": "생성 중...", "generateVideo": "영상 생성하기", "videoGenerating": "영상 생성 중", diff --git a/src/pages/Analysis/LoadingSection.tsx b/src/pages/Analysis/LoadingSection.tsx index 1643c9f..d411eb0 100755 --- a/src/pages/Analysis/LoadingSection.tsx +++ b/src/pages/Analysis/LoadingSection.tsx @@ -1,9 +1,26 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; const LoadingSection: React.FC = () => { const { t } = useTranslation(); + const [progress, setProgress] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setProgress(prev => { + if (prev >= 100) { + clearInterval(interval); + return 100; + } + // 느리게 올라가다가 90% 이후 거의 멈춤 + const increment = prev < 50 ? 1 : prev < 85 ? 0.5 : 0.1; + return Math.min(prev + increment, 100); + }); + }, 100); + return () => clearInterval(interval); + }, []); + return (
@@ -23,6 +40,16 @@ const LoadingSection: React.FC = () => { className="loading-spinner-icon" />
+ +
+
+
+
+ {Math.floor(progress)}% +
diff --git a/src/pages/Dashboard/CompletionContent.tsx b/src/pages/Dashboard/CompletionContent.tsx index cad61af..a676b5c 100755 --- a/src/pages/Dashboard/CompletionContent.tsx +++ b/src/pages/Dashboard/CompletionContent.tsx @@ -369,6 +369,15 @@ const CompletionContent: React.FC = ({

{statusMessage}

+
+
+
+
+ {renderProgress}% +
) : videoStatus === 'error' ? (
@@ -397,7 +406,7 @@ const CompletionContent: React.FC = ({ {/* 오른쪽: 콘텐츠 정보 */}
- {t('completion.contentInfo', { defaultValue: '콘텐츠 정보' })} + {t('completion.contentInfo', { defaultValue: '파일명' })}
diff --git a/src/pages/Dashboard/GenerationFlow.tsx b/src/pages/Dashboard/GenerationFlow.tsx index 1905856..05859ed 100755 --- a/src/pages/Dashboard/GenerationFlow.tsx +++ b/src/pages/Dashboard/GenerationFlow.tsx @@ -519,23 +519,22 @@ const GenerationFlow: React.FC = ({ return null; }; - const handleTutorialRestart = () => { - tutorial.showRestartPopup(getCurrentTutorialKey()!); - }; - const tutorialUI = ( <> {getCurrentTutorialKey() && ( )} {tutorial.isActive && ( diff --git a/src/pages/Dashboard/SoundStudioContent.tsx b/src/pages/Dashboard/SoundStudioContent.tsx index f79d126..3fb6ac4 100755 --- a/src/pages/Dashboard/SoundStudioContent.tsx +++ b/src/pages/Dashboard/SoundStudioContent.tsx @@ -60,11 +60,8 @@ const SoundStudioContent: React.FC = ({ const [statusMessage, setStatusMessage] = useState(''); const [retryCount, setRetryCount] = useState(0); const [songTaskId, setSongTaskId] = useState(null); - const [isLanguageDropdownOpen, setIsLanguageDropdownOpen] = useState(false); - const progressBarRef = useRef(null); const audioRef = useRef(null); - const languageDropdownRef = useRef(null); // 완료 데이터를 localStorage에 저장 const saveSongCompletionData = () => { @@ -78,22 +75,6 @@ const SoundStudioContent: React.FC = ({ }; // Close language dropdown when clicking outside - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (languageDropdownRef.current && !languageDropdownRef.current.contains(event.target as Node)) { - setIsLanguageDropdownOpen(false); - } - }; - - if (isLanguageDropdownOpen) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [isLanguageDropdownOpen]); - // Auto-navigate to next page when video generation is complete useEffect(() => { if (videoGenerationStatus === 'complete' && songTaskId) { @@ -494,39 +475,31 @@ const SoundStudioContent: React.FC = ({ {/* Language Selection */}
-
- - {isLanguageDropdownOpen && ( -
- {Object.keys(LANGUAGE_MAP).map((lang) => ( - - ))} -
- )} +
+
+ {['한국어', 'English', '中文'].map((lang) => ( + + ))} +
+
+ {['日本語', 'ไทย', 'Tiếng Việt'].map((lang) => ( + + ))} +
@@ -539,6 +512,16 @@ const SoundStudioContent: React.FC = ({ {statusMessage}
+ ) : status === 'complete' ? ( + <> + +

{t('soundStudio.regenerateHint')}

+ ) : (