Compare commits
No commits in common. "8734d3388dcf122918662d986eab33614150d634" and "5407812889e6c83bd3ce41b482685da55485595c" have entirely different histories.
8734d3388d
...
5407812889
20
index.css
20
index.css
|
|
@ -7798,7 +7798,7 @@
|
|||
top: calc(100% + 0.5rem);
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: 220px;
|
||||
max-height: 240px;
|
||||
background-color: #002224;
|
||||
border: 1px solid #046266;
|
||||
border-radius: 8px;
|
||||
|
|
@ -10568,23 +10568,8 @@
|
|||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tutorial-overlay-blocker {
|
||||
position: fixed;
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.tutorial-spotlight-ring {
|
||||
position: fixed;
|
||||
border: 2px solid rgba(166, 255, 234, 0.85);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 0 1px rgba(166, 255, 234, 0.2);
|
||||
pointer-events: none;
|
||||
z-index: 10001;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tutorial-tooltip {
|
||||
|
|
@ -10611,7 +10596,6 @@
|
|||
color: rgba(255, 255, 255, 0.75);
|
||||
margin: 0 0 14px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.tutorial-tooltip-footer {
|
||||
|
|
|
|||
|
|
@ -140,13 +140,7 @@ const Sidebar: React.FC<SidebarProps> = ({ activeItem, onNavigate, onHome, userI
|
|||
{menuItems.map(item => (
|
||||
<SidebarItem
|
||||
key={item.id}
|
||||
id={
|
||||
item.id === '내 정보'
|
||||
? 'sidebar-my-info'
|
||||
: item.id === 'ADO2 콘텐츠'
|
||||
? 'sidebar-ado2-contents'
|
||||
: undefined
|
||||
}
|
||||
id={item.id === '내 정보' ? 'sidebar-my-info' : undefined}
|
||||
icon={item.icon}
|
||||
label={item.label}
|
||||
isCollapsed={isCollapsed}
|
||||
|
|
|
|||
|
|
@ -139,7 +139,6 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||
const [videoMeta, setVideoMeta] = useState<{ width: number; height: number; duration: number } | null>(null);
|
||||
const channelDropdownRef = useRef<HTMLDivElement>(null);
|
||||
const privacyDropdownRef = useRef<HTMLDivElement>(null);
|
||||
const hasBeenOpenedRef = useRef(false);
|
||||
|
||||
// Upload progress modal state
|
||||
const [showUploadProgress, setShowUploadProgress] = useState(false);
|
||||
|
|
@ -176,31 +175,15 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||
return () => { document.body.style.overflow = ''; };
|
||||
}, [isOpen]);
|
||||
|
||||
// 모달 오픈/닫힘 시 튜토리얼 트리거
|
||||
// 모달 최초 오픈 시 튜토리얼 트리거
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
hasBeenOpenedRef.current = true;
|
||||
if (!tutorial.hasSeen(TUTORIAL_KEYS.UPLOAD_MODAL)) {
|
||||
const timer = setTimeout(() => {
|
||||
tutorial.startTutorial(TUTORIAL_KEYS.UPLOAD_MODAL);
|
||||
}, 400);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
} else if (hasBeenOpenedRef.current && !tutorial.hasSeen(TUTORIAL_KEYS.FEEDBACK)) {
|
||||
hasBeenOpenedRef.current = false;
|
||||
tutorial.startTutorial(TUTORIAL_KEYS.FEEDBACK);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// SEO 생성 완료 시 UPLOAD_FORM 튜토리얼 트리거
|
||||
useEffect(() => {
|
||||
if (!isLoadingAutoDescription && isOpen && !tutorial.hasSeen(TUTORIAL_KEYS.UPLOAD_FORM)) {
|
||||
if (isOpen && !tutorial.hasSeen(TUTORIAL_KEYS.UPLOAD_MODAL)) {
|
||||
const timer = setTimeout(() => {
|
||||
tutorial.startTutorial(TUTORIAL_KEYS.UPLOAD_FORM);
|
||||
tutorial.startTutorial(TUTORIAL_KEYS.UPLOAD_MODAL);
|
||||
}, 400);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isLoadingAutoDescription]);
|
||||
}, [isOpen]);
|
||||
|
||||
// 소셜 계정 로드
|
||||
useEffect(() => {
|
||||
|
|
@ -430,20 +413,8 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||
);
|
||||
|
||||
if (!isOpen || !video) {
|
||||
return (
|
||||
<>
|
||||
{showUploadProgress && uploadProgressModalElement}
|
||||
{tutorial.isActive && (
|
||||
<TutorialOverlay
|
||||
hints={tutorial.hints}
|
||||
currentIndex={tutorial.currentHintIndex}
|
||||
onNext={tutorial.nextHint}
|
||||
onPrev={tutorial.prevHint}
|
||||
onSkip={tutorial.skipTutorial}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
// Still render upload progress modal even when main modal is closed
|
||||
return showUploadProgress ? uploadProgressModalElement : null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -31,28 +31,6 @@ function getTargetRect(selector: string): Rect | null {
|
|||
return { top: r.top, left: r.left, width: r.width, height: r.height };
|
||||
}
|
||||
|
||||
function getSpotlightRect(
|
||||
rect: Rect,
|
||||
padding: number,
|
||||
override?: { top?: number; right?: number; bottom?: number; left?: number }
|
||||
): Rect {
|
||||
const pTop = override?.top ?? padding;
|
||||
const pRight = override?.right ?? padding;
|
||||
const pBottom = override?.bottom ?? padding;
|
||||
const pLeft = override?.left ?? padding;
|
||||
const left = Math.max(0, Math.floor(rect.left - pLeft));
|
||||
const top = Math.max(0, Math.floor(rect.top - pTop));
|
||||
const right = Math.min(window.innerWidth, Math.ceil(rect.left + rect.width + pRight));
|
||||
const bottom = Math.min(window.innerHeight, Math.ceil(rect.top + rect.height + pBottom));
|
||||
|
||||
return {
|
||||
top,
|
||||
left,
|
||||
width: Math.max(0, right - left),
|
||||
height: Math.max(0, bottom - top),
|
||||
};
|
||||
}
|
||||
|
||||
function calcTooltipPos(rect: Rect, position: TutorialHint['position']): TooltipPos {
|
||||
const tooltipW = 300;
|
||||
const tooltipH = 140;
|
||||
|
|
@ -60,23 +38,29 @@ function calcTooltipPos(rect: Rect, position: TutorialHint['position']): Tooltip
|
|||
switch (position) {
|
||||
case 'bottom':
|
||||
return {
|
||||
top: Math.min(rect.top + rect.height + PADDING, window.innerHeight - tooltipH - 8),
|
||||
left: Math.min(Math.max(rect.left + rect.width / 2 - tooltipW / 2, 8), window.innerWidth - tooltipW - 8),
|
||||
top: rect.top + rect.height + PADDING,
|
||||
left: Math.min(
|
||||
Math.max(rect.left + rect.width / 2 - tooltipW / 2, 8),
|
||||
window.innerWidth - tooltipW - 8
|
||||
),
|
||||
};
|
||||
case 'top':
|
||||
return {
|
||||
top: Math.max(rect.top - tooltipH - PADDING, 8),
|
||||
left: Math.min(Math.max(rect.left + rect.width / 2 - tooltipW / 2, 8), window.innerWidth - tooltipW - 8),
|
||||
top: rect.top - tooltipH - PADDING,
|
||||
left: Math.min(
|
||||
Math.max(rect.left + rect.width / 2 - tooltipW / 2, 8),
|
||||
window.innerWidth - tooltipW - 8
|
||||
),
|
||||
};
|
||||
case 'right':
|
||||
return {
|
||||
top: Math.min(Math.max(rect.top + rect.height / 2 - tooltipH / 2, 8), window.innerHeight - tooltipH - 8),
|
||||
left: Math.min(rect.left + rect.width + PADDING, window.innerWidth - tooltipW - 8),
|
||||
top: Math.max(rect.top + rect.height / 2 - tooltipH / 2, 8),
|
||||
left: rect.left + rect.width + PADDING,
|
||||
};
|
||||
case 'left':
|
||||
return {
|
||||
top: Math.min(Math.max(rect.top + rect.height / 2 - tooltipH / 2, 8), window.innerHeight - tooltipH - 8),
|
||||
left: Math.max(rect.left - tooltipW - PADDING, 8),
|
||||
top: Math.max(rect.top + rect.height / 2 - tooltipH / 2, 8),
|
||||
left: rect.left - tooltipW - PADDING,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -102,62 +86,47 @@ const TutorialOverlay: React.FC<TutorialOverlayProps> = ({
|
|||
useEffect(() => {
|
||||
updateRect();
|
||||
window.addEventListener('resize', updateRect);
|
||||
window.addEventListener('scroll', updateRect, true);
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateRect);
|
||||
window.removeEventListener('scroll', updateRect, true);
|
||||
};
|
||||
return () => window.removeEventListener('resize', updateRect);
|
||||
}, [updateRect]);
|
||||
|
||||
// 대상 요소 z-index 끌어올리기 + 스크롤 + 클릭 시 onNext 연결
|
||||
useEffect(() => {
|
||||
if (!hint) return;
|
||||
let retryTimer: number | undefined;
|
||||
let rectTimer: number | undefined;
|
||||
let cleanupTarget: (() => void) | undefined;
|
||||
const el = document.querySelector<HTMLElement>(hint.targetSelector);
|
||||
if (!el) return;
|
||||
|
||||
const bindToTarget = (el: HTMLElement) => {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
rectTimer = window.setTimeout(updateRect, 200);
|
||||
// 스크롤해서 요소가 화면 중앙에 오도록
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
|
||||
const shouldClickAdvance = hint.clickToAdvance !== false;
|
||||
if (shouldClickAdvance) {
|
||||
el.style.cursor = 'pointer';
|
||||
el.addEventListener('click', onNext);
|
||||
}
|
||||
// scrollIntoView 완료 후 rect 재계산
|
||||
const timer = setTimeout(updateRect, 400);
|
||||
|
||||
cleanupTarget = () => {
|
||||
if (shouldClickAdvance) {
|
||||
el.style.cursor = '';
|
||||
el.removeEventListener('click', onNext);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const tryBind = (): boolean => {
|
||||
const el = document.querySelector(hint.targetSelector) as HTMLElement | null;
|
||||
if (!el) return false;
|
||||
bindToTarget(el);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!tryBind()) {
|
||||
retryTimer = window.setInterval(() => {
|
||||
if (tryBind() && retryTimer) {
|
||||
window.clearInterval(retryTimer);
|
||||
retryTimer = undefined;
|
||||
}
|
||||
}, 80);
|
||||
const prevZ = el.style.zIndex;
|
||||
const prevPos = el.style.position;
|
||||
const prevCursor = el.style.cursor;
|
||||
el.style.zIndex = '10001';
|
||||
el.style.position = el.style.position || 'relative';
|
||||
// clickToAdvance가 명시적으로 false가 아닌 경우에만 클릭 리스너 연결
|
||||
const shouldClickAdvance = hint.clickToAdvance !== false;
|
||||
if (shouldClickAdvance) {
|
||||
el.style.cursor = 'pointer';
|
||||
el.addEventListener('click', onNext);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (retryTimer) window.clearInterval(retryTimer);
|
||||
if (rectTimer) window.clearTimeout(rectTimer);
|
||||
cleanupTarget?.();
|
||||
clearTimeout(timer);
|
||||
el.style.zIndex = prevZ;
|
||||
el.style.position = prevPos;
|
||||
el.style.cursor = prevCursor;
|
||||
if (shouldClickAdvance) {
|
||||
el.removeEventListener('click', onNext);
|
||||
}
|
||||
};
|
||||
}, [hint, updateRect, onNext]);
|
||||
|
||||
if (!hint) return null;
|
||||
|
||||
// 대상 요소가 없으면 화면 중앙에 툴팁 표시 (오버레이는 보이지 않음)
|
||||
const tooltipPos: TooltipPos = targetRect
|
||||
? calcTooltipPos(targetRect, hint.position)
|
||||
: {
|
||||
|
|
@ -165,85 +134,57 @@ const TutorialOverlay: React.FC<TutorialOverlayProps> = ({
|
|||
left: window.innerWidth / 2 - 150,
|
||||
};
|
||||
|
||||
const spotlightPadding = hint.spotlightPadding ?? PADDING;
|
||||
const spotlightRect = targetRect ? getSpotlightRect(targetRect, spotlightPadding, hint.spotlightPaddingOverride) : null;
|
||||
// spotlight SVG 경로: 화면 전체 - 대상 요소 구멍
|
||||
// 대상 요소가 없으면 오버레이 없음
|
||||
const spotlight = targetRect
|
||||
? `M0,0 H${window.innerWidth} V${window.innerHeight} H0 Z
|
||||
M${targetRect.left - PADDING},${targetRect.top - PADDING}
|
||||
H${targetRect.left + targetRect.width + PADDING}
|
||||
V${targetRect.top + targetRect.height + PADDING}
|
||||
H${targetRect.left - PADDING} Z`
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="tutorial-overlay-root">
|
||||
{spotlightRect ? (
|
||||
<>
|
||||
<div
|
||||
className="tutorial-overlay-blocker"
|
||||
style={{ top: 0, left: 0, right: 0, height: spotlightRect.top }}
|
||||
{/* 어두운 배경 + 스포트라이트 — 배경 클릭 시 아무 동작 없음 */}
|
||||
{spotlight && (
|
||||
<svg className="tutorial-overlay-svg">
|
||||
<path
|
||||
d={spotlight}
|
||||
fillRule="evenodd"
|
||||
fill="rgba(0,0,0,0.72)"
|
||||
/>
|
||||
<div
|
||||
className="tutorial-overlay-blocker"
|
||||
style={{
|
||||
top: spotlightRect.top,
|
||||
left: 0,
|
||||
width: spotlightRect.left,
|
||||
height: spotlightRect.height,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="tutorial-overlay-blocker"
|
||||
style={{
|
||||
top: spotlightRect.top,
|
||||
left: spotlightRect.left + spotlightRect.width,
|
||||
right: 0,
|
||||
height: spotlightRect.height,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="tutorial-overlay-blocker"
|
||||
style={{
|
||||
top: spotlightRect.top + spotlightRect.height,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="tutorial-spotlight-ring"
|
||||
style={{
|
||||
top: spotlightRect.top,
|
||||
left: spotlightRect.left,
|
||||
width: spotlightRect.width,
|
||||
height: spotlightRect.height,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="tutorial-overlay-blocker" style={{ inset: 0 }} />
|
||||
</svg>
|
||||
)}
|
||||
|
||||
{/* 툴팁 — 항상 표시 */}
|
||||
<div
|
||||
className="tutorial-tooltip"
|
||||
style={{ top: tooltipPos.top, left: tooltipPos.left }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<p className="tutorial-tooltip-title">{t(hint.titleKey)}</p>
|
||||
<p className="tutorial-tooltip-desc">{t(hint.descriptionKey)}</p>
|
||||
className="tutorial-tooltip"
|
||||
style={{ top: tooltipPos.top, left: tooltipPos.left }}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<p className="tutorial-tooltip-title">{t(hint.titleKey)}</p>
|
||||
<p className="tutorial-tooltip-desc">{t(hint.descriptionKey)}</p>
|
||||
|
||||
<div className="tutorial-tooltip-footer">
|
||||
<span className="tutorial-tooltip-counter">
|
||||
{currentIndex + 1} / {hints.length}
|
||||
</span>
|
||||
<div className="tutorial-tooltip-actions">
|
||||
<button className="tutorial-btn-skip" onClick={onSkip}>
|
||||
{t('tutorial.skip')}
|
||||
</button>
|
||||
{currentIndex > 0 && (
|
||||
<button className="tutorial-btn-prev" onClick={onPrev}>
|
||||
{t('tutorial.prev')}
|
||||
<div className="tutorial-tooltip-footer">
|
||||
<span className="tutorial-tooltip-counter">
|
||||
{currentIndex + 1} / {hints.length}
|
||||
</span>
|
||||
<div className="tutorial-tooltip-actions">
|
||||
<button className="tutorial-btn-skip" onClick={onSkip}>
|
||||
{t('tutorial.skip')}
|
||||
</button>
|
||||
)}
|
||||
<button className="tutorial-btn-next" onClick={onNext}>
|
||||
{isLast ? t('tutorial.finish') : t('tutorial.next')}
|
||||
</button>
|
||||
{currentIndex > 0 && (
|
||||
<button className="tutorial-btn-prev" onClick={onPrev}>
|
||||
{t('tutorial.prev')}
|
||||
</button>
|
||||
)}
|
||||
<button className="tutorial-btn-next" onClick={onNext}>
|
||||
{isLast ? t('tutorial.finish') : t('tutorial.next')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -259,7 +200,7 @@ export const TutorialRestartPopup: React.FC<TutorialRestartPopupProps> = ({ onCo
|
|||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="tutorial-restart-backdrop">
|
||||
<div className="tutorial-restart-popup" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="tutorial-restart-popup" onClick={e => e.stopPropagation()}>
|
||||
<p className="tutorial-restart-title">{t('tutorial.restart.title')}</p>
|
||||
<p className="tutorial-restart-desc">{t('tutorial.restart.desc')}</p>
|
||||
<div className="tutorial-restart-actions">
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@ export interface TutorialHint {
|
|||
titleKey: string;
|
||||
descriptionKey: string;
|
||||
position: 'top' | 'bottom' | 'left' | 'right';
|
||||
clickToAdvance?: boolean;
|
||||
advanceSelector?: string;
|
||||
spotlightPadding?: number;
|
||||
spotlightPaddingOverride?: { top?: number; right?: number; bottom?: number; left?: number };
|
||||
clickToAdvance?: boolean; // true면 요소 클릭 시 튜토리얼 진행, false면 툴팁 버튼으로만 진행
|
||||
}
|
||||
|
||||
export interface TutorialStepDef {
|
||||
|
|
@ -19,16 +16,11 @@ export const TUTORIAL_KEYS = {
|
|||
ANALYSIS: 'analysis',
|
||||
ASSET: 'asset',
|
||||
SOUND: 'sound',
|
||||
SOUND_LYRICS: 'soundLyrics',
|
||||
SOUND_AUDIO: 'soundAudio',
|
||||
GENERATING:'generating',
|
||||
COMPLETION: 'completion',
|
||||
MY_INFO: 'myInfo',
|
||||
ADO2_CONTENTS: 'ado2Contents',
|
||||
UPLOAD_MODAL: 'uploadModal',
|
||||
UPLOAD_FORM: 'uploadForm',
|
||||
DASHBOARD: 'dashboard',
|
||||
CONTENT_CALENDAR: 'contentCalendar',
|
||||
FEEDBACK: 'feedback',
|
||||
} as const;
|
||||
|
||||
|
|
@ -40,24 +32,19 @@ export const tutorialSteps: TutorialStepDef[] = [
|
|||
targetSelector: '.hero-dropdown-container',
|
||||
titleKey: 'tutorial.landing.dropdown.title',
|
||||
descriptionKey: 'tutorial.landing.dropdown.desc',
|
||||
position: 'top',
|
||||
clickToAdvance: false,
|
||||
spotlightPaddingOverride: { top: 10, right: 15, bottom: 100, left: 0 },
|
||||
position: 'bottom',
|
||||
},
|
||||
{
|
||||
targetSelector: '.hero-input-wrapper',
|
||||
targetSelector: '.hero-input-container',
|
||||
titleKey: 'tutorial.landing.field.title',
|
||||
descriptionKey: 'tutorial.landing.field.desc',
|
||||
position: 'right',
|
||||
clickToAdvance: false,
|
||||
spotlightPaddingOverride: { top: 0, right: 0, bottom: 290, left: -105 },
|
||||
position: 'bottom',
|
||||
},
|
||||
{
|
||||
targetSelector: '.hero-button',
|
||||
titleKey: 'tutorial.landing.button.title',
|
||||
descriptionKey: 'tutorial.landing.button.desc',
|
||||
position: 'bottom',
|
||||
clickToAdvance: true,
|
||||
position: 'top',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -68,35 +55,35 @@ export const tutorialSteps: TutorialStepDef[] = [
|
|||
targetSelector: '.bi2-identity-card',
|
||||
titleKey: 'tutorial.analysis.identity.title',
|
||||
descriptionKey: 'tutorial.analysis.identity.desc',
|
||||
position: 'top',
|
||||
position: 'bottom',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
{
|
||||
targetSelector: '.bi2-selling-card',
|
||||
titleKey: 'tutorial.analysis.selling.title',
|
||||
descriptionKey: 'tutorial.analysis.selling.desc',
|
||||
position: 'top',
|
||||
position: 'bottom',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
{
|
||||
targetSelector: '.bi2-persona-grid',
|
||||
titleKey: 'tutorial.analysis.persona.title',
|
||||
descriptionKey: 'tutorial.analysis.persona.desc',
|
||||
position: 'top',
|
||||
position: 'bottom',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
{
|
||||
targetSelector: '.bi2-keyword-tags',
|
||||
titleKey: 'tutorial.analysis.keywords.title',
|
||||
descriptionKey: 'tutorial.analysis.keywords.desc',
|
||||
position: 'top',
|
||||
position: 'bottom',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
{
|
||||
targetSelector: '.bi2-generate-btn',
|
||||
titleKey: 'tutorial.analysis.generate.title',
|
||||
descriptionKey: 'tutorial.analysis.generate.desc',
|
||||
position: 'right',
|
||||
position: 'top',
|
||||
clickToAdvance: true,
|
||||
},
|
||||
],
|
||||
|
|
@ -104,19 +91,11 @@ export const tutorialSteps: TutorialStepDef[] = [
|
|||
{
|
||||
key: TUTORIAL_KEYS.ASSET,
|
||||
hints: [
|
||||
{
|
||||
targetSelector: '.asset-column.asset-column-left',
|
||||
titleKey: 'tutorial.asset.image.title',
|
||||
descriptionKey: 'tutorial.asset.image.desc',
|
||||
position: 'left',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
{
|
||||
targetSelector: '.asset-upload-zone',
|
||||
titleKey: 'tutorial.asset.upload.title',
|
||||
descriptionKey: 'tutorial.asset.upload.desc',
|
||||
position: 'left',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
{
|
||||
targetSelector: '.asset-ratio-section',
|
||||
|
|
@ -129,7 +108,6 @@ export const tutorialSteps: TutorialStepDef[] = [
|
|||
titleKey: 'tutorial.asset.next.title',
|
||||
descriptionKey: 'tutorial.asset.next.desc',
|
||||
position: 'top',
|
||||
clickToAdvance: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -140,48 +118,23 @@ export const tutorialSteps: TutorialStepDef[] = [
|
|||
targetSelector: '.genre-grid',
|
||||
titleKey: 'tutorial.sound.genre.title',
|
||||
descriptionKey: 'tutorial.sound.genre.desc',
|
||||
position: 'top',
|
||||
clickToAdvance: true,
|
||||
position: 'right',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
{
|
||||
targetSelector: '.language-selector-wrapper',
|
||||
titleKey: 'tutorial.sound.language.title',
|
||||
descriptionKey: 'tutorial.sound.language.desc',
|
||||
position: 'top',
|
||||
position: 'right',
|
||||
clickToAdvance: false,
|
||||
spotlightPaddingOverride: { bottom: 230 },
|
||||
},
|
||||
{
|
||||
targetSelector: '.btn-generate-sound',
|
||||
titleKey: 'tutorial.sound.generate.title',
|
||||
descriptionKey: 'tutorial.sound.generate.desc',
|
||||
position: 'right',
|
||||
position: 'top',
|
||||
clickToAdvance: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: TUTORIAL_KEYS.SOUND_LYRICS,
|
||||
hints: [
|
||||
{
|
||||
targetSelector: '.lyrics-display',
|
||||
titleKey: 'tutorial.sound.lyrics.title',
|
||||
descriptionKey: 'tutorial.sound.lyrics.desc',
|
||||
position: 'left',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: TUTORIAL_KEYS.SOUND_AUDIO,
|
||||
hints: [
|
||||
{
|
||||
targetSelector: '.audio-player',
|
||||
titleKey: 'tutorial.sound.audioPlayer.title',
|
||||
descriptionKey: 'tutorial.sound.audioPlayer.desc',
|
||||
position: 'left',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
{
|
||||
targetSelector: '.btn-video-generate',
|
||||
titleKey: 'tutorial.sound.video.title',
|
||||
|
|
@ -198,76 +151,36 @@ export const tutorialSteps: TutorialStepDef[] = [
|
|||
targetSelector: '.youtube-connect-section',
|
||||
titleKey: 'tutorial.myInfo.connect.title',
|
||||
descriptionKey: 'tutorial.myInfo.connect.desc',
|
||||
position: 'top',
|
||||
position: 'bottom',
|
||||
},
|
||||
{
|
||||
targetSelector: '.myinfo-social-btn.connected',
|
||||
targetSelector: '.youtube-connect-button',
|
||||
titleKey: 'tutorial.myInfo.button.title',
|
||||
descriptionKey: 'tutorial.myInfo.button.desc',
|
||||
position: 'top',
|
||||
},
|
||||
{
|
||||
targetSelector: '.myinfo-connected-accounts',
|
||||
titleKey: 'tutorial.myInfo.connected.title',
|
||||
descriptionKey: 'tutorial.myInfo.connected.desc',
|
||||
position: 'top',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
{
|
||||
targetSelector: '#sidebar-ado2-contents',
|
||||
titleKey: 'tutorial.myInfo.ado2.title',
|
||||
descriptionKey: 'tutorial.myInfo.ado2.desc',
|
||||
position: 'right',
|
||||
clickToAdvance: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: TUTORIAL_KEYS.ADO2_CONTENTS,
|
||||
hints: [
|
||||
{
|
||||
targetSelector: '.ado2-content-card',
|
||||
targetSelector: '.ado2-contents-list',
|
||||
titleKey: 'tutorial.ado2.list.title',
|
||||
descriptionKey: 'tutorial.ado2.list.desc',
|
||||
position: 'right',
|
||||
clickToAdvance: false,
|
||||
position: 'bottom',
|
||||
},
|
||||
{
|
||||
targetSelector: '.content-download-btn',
|
||||
targetSelector: '.ado2-upload-button',
|
||||
titleKey: 'tutorial.ado2.upload.title',
|
||||
descriptionKey: 'tutorial.ado2.upload.desc',
|
||||
position: 'right',
|
||||
clickToAdvance: true,
|
||||
position: 'top',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: TUTORIAL_KEYS.GENERATING,
|
||||
hints:[
|
||||
{
|
||||
targetSelector: '.comp2-info-section',
|
||||
titleKey: 'tutorial.completion.contentInfo.title',
|
||||
descriptionKey: 'tutorial.completion.contentInfo.desc',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
targetSelector: '.comp2-video-section',
|
||||
titleKey: 'tutorial.completion.generating.title',
|
||||
descriptionKey: 'tutorial.completion.generating.desc',
|
||||
position: 'right',
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: TUTORIAL_KEYS.COMPLETION,
|
||||
hints: [
|
||||
{
|
||||
targetSelector: '.comp2-video-section',
|
||||
titleKey: 'tutorial.completion.completion.title',
|
||||
descriptionKey: 'tutorial.completion.completion.desc',
|
||||
position: 'right',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
{
|
||||
targetSelector: '#sidebar-my-info',
|
||||
titleKey: 'tutorial.completion.myInfo.title',
|
||||
|
|
@ -281,21 +194,9 @@ export const tutorialSteps: TutorialStepDef[] = [
|
|||
key: TUTORIAL_KEYS.UPLOAD_MODAL,
|
||||
hints: [
|
||||
{
|
||||
targetSelector: '.social-posting-content',
|
||||
titleKey: 'tutorial.upload.seo.title',
|
||||
descriptionKey: 'tutorial.upload.seo.desc',
|
||||
position: 'right',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: TUTORIAL_KEYS.UPLOAD_FORM,
|
||||
hints: [
|
||||
{
|
||||
targetSelector: '.social-posting-form',
|
||||
titleKey: 'tutorial.upload.required.title',
|
||||
descriptionKey: 'tutorial.upload.required.desc',
|
||||
targetSelector: '.social-posting-field',
|
||||
titleKey: 'tutorial.upload.title.title',
|
||||
descriptionKey: 'tutorial.upload.title.desc',
|
||||
position: 'left',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
|
|
@ -319,55 +220,22 @@ export const tutorialSteps: TutorialStepDef[] = [
|
|||
key: TUTORIAL_KEYS.DASHBOARD,
|
||||
hints: [
|
||||
{
|
||||
targetSelector: '.stats-grid-8',
|
||||
targetSelector: '.dashboard-metrics-row',
|
||||
titleKey: 'tutorial.dashboard.metrics.title',
|
||||
descriptionKey: 'tutorial.dashboard.metrics.desc',
|
||||
position: 'bottom',
|
||||
},
|
||||
{
|
||||
targetSelector: '.yoy-chart-card',
|
||||
targetSelector: '.dashboard-chart-section',
|
||||
titleKey: 'tutorial.dashboard.chart.title',
|
||||
descriptionKey: 'tutorial.dashboard.chart.desc',
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
targetSelector: '.tutorial-center-anchor',
|
||||
titleKey: 'tutorial.dashboard.more.title',
|
||||
descriptionKey: 'tutorial.dashboard.more.desc',
|
||||
position: 'bottom',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: TUTORIAL_KEYS.CONTENT_CALENDAR,
|
||||
hints: [
|
||||
{
|
||||
targetSelector: '.calendar-grid-area',
|
||||
titleKey: 'tutorial.contentCalendar.grid.title',
|
||||
descriptionKey: 'tutorial.contentCalendar.grid.desc',
|
||||
position: 'top',
|
||||
clickToAdvance: true,
|
||||
},
|
||||
{
|
||||
targetSelector: '.calendar-side-panel',
|
||||
titleKey: 'tutorial.contentCalendar.panel.title',
|
||||
descriptionKey: 'tutorial.contentCalendar.panel.desc',
|
||||
position: 'left',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: TUTORIAL_KEYS.FEEDBACK,
|
||||
hints: [
|
||||
{
|
||||
targetSelector: '.tutorial-center-anchor',
|
||||
titleKey: 'tutorial.feedback.complete.title',
|
||||
descriptionKey: 'tutorial.feedback.complete.desc',
|
||||
position: 'bottom',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
{
|
||||
targetSelector: '.sidebar-inquiry-btn',
|
||||
titleKey: 'tutorial.feedback.title',
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
import React, { useState, useCallback } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { tutorialSteps, TutorialHint } from './tutorialSteps';
|
||||
|
||||
const SEEN_KEY = 'ado2_tutorial_seen';
|
||||
const PROGRESS_KEY = 'ado2_tutorial_progress';
|
||||
|
||||
// 전역 단일 활성 튜토리얼 관리 — 새 튜토리얼 시작 시 이전 것을 skip 처리
|
||||
let globalSkip: (() => void) | null = null;
|
||||
const SEEN_KEY = 'castad_tutorial_seen';
|
||||
|
||||
function getSeenKeys(): string[] {
|
||||
try {
|
||||
|
|
@ -20,32 +16,6 @@ function markSeen(key: string) {
|
|||
if (!seen.includes(key)) {
|
||||
localStorage.setItem(SEEN_KEY, JSON.stringify([...seen, key]));
|
||||
}
|
||||
clearProgress(key);
|
||||
}
|
||||
|
||||
function saveProgress(key: string, index: number) {
|
||||
try {
|
||||
const progress = JSON.parse(localStorage.getItem(PROGRESS_KEY) || '{}');
|
||||
progress[key] = index;
|
||||
localStorage.setItem(PROGRESS_KEY, JSON.stringify(progress));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function loadProgress(key: string): number {
|
||||
try {
|
||||
const progress = JSON.parse(localStorage.getItem(PROGRESS_KEY) || '{}');
|
||||
return progress[key] ?? 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function clearProgress(key: string) {
|
||||
try {
|
||||
const progress = JSON.parse(localStorage.getItem(PROGRESS_KEY) || '{}');
|
||||
delete progress[key];
|
||||
localStorage.setItem(PROGRESS_KEY, JSON.stringify(progress));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
interface UseTutorialReturn {
|
||||
|
|
@ -54,7 +24,7 @@ interface UseTutorialReturn {
|
|||
currentHintIndex: number;
|
||||
hints: TutorialHint[];
|
||||
tutorialKey: string | null;
|
||||
startTutorial: (key: string, onComplete?: () => void) => void;
|
||||
startTutorial: (key: string) => void;
|
||||
nextHint: () => void;
|
||||
prevHint: () => void;
|
||||
skipTutorial: () => void;
|
||||
|
|
@ -71,41 +41,22 @@ export function useTutorial(): UseTutorialReturn {
|
|||
const [currentHintIndex, setCurrentHintIndex] = useState(0);
|
||||
const [hints, setHints] = useState<TutorialHint[]>([]);
|
||||
const [tutorialKey, setTutorialKey] = useState<string | null>(null);
|
||||
const onCompleteRef = React.useRef<(() => void) | undefined>(undefined);
|
||||
|
||||
const startTutorial = useCallback((key: string, onComplete?: () => void) => {
|
||||
const startTutorial = useCallback((key: string) => {
|
||||
const step = tutorialSteps.find(s => s.key === key);
|
||||
if (!step || step.hints.length === 0) return;
|
||||
// 다른 인스턴스에서 활성화된 튜토리얼이 있으면 skip 처리
|
||||
globalSkip?.();
|
||||
const savedIndex = loadProgress(key);
|
||||
const resumeIndex = savedIndex < step.hints.length ? savedIndex : 0;
|
||||
onCompleteRef.current = onComplete;
|
||||
setHints(step.hints);
|
||||
setTutorialKey(key);
|
||||
setCurrentHintIndex(resumeIndex);
|
||||
setCurrentHintIndex(0);
|
||||
setIsActive(true);
|
||||
// 이 인스턴스의 skip을 전역에 등록
|
||||
globalSkip = () => {
|
||||
if (key) saveProgress(key, resumeIndex);
|
||||
setIsActive(false);
|
||||
setCurrentHintIndex(0);
|
||||
globalSkip = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const nextHint = useCallback(() => {
|
||||
setCurrentHintIndex(prev => {
|
||||
if (prev < hints.length - 1) {
|
||||
const next = prev + 1;
|
||||
if (tutorialKey) saveProgress(tutorialKey, next);
|
||||
return next;
|
||||
}
|
||||
// 마지막 힌트 완료 → seen 기록 + 진행 상태 삭제
|
||||
if (prev < hints.length - 1) return prev + 1;
|
||||
// 마지막 힌트 완료 → seen 기록
|
||||
setIsActive(false);
|
||||
if (tutorialKey) markSeen(tutorialKey);
|
||||
onCompleteRef.current?.();
|
||||
onCompleteRef.current = undefined;
|
||||
return 0;
|
||||
});
|
||||
}, [hints.length, tutorialKey]);
|
||||
|
|
@ -114,12 +65,11 @@ export function useTutorial(): UseTutorialReturn {
|
|||
setCurrentHintIndex(prev => Math.max(0, prev - 1));
|
||||
}, []);
|
||||
|
||||
// 건너뛰기: 현재 진행 인덱스 저장 후 오버레이 닫기 → 다음 방문 시 이어서 표시
|
||||
// 건너뛰기: 오버레이만 닫고 seen 기록 안 함 → 다음 방문 시 다시 표시
|
||||
const skipTutorial = useCallback(() => {
|
||||
if (tutorialKey) saveProgress(tutorialKey, currentHintIndex);
|
||||
setIsActive(false);
|
||||
setCurrentHintIndex(0);
|
||||
}, [tutorialKey, currentHintIndex]);
|
||||
}, []);
|
||||
|
||||
// 튜토리얼 다시 보기: 팝업 표시만
|
||||
const showRestartPopup = useCallback((key: string) => {
|
||||
|
|
|
|||
|
|
@ -21,68 +21,53 @@
|
|||
"next": "Next",
|
||||
"prev": "Back",
|
||||
"finish": "Done",
|
||||
"clickToProceed": "Click the highlighted item to continue.",
|
||||
"clickToFinish": "Click the highlighted item to finish the tutorial.",
|
||||
"landing": {
|
||||
"dropdown": { "title": "Choose Search Type", "desc": "Select URL or business name as your preferred search method." },
|
||||
"field": { "title": "Enter URL or Business Name", "desc": "Search for a place on Naver Maps, click Share, paste the URL — or type a business name and select it." },
|
||||
"dropdown": { "title": "Choose Search Type", "desc": "Search by URL or business name." },
|
||||
"field": { "title": "Enter URL or Business Name", "desc": "Paste a Naver Maps share URL or type a business name." },
|
||||
"button": { "title": "Start Brand Analysis", "desc": "Click to let AI analyze 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." },
|
||||
"identity": { "title": "Brand Identity", "desc": "Check the core values and market positioning analyzed by AI." },
|
||||
"selling": { "title": "Key Selling Points", "desc": "See your brand's strengths ranked by score." },
|
||||
"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." }
|
||||
"generate": { "title": "Generate Content", "desc": "Create video content based on the analysis. Click to proceed to the next step." }
|
||||
},
|
||||
"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." },
|
||||
"ratio": { "title": "Select Video Ratio", "desc": "Choose the ratio for the video to be generated." },
|
||||
"upload": { "title": "Add Images", "desc": "Upload or remove brand images." },
|
||||
"ratio": { "title": "Select Video Ratio", "desc": "Choose the best ratio for YouTube." },
|
||||
"next": { "title": "Next Step", "desc": "Proceed to the next step when ready." }
|
||||
},
|
||||
"sound": {
|
||||
"genre": { "title": "Select Genre", "desc": "Pick a music genre that fits your brand." },
|
||||
"language": { "title": "Select Language", "desc": "Choose the language for the sound. Then click Next." },
|
||||
"generate": { "title": "Generate Sound", "desc": "AI will create lyrics and music in your chosen genre and language." },
|
||||
"lyrics": { "title": "Generated Lyrics", "desc": "AI wrote these lyrics to match the music.\nCheck the generated lyrics." },
|
||||
"audioPlayer": { "title": "Preview the Music", "desc": "Music generation is complete.\nPress play to listen to the generated music." },
|
||||
"video": { "title": "Generate Video", "desc": "Click the button to start generating your video." }
|
||||
"language": { "title": "Select Language", "desc": "Choose the language for the sound." },
|
||||
"generate": { "title": "Generate Sound", "desc": "AI will create music tailored to your brand." },
|
||||
"video": { "title": "Generate Video", "desc": "Create your video after generating the sound." }
|
||||
},
|
||||
"myInfo": {
|
||||
"connect": { "title": "Connect Social Account", "desc": "You need to link a social account to upload videos." },
|
||||
"button": { "title": "Connect Now", "desc": "Click the social media button you want to connect." },
|
||||
"connected": { "title": "Connected Accounts", "desc": "Your linked social accounts appear here. Check after connecting." },
|
||||
"ado2": { "title": "Check My Contents", "desc": "After connecting, go to My Contents in the sidebar to view and upload your videos. Click to navigate." }
|
||||
"connect": { "title": "Connect YouTube", "desc": "Link your YouTube account to upload videos." },
|
||||
"button": { "title": "Connect Now", "desc": "Click to sign in with your Google account." }
|
||||
},
|
||||
"ado2": {
|
||||
"list": { "title": "Generated Videos", "desc": "View all AI-created videos here." },
|
||||
"upload": { "title": "Upload to Social Media", "desc": "Select a video and upload it to social media." }
|
||||
"upload": { "title": "Upload to YouTube", "desc": "Select a video and upload it to YouTube." }
|
||||
},
|
||||
"completion": {
|
||||
"contentInfo": { "title": "Content Info", "desc": "Check the title, genre, resolution, and lyrics of the generated content." },
|
||||
"generating": { "title": "Generating Video", "desc": "AI is creating your video. Please wait a moment." },
|
||||
"completion": { "title": "Video Complete!", "desc": "Your video is ready. Want to take a look?" },
|
||||
"upload": { "title": "Upload to YouTube", "desc": "Click the button to upload your video to YouTube." },
|
||||
"myInfo": { "title": "Connect Social Account", "desc": "To upload your video to YouTube, connect your social account in My Info. Click to go there." }
|
||||
},
|
||||
"upload": {
|
||||
"seo": { "title": "Title & Description", "desc": "AI is generating the title and description for your video.\nPlease wait a moment." },
|
||||
"required": { "title": "Required Fields", "desc": "Fields marked with * are required. Please fill them in before uploading." },
|
||||
"schedule": { "title": "Schedule Upload", "desc": "Post now or schedule for a specific time." },
|
||||
"submit": { "title": "Start Upload", "desc": "Click the Post button to start uploading." }
|
||||
"title": { "title": "Edit Title & Description", "desc": "Enter the title and description for YouTube." },
|
||||
"schedule": { "title": "Schedule Upload", "desc": "Upload now or schedule for a specific date." },
|
||||
"submit": { "title": "Start Upload", "desc": "Click the button to start uploading to YouTube." }
|
||||
},
|
||||
"dashboard": {
|
||||
"metrics": { "title": "Key Metrics", "desc": "Check views, subscribers, and other stats for content uploaded via ADO2." },
|
||||
"chart": { "title": "Growth Chart", "desc": "Track your channel's growth over time." },
|
||||
"more": { "title": "More Analytics", "desc": "Even more statistics are available at a glance on the dashboard." }
|
||||
},
|
||||
"contentCalendar": {
|
||||
"grid": { "title": "Content Calendar", "desc": "View your content schedule by date.\nWhy not select today?" },
|
||||
"panel": { "title": "Content List", "desc": "Check the detailed content schedule here." }
|
||||
},
|
||||
"feedback": {
|
||||
"complete": { "title": "Tutorial Complete 🎉", "desc": "You've completed everything from brand analysis to YouTube upload. Now give it a try on your own!" },
|
||||
"title": "Customer Feedback",
|
||||
"desc": "Share any issues or suggestions to help us improve."
|
||||
"metrics": { "title": "Key Metrics", "desc": "Check views, subscribers, and other stats." },
|
||||
"chart": { "title": "Growth Chart", "desc": "Track your channel's growth over time." }
|
||||
},
|
||||
"feedback": { "title": "Customer Feedback", "desc": "Share your thoughts to help us improve." },
|
||||
"restart": {
|
||||
"title": "Restart Tutorial?",
|
||||
"desc": "The tutorial will restart from the current screen.",
|
||||
|
|
@ -154,7 +139,7 @@
|
|||
"hero": {
|
||||
"searchTypeBusinessName": "Business Name",
|
||||
"placeholderBusinessName": "Enter a business name",
|
||||
"guideUrl": "Enter the Naver Place URL.",
|
||||
"guideUrl": "Select a place on Naver Maps, click Share,\nand paste the URL that appears.",
|
||||
"guideBusinessName": "Search by business name to retrieve information.",
|
||||
"errorUrlRequired": "Please enter a URL.",
|
||||
"errorNameRequired": "Please enter a business name.",
|
||||
|
|
|
|||
|
|
@ -21,68 +21,53 @@
|
|||
"next": "다음",
|
||||
"prev": "이전",
|
||||
"finish": "완료",
|
||||
"clickToProceed": "위 항목을 직접 선택하면 다음으로 넘어갑니다.",
|
||||
"clickToFinish": "위 항목을 직접 선택하면 튜토리얼이 완료됩니다.",
|
||||
"landing": {
|
||||
"dropdown": { "title": "검색 방식 선택", "desc": "URL 또는 업체명 중 원하는 방식을 선택하세요." },
|
||||
"field": { "title": "URL 또는 업체명 입력", "desc": "네이버 지도에서 장소를 검색하고 공유를 클릭하여 나온 URL을 붙여넣거나 업체명을 입력하고 선택하세요." },
|
||||
"field": { "title": "URL 또는 업체명 입력", "desc": "네이버 지도에서 장소를 검색하고 공유 클릭하여 나온 URL을 붙여넣거나 업체명을 입력하고 선택하세요." },
|
||||
"button": { "title": "브랜드 분석 시작", "desc": "버튼을 누르면 AI가 브랜드를 분석하기 시작해요." }
|
||||
},
|
||||
"analysis": {
|
||||
"identity": { "title": "브랜드 정체성", "desc": "AI가 분석한 펜션의 핵심 가치와 시장 포지셔닝을 확인하세요." },
|
||||
"selling": { "title": "주요 셀링 포인트", "desc": "펜션의 강점을 확인할 수 있어요." },
|
||||
"persona": { "title": "주요 고객 유형", "desc": "어떤 고객이 이 펜션을 찾는지 분석했어요." },
|
||||
"identity": { "title": "브랜드 정체성", "desc": "AI가 분석한 브랜드의 핵심 가치와 시장 포지셔닝을 확인하세요." },
|
||||
"selling": { "title": "주요 셀링 포인트", "desc": "브랜드의 강점을 확인할 수 있어요." },
|
||||
"persona": { "title": "주요 고객 유형", "desc": "어떤 고객이 이 브랜드를 찾는지 분석했어요." },
|
||||
"keywords": { "title": "추천 타겟 키워드", "desc": "고객이 검색할 가능성이 높은 키워드들이에요." },
|
||||
"generate": { "title": "콘텐츠 생성", "desc": "분석 결과를 바탕으로 영상을 만들어 보세요.\n클릭하면 카카오 로그인으로 이동합니다." }
|
||||
"generate": { "title": "콘텐츠 생성", "desc": "분석 결과를 바탕으로 영상 콘텐츠를 만들어 보세요. 버튼을 클릭하면 카카오 로그인으로 이동합니다." }
|
||||
},
|
||||
"asset": {
|
||||
"image": { "title": "이미지 목록", "desc": "네이버 Place에서 사진이에요. 더보기를 누르면 나머지 사진도 볼 수 있고 X를 눌러 삭제 할 수 있어요." },
|
||||
"upload": { "title": "이미지 추가", "desc": "이미지를 자유롭게 추가 할 수 있어요." },
|
||||
"ratio": { "title": "영상 비율 선택", "desc": "생성 할 영상의 비율을 선택하세요." },
|
||||
"upload": { "title": "이미지 추가", "desc": "브랜드 이미지를 추가하거나 삭제할 수 있어요." },
|
||||
"ratio": { "title": "영상 비율 선택", "desc": "유튜브에 최적화된 비율을 선택하세요." },
|
||||
"next": { "title": "다음 단계로", "desc": "설정이 완료되면 다음 단계로 진행하세요." }
|
||||
},
|
||||
"sound": {
|
||||
"genre": { "title": "장르 선택", "desc": "영상에 어울리는 음악 장르를 선택하세요." },
|
||||
"language": { "title": "언어 선택", "desc": "사운드의 언어를 선택하세요. 다음을 눌러주세요." },
|
||||
"generate": { "title": "사운드 생성", "desc": "AI가 선택한 장르와 언어로 가사와 음악을 생성해요." },
|
||||
"lyrics": { "title": "생성된 가사", "desc": "AI가 음악에 맞는 가사를 만들었어요.\n생성된 가사를 확인하세요." },
|
||||
"audioPlayer": { "title": "음악 미리 듣기", "desc": "음악 생성이 완료되었어요.\n재생 버튼을 눌러 생성된 음악을 미리 들어보세요." },
|
||||
"video": { "title": "영상 생성", "desc": "버튼을 클릭해서 영상 생성을 시작하세요." }
|
||||
"language": { "title": "언어 선택", "desc": "사운드의 언어를 선택하세요." },
|
||||
"generate": { "title": "사운드 생성", "desc": "AI가 브랜드에 맞는 음악을 생성해요." },
|
||||
"video": { "title": "영상 생성", "desc": "사운드 생성 후 영상을 만들 수 있어요." }
|
||||
},
|
||||
"myInfo": {
|
||||
"connect": { "title": "소셜 연동", "desc": "영상을 업로드하려면 소셜 계정을 연동해야 해요." },
|
||||
"button": { "title": "연동하기", "desc": "원하는 소셜미디어 버튼을 클릭해서 연동하세요." },
|
||||
"connected": { "title": "연동된 계정", "desc": "연동된 소셜 계정 목록이에요. 연동 후 여기서 확인할 수 있어요." },
|
||||
"ado2": { "title": "내 콘텐츠 확인", "desc": "연동 후 내 콘텐츠 메뉴에서 생성된 영상을 확인하고 업로드할 수 있어요. 클릭해서 이동하세요." }
|
||||
"button": { "title": "연동하기", "desc": "원하는 소셜미디어 버튼을 클릭해서 연동하세요." }
|
||||
},
|
||||
"ado2": {
|
||||
"list": { "title": "생성된 영상 목록", "desc": "ADO2에서 만든 영상들을 여기서 확인할 수 있어요." },
|
||||
"upload": { "title": "소셜 업로드", "desc": "선택해서 소셜미디어에 업로드하세요." }
|
||||
"upload": { "title": "소셜 업로드", "desc": "원하는 영상을 선택해서 소셜미디어에 업로드하세요." }
|
||||
},
|
||||
"completion": {
|
||||
"contentInfo": { "title": "콘텐츠 정보", "desc": "생성된 콘텐츠의 제목, 장르, 규격, 가사 정보를 확인하세요." },
|
||||
"generating": { "title": "영상 제작 중", "desc": "AI가 영상을 만들고 있어요. 잠시만 기다려 주세요." },
|
||||
"completion": { "title": "영상 완성!", "desc": "영상 제작이 완료되었어요. 영상을 확인해 볼까요?" },
|
||||
"upload": { "title": "업로드", "desc": "버튼을 눌러 완성된 영상을 업로드하세요." },
|
||||
"myInfo": { "title": "소셜 계정 연동", "desc": "영상을 유튜브에 업로드하려면 내 정보에서 소셜 계정을 연동해야 해요. 클릭해서 이동하세요." }
|
||||
},
|
||||
"upload": {
|
||||
"seo": { "title": "제목 및 설명", "desc": "영상에 표시될 제목과 설명을 AI가 만들고 있어요. \n잠시만 기다려 주세요." },
|
||||
"required": { "title": "필수 항목", "desc": "영상을 업로드 하기 전 *는 필수항목으로 반드시 확인해 주세요." },
|
||||
"schedule": { "title": "업로드 예약", "desc": "지금 게시하거나 원하는 시간에 예약할 수 있어요." },
|
||||
"submit": { "title": "업로드 시작", "desc": "게시 버튼을 눌러 업로드를 시작하세요." }
|
||||
"title": { "title": "제목 및 설명 입력", "desc": "유튜브에 표시될 제목과 설명을 입력하세요." },
|
||||
"schedule": { "title": "업로드 예약", "desc": "즉시 업로드하거나 원하는 날짜에 예약할 수 있어요." },
|
||||
"submit": { "title": "업로드 시작", "desc": "버튼을 눌러 유튜브 업로드를 시작하세요." }
|
||||
},
|
||||
"dashboard": {
|
||||
"metrics": { "title": "핵심 지표", "desc": "조회수, 구독자 등 ADO2로 업로드한 콘텐츠의 주요 통계를 확인하세요." },
|
||||
"chart": { "title": "성장 추이 차트", "desc": "기간별 성장 추이를 그래프로 확인할 수 있어요." },
|
||||
"more": { "title": "더 많은 통계", "desc": "그 외에도 다양한 통계를 대시보드에서 한눈에 확인할 수 있어요." }
|
||||
},
|
||||
"contentCalendar": {
|
||||
"grid": { "title": "콘텐츠 캘린더", "desc": "날짜별로 콘텐츠 스케줄을 확인할 수 있어요. \n오늘 날짜를 선택해 볼까요?" },
|
||||
"panel": { "title": "콘텐츠 목록", "desc": "자세한 콘텐츠 스케줄을 확인 할수 있어요." }
|
||||
},
|
||||
"feedback": {
|
||||
"complete": { "title": "튜토리얼 완료 🎉", "desc": "브랜드 분석부터 유튜브 업로드까지 모든 과정을 완료했어요. 이제 직접 시작해 보세요!" },
|
||||
"title": "고객의견",
|
||||
"desc": "서비스 이용 중 불편한 점이나 개선 의견을 보내주세요."
|
||||
"metrics": { "title": "핵심 지표", "desc": "조회수, 구독자 등 채널의 주요 통계를 확인하세요." },
|
||||
"chart": { "title": "성장 추이 차트", "desc": "기간별 성장 추이를 그래프로 확인할 수 있어요." }
|
||||
},
|
||||
"feedback": { "title": "고객의견", "desc": "서비스 이용 중 불편한 점이나 개선 의견을 보내주세요." },
|
||||
"restart": {
|
||||
"title": "튜토리얼을 다시 시작할까요?",
|
||||
"desc": "현재 화면부터 튜토리얼이 다시 시작됩니다.",
|
||||
|
|
@ -153,8 +138,8 @@
|
|||
"landing": {
|
||||
"hero": {
|
||||
"searchTypeBusinessName": "업체명",
|
||||
"placeholderBusinessName": "업체명을 입력하세요.",
|
||||
"guideUrl": "네이버 Place URL을 입력하세요.",
|
||||
"placeholderBusinessName": "업체명을 입력하세요",
|
||||
"guideUrl": "네이버 지도에서 우리 펜션을 검색한 뒤 공유를 클릭하고,\nURL을 복사해서 여기에 붙여넣어 주세요.",
|
||||
"guideBusinessName": "업체명으로 검색하여 정보를 가져옵니다.",
|
||||
"errorUrlRequired": "URL을 입력해주세요.",
|
||||
"errorNameRequired": "업체명을 입력해주세요.",
|
||||
|
|
@ -170,7 +155,7 @@
|
|||
"title": "ADO2.AI에 오신 것을 환영합니다.",
|
||||
"subtitle": "분석, 제작, 배포까지 콘텐츠 마케팅의 전과정을 자동화",
|
||||
"feature1Title": "비즈니스 핵심 정보 분석",
|
||||
"feature1Desc": "홈페이지, 네이버 지도, 블로그 등의\nURL을 입력하세요.",
|
||||
"feature1Desc": "홈페이지, 네이버 지도, 블로그 등의\nURL을 입력하세요",
|
||||
"feature2Title": "홍보 콘텐츠 자동 제작",
|
||||
"feature2Desc": "분석된 정보를 바탕으로\n비즈니스에 맞는 음악, 자막, 노래, 영상을\n자동으로 제작해요",
|
||||
"feature3Title": "멀티채널 자동 배포",
|
||||
|
|
@ -189,8 +174,8 @@
|
|||
},
|
||||
"urlInput": {
|
||||
"searchTypeBusinessName": "업체명",
|
||||
"placeholderBusinessName": "업체명을 입력하세요.",
|
||||
"guideUrl": "네이버 Place URL을 입력하세요.",
|
||||
"placeholderBusinessName": "업체명을 입력하세요",
|
||||
"guideUrl": "네이버 지도에서 우리 펜션을 검색한 뒤 공유를 클릭하고,\nURL을 복사해서 여기에 붙여넣어 주세요.",
|
||||
"guideBusinessName": "업체명으로 검색하여 정보를 가져옵니다.",
|
||||
"searchButton": "검색하기",
|
||||
"searching": "검색 중...",
|
||||
|
|
|
|||
|
|
@ -44,17 +44,12 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
|||
|
||||
const tutorial = useTutorial();
|
||||
|
||||
// 영상 생성 중 튜토리얼 트리거 (생성 상태 안내 -> 콘텐츠 정보 -> 내 정보 이동)
|
||||
// 영상 완료 시 튜토리얼 트리거
|
||||
useEffect(() => {
|
||||
const isComplete = videoStatus === 'complete';
|
||||
const isProcessing = videoStatus === 'generating' || videoStatus === 'polling';
|
||||
|
||||
if (isProcessing && !tutorial.isActive && !tutorial.hasSeen(TUTORIAL_KEYS.GENERATING)) {
|
||||
tutorial.startTutorial(TUTORIAL_KEYS.GENERATING);
|
||||
} else if (isComplete && !tutorial.isActive && !tutorial.hasSeen(TUTORIAL_KEYS.COMPLETION)) {
|
||||
if (videoStatus === 'complete' && !tutorial.hasSeen(TUTORIAL_KEYS.COMPLETION)) {
|
||||
tutorial.startTutorial(TUTORIAL_KEYS.COMPLETION);
|
||||
}
|
||||
}, [videoStatus, tutorial]);
|
||||
}, [videoStatus]);
|
||||
|
||||
// 소셜 미디어 포스팅 모달
|
||||
const [showSocialModal, setShowSocialModal] = useState(false);
|
||||
|
|
|
|||
|
|
@ -758,7 +758,7 @@ const ContentCalendarContent: React.FC<ContentCalendarContentProps> = ({ onNavig
|
|||
flex: 1, minHeight: 0,
|
||||
}}>
|
||||
{/* 캘린더 영역 */}
|
||||
<div className="calendar-grid-area" style={{
|
||||
<div style={{
|
||||
gridColumn: '1 / span 8',
|
||||
backgroundColor: '#01393b',
|
||||
borderRadius: 20, padding: 16,
|
||||
|
|
@ -780,7 +780,7 @@ const ContentCalendarContent: React.FC<ContentCalendarContentProps> = ({ onNavig
|
|||
</div>
|
||||
|
||||
{/* 오른쪽 패널 */}
|
||||
<div className="calendar-side-panel" style={{
|
||||
<div style={{
|
||||
gridColumn: '9 / span 1',
|
||||
backgroundColor: '#01393b', borderRadius: 20,
|
||||
display: 'flex', flexDirection: 'column', overflow: 'hidden',
|
||||
|
|
|
|||
|
|
@ -320,10 +320,8 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
|||
localStorage.setItem(ACTIVE_ITEM_KEY, activeItem);
|
||||
}, [activeItem]);
|
||||
|
||||
// wizardStep 변경 시 튜토리얼 트리거 (사이드바 메뉴 화면에서는 제외)
|
||||
const SIDEBAR_ITEMS = ['대시보드', 'ADO2 콘텐츠', '내 정보', '콘텐츠 캘린더'];
|
||||
// wizardStep 변경 시 튜토리얼 트리거
|
||||
useEffect(() => {
|
||||
if (SIDEBAR_ITEMS.includes(activeItem)) return;
|
||||
const timer = setTimeout(() => {
|
||||
if (wizardStep === 1 && !tutorial.hasSeen(TUTORIAL_KEYS.ASSET)) {
|
||||
tutorial.startTutorial(TUTORIAL_KEYS.ASSET);
|
||||
|
|
@ -332,7 +330,7 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
|||
}
|
||||
}, 600);
|
||||
return () => clearTimeout(timer);
|
||||
}, [wizardStep, activeItem]);
|
||||
}, [wizardStep]);
|
||||
|
||||
// activeItem 변경 시 튜토리얼 트리거
|
||||
useEffect(() => {
|
||||
|
|
@ -342,8 +340,6 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
|||
tutorial.startTutorial(TUTORIAL_KEYS.ADO2_CONTENTS);
|
||||
} else if (activeItem === '대시보드' && !tutorial.hasSeen(TUTORIAL_KEYS.DASHBOARD)) {
|
||||
tutorial.startTutorial(TUTORIAL_KEYS.DASHBOARD);
|
||||
} else if (activeItem === '콘텐츠 캘린더' && !tutorial.hasSeen(TUTORIAL_KEYS.CONTENT_CALENDAR)) {
|
||||
tutorial.startTutorial(TUTORIAL_KEYS.CONTENT_CALENDAR);
|
||||
}
|
||||
}, [activeItem]);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,6 @@ import React, { useState, useRef, useEffect } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { generateLyric, waitForLyricComplete, generateSong, waitForSongComplete } from '../../utils/api';
|
||||
import { LANGUAGE_MAP } from '../../types/api';
|
||||
import { useTutorial } from '../../components/Tutorial/useTutorial';
|
||||
import { TUTORIAL_KEYS } from '../../components/Tutorial/tutorialSteps';
|
||||
import TutorialOverlay from '../../components/Tutorial/TutorialOverlay';
|
||||
|
||||
interface BusinessInfo {
|
||||
customer_name: string;
|
||||
|
|
@ -46,7 +43,6 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
|||
videoGenerationProgress = 0
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const tutorial = useTutorial();
|
||||
const [selectedType, setSelectedType] = useState('보컬');
|
||||
const [selectedLang, setSelectedLang] = useState('한국어');
|
||||
const [selectedGenre, setSelectedGenre] = useState('자동 선택');
|
||||
|
|
@ -393,28 +389,7 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
|||
|
||||
const isGenerating = status === 'generating_lyric' || status === 'generating_song' || status === 'polling';
|
||||
|
||||
// 가사 생성 완료 시 가사 튜토리얼 트리거
|
||||
useEffect(() => {
|
||||
if (status === 'generating_song' && !tutorial.hasSeen(TUTORIAL_KEYS.SOUND_LYRICS)) {
|
||||
const timer = setTimeout(() => {
|
||||
tutorial.startTutorial(TUTORIAL_KEYS.SOUND_LYRICS);
|
||||
}, 400);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
// 음악 생성 완료 시 오디오 플레이어 튜토리얼 트리거
|
||||
useEffect(() => {
|
||||
if (status === 'complete' && !tutorial.hasSeen(TUTORIAL_KEYS.SOUND_AUDIO)) {
|
||||
const timer = setTimeout(() => {
|
||||
tutorial.startTutorial(TUTORIAL_KEYS.SOUND_AUDIO);
|
||||
}, 400);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="sound-studio-page">
|
||||
{audioUrl && (
|
||||
<audio ref={audioRef} src={audioUrl} preload="metadata" />
|
||||
|
|
@ -668,17 +643,6 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
|||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{tutorial.isActive && (
|
||||
<TutorialOverlay
|
||||
hints={tutorial.hints}
|
||||
currentIndex={tutorial.currentHintIndex}
|
||||
onNext={tutorial.nextHint}
|
||||
onPrev={tutorial.prevHint}
|
||||
onSkip={tutorial.skipTutorial}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomp
|
|||
|
||||
const getPlaceholder = () => {
|
||||
return searchType === 'url'
|
||||
? 'https://naver.me/abcdef'
|
||||
? 'https://www.castad.com'
|
||||
: t('urlInput.placeholderBusinessName');
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue