프로그래스바 추가 및 UI 수정
parent
6675b5301f
commit
29cee13da7
111
index.css
111
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 */
|
||||||
.loading-spinner-wrapper {
|
.loading-spinner-wrapper {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
|
|
@ -7972,7 +8003,7 @@
|
||||||
|
|
||||||
/* Generate Sound Button */
|
/* Generate Sound Button */
|
||||||
.btn-generate-sound {
|
.btn-generate-sound {
|
||||||
height: 48px;
|
height: 40px;
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
padding: 0.625rem 1.25rem;
|
padding: 0.625rem 1.25rem;
|
||||||
background-color: #94FBE0;
|
background-color: #94FBE0;
|
||||||
|
|
@ -7989,6 +8020,7 @@
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
line-height: 1.19;
|
line-height: 1.19;
|
||||||
letter-spacing: -0.006em;
|
letter-spacing: -0.006em;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-generate-sound:hover:not(.disabled) {
|
.btn-generate-sound:hover:not(.disabled) {
|
||||||
|
|
@ -8000,6 +8032,12 @@
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.regenerate-hint {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #CEE5E6;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* Video Generate Button */
|
/* Video Generate Button */
|
||||||
.btn-video-generate {
|
.btn-video-generate {
|
||||||
padding: 0.625rem 2.5rem;
|
padding: 0.625rem 2.5rem;
|
||||||
|
|
@ -8007,6 +8045,7 @@
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-full);
|
border-radius: var(--radius-full);
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
|
height: 40px;
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -8086,6 +8125,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive Styles for Sound Studio */
|
/* Responsive Styles for Sound Studio */
|
||||||
|
|
@ -10621,13 +10661,29 @@
|
||||||
pointer-events: all;
|
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 {
|
.tutorial-spotlight-ring {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
border: 2px solid rgba(166, 255, 234, 0.85);
|
border: 4px solid rgba(166, 255, 234, 0.85);
|
||||||
border-radius: 12px;
|
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;
|
pointer-events: none;
|
||||||
z-index: 10001;
|
z-index: 10001;
|
||||||
|
animation: tutorial-pulse 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tutorial-tooltip {
|
.tutorial-tooltip {
|
||||||
|
|
@ -10914,3 +10970,52 @@
|
||||||
border-color: rgba(166, 255, 234, 0.5);
|
border-color: rgba(166, 255, 234, 0.5);
|
||||||
color: #a6ffea;
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>CASTAD</title>
|
<title>ADO2</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon_32.svg" sizes="32x32">
|
<link rel="icon" type="image/svg+xml" href="/favicon_32.svg" sizes="32x32">
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon_48.svg" sizes="48x48">
|
<link rel="icon" type="image/svg+xml" href="/favicon_48.svg" sizes="48x48">
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
|
|
||||||
|
|
@ -46,14 +46,6 @@ export const tutorialSteps: TutorialStepDef[] = [
|
||||||
{
|
{
|
||||||
key: TUTORIAL_KEYS.LANDING,
|
key: TUTORIAL_KEYS.LANDING,
|
||||||
hints: [
|
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',
|
targetSelector: '.hero-dropdown-trigger',
|
||||||
titleKey: 'tutorial.landing.dropdown.title',
|
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,
|
key: TUTORIAL_KEYS.ASSET,
|
||||||
hints: [
|
hints: [
|
||||||
|
|
@ -167,7 +119,7 @@ export const tutorialSteps: TutorialStepDef[] = [
|
||||||
clickToAdvance: false,
|
clickToAdvance: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
targetSelector: '.language-selector-wrapper',
|
targetSelector: '.language-grid',
|
||||||
titleKey: 'tutorial.sound.language.title',
|
titleKey: 'tutorial.sound.language.title',
|
||||||
descriptionKey: 'tutorial.sound.language.desc',
|
descriptionKey: 'tutorial.sound.language.desc',
|
||||||
position: 'top',
|
position: 'top',
|
||||||
|
|
@ -393,7 +345,7 @@ export const tutorialSteps: TutorialStepDef[] = [
|
||||||
titleKey: 'tutorial.contentCalendar.grid.title',
|
titleKey: 'tutorial.contentCalendar.grid.title',
|
||||||
descriptionKey: 'tutorial.contentCalendar.grid.desc',
|
descriptionKey: 'tutorial.contentCalendar.grid.desc',
|
||||||
position: 'top',
|
position: 'top',
|
||||||
clickToAdvance: true,
|
clickToAdvance: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
targetSelector: '.calendar-side-panel',
|
targetSelector: '.calendar-side-panel',
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ function getGroupProgress(key: string, currentIndex: number): { groupTotal: numb
|
||||||
|
|
||||||
const SEEN_KEY = 'ado2_tutorial_seen';
|
const SEEN_KEY = 'ado2_tutorial_seen';
|
||||||
const PROGRESS_KEY = 'ado2_tutorial_progress';
|
const PROGRESS_KEY = 'ado2_tutorial_progress';
|
||||||
|
const ENABLED_KEY = 'ado2_tutorial_enabled';
|
||||||
|
|
||||||
// 전역 단일 활성 튜토리얼 관리 — 새 튜토리얼 시작 시 이전 것을 skip 처리
|
// 전역 단일 활성 튜토리얼 관리 — 새 튜토리얼 시작 시 이전 것을 skip 처리
|
||||||
let globalSkip: (() => void) | null = null;
|
let globalSkip: (() => void) | null = null;
|
||||||
|
|
@ -66,6 +67,7 @@ function clearProgress(key: string) {
|
||||||
|
|
||||||
interface UseTutorialReturn {
|
interface UseTutorialReturn {
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
isEnabled: boolean;
|
||||||
isRestartPopupVisible: boolean;
|
isRestartPopupVisible: boolean;
|
||||||
currentHintIndex: number;
|
currentHintIndex: number;
|
||||||
hints: TutorialHint[];
|
hints: TutorialHint[];
|
||||||
|
|
@ -75,6 +77,7 @@ interface UseTutorialReturn {
|
||||||
nextHint: () => void;
|
nextHint: () => void;
|
||||||
prevHint: () => void;
|
prevHint: () => void;
|
||||||
skipTutorial: () => void;
|
skipTutorial: () => void;
|
||||||
|
toggleTutorial: (currentKey: string | null) => void;
|
||||||
showRestartPopup: (key: string) => void;
|
showRestartPopup: (key: string) => void;
|
||||||
confirmRestart: () => void;
|
confirmRestart: () => void;
|
||||||
cancelRestart: () => void;
|
cancelRestart: () => void;
|
||||||
|
|
@ -83,6 +86,7 @@ interface UseTutorialReturn {
|
||||||
|
|
||||||
export function useTutorial(): UseTutorialReturn {
|
export function useTutorial(): UseTutorialReturn {
|
||||||
const [isActive, setIsActive] = useState(false);
|
const [isActive, setIsActive] = useState(false);
|
||||||
|
const [isEnabled, setIsEnabled] = useState(() => localStorage.getItem(ENABLED_KEY) !== 'false');
|
||||||
const [isRestartPopupVisible, setIsRestartPopupVisible] = useState(false);
|
const [isRestartPopupVisible, setIsRestartPopupVisible] = useState(false);
|
||||||
const [pendingRestartKey, setPendingRestartKey] = useState<string | null>(null);
|
const [pendingRestartKey, setPendingRestartKey] = useState<string | null>(null);
|
||||||
const [currentHintIndex, setCurrentHintIndex] = useState(0);
|
const [currentHintIndex, setCurrentHintIndex] = useState(0);
|
||||||
|
|
@ -91,6 +95,7 @@ export function useTutorial(): UseTutorialReturn {
|
||||||
const onCompleteRef = React.useRef<(() => void) | undefined>(undefined);
|
const onCompleteRef = React.useRef<(() => void) | undefined>(undefined);
|
||||||
|
|
||||||
const startTutorial = useCallback((key: string, onComplete?: () => void, forceFromStart?: boolean) => {
|
const startTutorial = useCallback((key: string, onComplete?: () => void, forceFromStart?: boolean) => {
|
||||||
|
if (localStorage.getItem(ENABLED_KEY) === 'false') return;
|
||||||
const step = tutorialSteps.find(s => s.key === key);
|
const step = tutorialSteps.find(s => s.key === key);
|
||||||
if (!step || step.hints.length === 0) return;
|
if (!step || step.hints.length === 0) return;
|
||||||
// 다른 인스턴스에서 활성화된 튜토리얼이 있으면 skip 처리
|
// 다른 인스턴스에서 활성화된 튜토리얼이 있으면 skip 처리
|
||||||
|
|
@ -139,6 +144,24 @@ export function useTutorial(): UseTutorialReturn {
|
||||||
setCurrentHintIndex(0);
|
setCurrentHintIndex(0);
|
||||||
}, [tutorialKey, currentHintIndex]);
|
}, [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) => {
|
const showRestartPopup = useCallback((key: string) => {
|
||||||
setPendingRestartKey(key);
|
setPendingRestartKey(key);
|
||||||
|
|
@ -168,6 +191,7 @@ export function useTutorial(): UseTutorialReturn {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isActive,
|
isActive,
|
||||||
|
isEnabled,
|
||||||
isRestartPopupVisible,
|
isRestartPopupVisible,
|
||||||
currentHintIndex,
|
currentHintIndex,
|
||||||
hints,
|
hints,
|
||||||
|
|
@ -177,6 +201,7 @@ export function useTutorial(): UseTutorialReturn {
|
||||||
nextHint,
|
nextHint,
|
||||||
prevHint,
|
prevHint,
|
||||||
skipTutorial,
|
skipTutorial,
|
||||||
|
toggleTutorial,
|
||||||
showRestartPopup,
|
showRestartPopup,
|
||||||
confirmRestart,
|
confirmRestart,
|
||||||
cancelRestart,
|
cancelRestart,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,10 @@
|
||||||
"defaultUser": "User",
|
"defaultUser": "User",
|
||||||
"loggingOut": "Logging out...",
|
"loggingOut": "Logging out...",
|
||||||
"logout": "Log Out",
|
"logout": "Log Out",
|
||||||
"tutorialRestart": "Restart Tutorial"
|
"tutorialRestart": "Restart Tutorial",
|
||||||
|
"tutorial": "Tutorial",
|
||||||
|
"tutorialOn": "Enable Tutorial",
|
||||||
|
"tutorialOff": "Disable Tutorial"
|
||||||
},
|
},
|
||||||
"tutorial": {
|
"tutorial": {
|
||||||
"skip": "Skip",
|
"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." },
|
"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." }
|
"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": {
|
"asset": {
|
||||||
"image": { "title": "Image List", "desc": "Photos from Naver Place. Tap 'Show more' to see the rest, or X to remove any." },
|
"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." },
|
"upload": { "title": "Add Images", "desc": "You can freely add more images." },
|
||||||
|
|
@ -234,6 +230,8 @@
|
||||||
"lyricsHint": "Select the lyrics area to edit",
|
"lyricsHint": "Select the lyrics area to edit",
|
||||||
"lyricsPlaceholder": "Lyrics will be displayed when sound is generated.",
|
"lyricsPlaceholder": "Lyrics will be displayed when sound is generated.",
|
||||||
"generateButton": "Generate Sound",
|
"generateButton": "Generate Sound",
|
||||||
|
"regenerateButton": "Regenerate",
|
||||||
|
"regenerateHint": "Press the regenerate button to create new lyrics and music.",
|
||||||
"generating": "Generating...",
|
"generating": "Generating...",
|
||||||
"generateVideo": "Generate Video",
|
"generateVideo": "Generate Video",
|
||||||
"videoGenerating": "Generating Video",
|
"videoGenerating": "Generating Video",
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,10 @@
|
||||||
"defaultUser": "사용자",
|
"defaultUser": "사용자",
|
||||||
"loggingOut": "로그아웃 중...",
|
"loggingOut": "로그아웃 중...",
|
||||||
"logout": "로그아웃",
|
"logout": "로그아웃",
|
||||||
"tutorialRestart": "튜토리얼 다시 보기"
|
"tutorialRestart": "튜토리얼 다시 보기",
|
||||||
|
"tutorial": "튜토리얼",
|
||||||
|
"tutorialOn": "튜토리얼 켜기",
|
||||||
|
"tutorialOff": "튜토리얼 끄기"
|
||||||
},
|
},
|
||||||
"tutorial": {
|
"tutorial": {
|
||||||
"skip": "건너뛰기",
|
"skip": "건너뛰기",
|
||||||
|
|
@ -27,13 +30,6 @@
|
||||||
"field": { "title": "입력하기", "desc": "URL 방식이라면 네이버 지도 공유 URL,\n업체명 방식이라면 업체명을 입력하고 목록에서 선택하세요." },
|
"field": { "title": "입력하기", "desc": "URL 방식이라면 네이버 지도 공유 URL,\n업체명 방식이라면 업체명을 입력하고 목록에서 선택하세요." },
|
||||||
"button": { "title": "브랜드 분석 시작", "desc": "버튼을 누르면 AI가 브랜드를 분석하기 시작해요." }
|
"button": { "title": "브랜드 분석 시작", "desc": "버튼을 누르면 AI가 브랜드를 분석하기 시작해요." }
|
||||||
},
|
},
|
||||||
"analysis": {
|
|
||||||
"identity": { "title": "브랜드 정체성", "desc": "AI가 분석한 펜션의 핵심 가치와 시장 포지셔닝을 확인하세요." },
|
|
||||||
"selling": { "title": "주요 셀링 포인트", "desc": "펜션의 강점을 확인할 수 있어요." },
|
|
||||||
"persona": { "title": "주요 고객 유형", "desc": "어떤 고객이 이 펜션을 찾는지 분석했어요." },
|
|
||||||
"keywords": { "title": "추천 타겟 키워드", "desc": "고객이 검색할 가능성이 높은 키워드들이에요." },
|
|
||||||
"generate": { "title": "콘텐츠 생성", "desc": "분석 결과를 바탕으로 영상을 만들어 보세요.\n클릭하면 카카오 로그인으로 이동합니다." }
|
|
||||||
},
|
|
||||||
"asset": {
|
"asset": {
|
||||||
"image": { "title": "이미지 목록", "desc": "네이버 Place에서 가져 온 사진이에요. \n더보기를 누르면 나머지 사진도 볼 수 있고 X를 눌러 삭제 할 수 있어요." },
|
"image": { "title": "이미지 목록", "desc": "네이버 Place에서 가져 온 사진이에요. \n더보기를 누르면 나머지 사진도 볼 수 있고 X를 눌러 삭제 할 수 있어요." },
|
||||||
"upload": { "title": "이미지 추가", "desc": "이미지를 자유롭게 추가 할 수 있어요." },
|
"upload": { "title": "이미지 추가", "desc": "이미지를 자유롭게 추가 할 수 있어요." },
|
||||||
|
|
@ -50,7 +46,7 @@
|
||||||
"video": { "title": "영상 생성", "desc": "버튼을 클릭해서 영상 생성을 시작하세요." }
|
"video": { "title": "영상 생성", "desc": "버튼을 클릭해서 영상 생성을 시작하세요." }
|
||||||
},
|
},
|
||||||
"completion": {
|
"completion": {
|
||||||
"contentInfo": { "title": "콘텐츠 정보", "desc": "콘텐츠의 제목,장르,규격,가사를 확인하세요." },
|
"contentInfo": { "title": "콘텐츠 정보", "desc": "콘텐츠의 파일명, 장르, 규격, 가사를 확인하세요." },
|
||||||
"generating": { "title": "영상 제작 중", "desc": "AI가 영상을 만들고 있어요. \n잠시만 기다려 주세요." },
|
"generating": { "title": "영상 제작 중", "desc": "AI가 영상을 만들고 있어요. \n잠시만 기다려 주세요." },
|
||||||
"completion": { "title": "영상 완성!", "desc": "영상 제작이 완료되었어요. \n영상을 확인해 볼까요?" },
|
"completion": { "title": "영상 완성!", "desc": "영상 제작이 완료되었어요. \n영상을 확인해 볼까요?" },
|
||||||
"myInfo": { "title": "소셜 계정 연동", "desc": "영상을 유튜브에 업로드하려면 내 정보에서 소셜 계정을 연동해야 해요. \n클릭해서 이동하세요." }
|
"myInfo": { "title": "소셜 계정 연동", "desc": "영상을 유튜브에 업로드하려면 내 정보에서 소셜 계정을 연동해야 해요. \n클릭해서 이동하세요." }
|
||||||
|
|
@ -228,12 +224,14 @@
|
||||||
"genreLabel": "장르 선택",
|
"genreLabel": "장르 선택",
|
||||||
"genreAuto": "자동 선택",
|
"genreAuto": "자동 선택",
|
||||||
"genreBallad": "발라드",
|
"genreBallad": "발라드",
|
||||||
"languageLabel": "언어",
|
"languageLabel": "언어 선택",
|
||||||
"languageKorean": "한국어",
|
"languageKorean": "한국어",
|
||||||
"lyricsColumn": "가사",
|
"lyricsColumn": "가사",
|
||||||
"lyricsHint": "가사 영역을 선택해서 수정 가능해요",
|
"lyricsHint": "가사 영역을 선택해서 수정 가능해요",
|
||||||
"lyricsPlaceholder": "사운드 생성 시 가사 표시됩니다.",
|
"lyricsPlaceholder": "사운드 생성 시 가사 표시됩니다.",
|
||||||
"generateButton": "사운드 생성",
|
"generateButton": "사운드 생성",
|
||||||
|
"regenerateButton": "재생성 하기",
|
||||||
|
"regenerateHint": "재생성 버튼을 누르면 가사와 음악을 다시 만들 수 있어요.",
|
||||||
"generating": "생성 중...",
|
"generating": "생성 중...",
|
||||||
"generateVideo": "영상 생성하기",
|
"generateVideo": "영상 생성하기",
|
||||||
"videoGenerating": "영상 생성 중",
|
"videoGenerating": "영상 생성 중",
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,26 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const LoadingSection: React.FC = () => {
|
const LoadingSection: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
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 (
|
return (
|
||||||
<div className="loading-container">
|
<div className="loading-container">
|
||||||
<div className="loading-content">
|
<div className="loading-content">
|
||||||
|
|
@ -23,6 +40,16 @@ const LoadingSection: React.FC = () => {
|
||||||
className="loading-spinner-icon"
|
className="loading-spinner-icon"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="loading-progress-wrapper">
|
||||||
|
<div className="loading-progress-bar">
|
||||||
|
<div
|
||||||
|
className="loading-progress-fill"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="loading-progress-text">{Math.floor(progress)}%</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -369,6 +369,15 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="comp2-loading-text">{statusMessage}</p>
|
<p className="comp2-loading-text">{statusMessage}</p>
|
||||||
|
<div className="loading-progress-wrapper">
|
||||||
|
<div className="loading-progress-bar">
|
||||||
|
<div
|
||||||
|
className="loading-progress-fill"
|
||||||
|
style={{ width: `${renderProgress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="loading-progress-text">{renderProgress}%</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : videoStatus === 'error' ? (
|
) : videoStatus === 'error' ? (
|
||||||
<div className="comp2-video-error">
|
<div className="comp2-video-error">
|
||||||
|
|
@ -397,7 +406,7 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
||||||
{/* 오른쪽: 콘텐츠 정보 */}
|
{/* 오른쪽: 콘텐츠 정보 */}
|
||||||
<div className="comp2-info-section">
|
<div className="comp2-info-section">
|
||||||
<div className="comp2-info-header">
|
<div className="comp2-info-header">
|
||||||
<span className="comp2-info-label">{t('completion.contentInfo', { defaultValue: '콘텐츠 정보' })}</span>
|
<span className="comp2-info-label">{t('completion.contentInfo', { defaultValue: '파일명' })}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="comp2-info-content">
|
<div className="comp2-info-content">
|
||||||
<div className="comp2-file-info">
|
<div className="comp2-file-info">
|
||||||
|
|
|
||||||
|
|
@ -519,23 +519,22 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTutorialRestart = () => {
|
|
||||||
tutorial.showRestartPopup(getCurrentTutorialKey()!);
|
|
||||||
};
|
|
||||||
|
|
||||||
const tutorialUI = (
|
const tutorialUI = (
|
||||||
<>
|
<>
|
||||||
{getCurrentTutorialKey() && (
|
{getCurrentTutorialKey() && (
|
||||||
<button
|
<button
|
||||||
className="tutorial-restart-fab"
|
className={`tutorial-toggle-fab ${tutorial.isEnabled ? 'active' : ''}`}
|
||||||
onClick={handleTutorialRestart}
|
onClick={() => tutorial.toggleTutorial(getCurrentTutorialKey())}
|
||||||
title={t('sidebar.tutorialRestart')}
|
title={tutorial.isEnabled ? t('sidebar.tutorialOff') : t('sidebar.tutorialOn')}
|
||||||
>
|
>
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||||
<circle cx="12" cy="12" r="10"/>
|
<circle cx="12" cy="12" r="10"/>
|
||||||
<path d="M12 8v4l3 3"/>
|
<path d="M12 8v4l3 3"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>{t('sidebar.tutorialRestart')}</span>
|
<span>{t('sidebar.tutorial')}</span>
|
||||||
|
<span className={`tutorial-toggle-badge ${tutorial.isEnabled ? 'on' : 'off'}`}>
|
||||||
|
{tutorial.isEnabled ? 'ON' : 'OFF'}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{tutorial.isActive && (
|
{tutorial.isActive && (
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,8 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
const [statusMessage, setStatusMessage] = useState('');
|
const [statusMessage, setStatusMessage] = useState('');
|
||||||
const [retryCount, setRetryCount] = useState(0);
|
const [retryCount, setRetryCount] = useState(0);
|
||||||
const [songTaskId, setSongTaskId] = useState<string | null>(null);
|
const [songTaskId, setSongTaskId] = useState<string | null>(null);
|
||||||
const [isLanguageDropdownOpen, setIsLanguageDropdownOpen] = useState(false);
|
|
||||||
|
|
||||||
const progressBarRef = useRef<HTMLDivElement>(null);
|
const progressBarRef = useRef<HTMLDivElement>(null);
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
const languageDropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// 완료 데이터를 localStorage에 저장
|
// 완료 데이터를 localStorage에 저장
|
||||||
const saveSongCompletionData = () => {
|
const saveSongCompletionData = () => {
|
||||||
|
|
@ -78,22 +75,6 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
// Close language dropdown when clicking outside
|
// 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
|
// Auto-navigate to next page when video generation is complete
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (videoGenerationStatus === 'complete' && songTaskId) {
|
if (videoGenerationStatus === 'complete' && songTaskId) {
|
||||||
|
|
@ -494,39 +475,31 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
{/* Language Selection */}
|
{/* Language Selection */}
|
||||||
<div className="sound-studio-section">
|
<div className="sound-studio-section">
|
||||||
<label className="input-label">{t('soundStudio.languageLabel')}</label>
|
<label className="input-label">{t('soundStudio.languageLabel')}</label>
|
||||||
<div className="language-selector-wrapper" ref={languageDropdownRef}>
|
<div className="genre-grid language-grid">
|
||||||
<button
|
<div className="genre-row">
|
||||||
onClick={() => setIsLanguageDropdownOpen(!isLanguageDropdownOpen)}
|
{['한국어', 'English', '中文'].map((lang) => (
|
||||||
disabled={isGenerating}
|
<button
|
||||||
className="language-selector"
|
key={lang}
|
||||||
>
|
onClick={() => !isGenerating && setSelectedLang(lang)}
|
||||||
<div className="language-display">
|
disabled={isGenerating}
|
||||||
<span className="language-flag">{LANGUAGE_FLAGS[selectedLang]}</span>
|
className={`genre-btn ${selectedLang === lang ? 'active' : ''}`}
|
||||||
<span className="language-name">{selectedLang}</span>
|
>
|
||||||
</div>
|
<span>{LANGUAGE_FLAGS[lang]}</span> {lang}
|
||||||
<img
|
</button>
|
||||||
src="/assets/images/icon-dropdown.svg"
|
))}
|
||||||
alt=""
|
</div>
|
||||||
className={`language-dropdown-icon ${isLanguageDropdownOpen ? 'open' : ''}`}
|
<div className="genre-row">
|
||||||
/>
|
{['日本語', 'ไทย', 'Tiếng Việt'].map((lang) => (
|
||||||
</button>
|
<button
|
||||||
{isLanguageDropdownOpen && (
|
key={lang}
|
||||||
<div className="language-dropdown-menu">
|
onClick={() => !isGenerating && setSelectedLang(lang)}
|
||||||
{Object.keys(LANGUAGE_MAP).map((lang) => (
|
disabled={isGenerating}
|
||||||
<button
|
className={`genre-btn ${selectedLang === lang ? 'active' : ''}`}
|
||||||
key={lang}
|
>
|
||||||
onClick={() => {
|
<span>{LANGUAGE_FLAGS[lang]}</span> {lang}
|
||||||
setSelectedLang(lang);
|
</button>
|
||||||
setIsLanguageDropdownOpen(false);
|
))}
|
||||||
}}
|
</div>
|
||||||
className={`language-dropdown-item ${selectedLang === lang ? 'active' : ''}`}
|
|
||||||
>
|
|
||||||
<span className="language-flag">{LANGUAGE_FLAGS[lang]}</span>
|
|
||||||
<span className="language-name">{lang}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -539,6 +512,16 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
</svg>
|
</svg>
|
||||||
{statusMessage}
|
{statusMessage}
|
||||||
</div>
|
</div>
|
||||||
|
) : status === 'complete' ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={handleRegenerate}
|
||||||
|
className="btn-generate-sound"
|
||||||
|
>
|
||||||
|
{t('soundStudio.regenerateButton')}
|
||||||
|
</button>
|
||||||
|
<p className="regenerate-hint">{t('soundStudio.regenerateHint')}</p>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={handleGenerateMusic}
|
onClick={handleGenerateMusic}
|
||||||
|
|
@ -560,7 +543,7 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
<div className="lyrics-column">
|
<div className="lyrics-column">
|
||||||
<div className="lyrics-header">
|
<div className="lyrics-header">
|
||||||
<h3 className="column-title">{t('soundStudio.lyricsColumn')}</h3>
|
<h3 className="column-title">{t('soundStudio.lyricsColumn')}</h3>
|
||||||
<p className="lyrics-subtitle">{t('soundStudio.lyricsHint')}</p>
|
{/* <p className="lyrics-subtitle">{t('soundStudio.lyricsHint')}</p> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Audio Player */}
|
{/* Audio Player */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue