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}
+
) : 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')}
+ >
) : (