UI 및 튜토리얼 수정
parent
d5647bdfa5
commit
c938c81875
206
index.css
206
index.css
|
|
@ -8080,6 +8080,7 @@
|
|||
color: var(--color-mint);
|
||||
font-size: var(--text-sm);
|
||||
text-align: center;
|
||||
white-space: pre-line;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -9759,6 +9760,47 @@
|
|||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* SEO 자동 생성 shimmer 효과 */
|
||||
@keyframes seo-shimmer {
|
||||
0% { background-position: 200% center; }
|
||||
100% { background-position: -200% center; }
|
||||
}
|
||||
|
||||
.seo-input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.seo-input-wrapper .social-posting-input,
|
||||
.seo-input-wrapper .social-posting-textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.seo-shimmer-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 12px;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
font-size: 14px;
|
||||
font-family: 'Pretendard', sans-serif;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(166, 255, 234, 0.25) 0%,
|
||||
#a6ffea 40%,
|
||||
rgba(166, 255, 234, 0.25) 80%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: seo-shimmer 3s linear infinite;
|
||||
}
|
||||
|
||||
.seo-input-wrapper:has(.social-posting-textarea) .seo-shimmer-text {
|
||||
top: 14px;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.social-posting-input:focus,
|
||||
.social-posting-select:focus,
|
||||
.social-posting-textarea:focus {
|
||||
|
|
@ -10589,29 +10631,59 @@
|
|||
|
||||
.tutorial-tooltip {
|
||||
position: fixed;
|
||||
width: 300px;
|
||||
background: #1a2630;
|
||||
border: 1px solid rgba(166, 255, 234, 0.25);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
pointer-events: all;
|
||||
z-index: 10002;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
width: 370px;
|
||||
padding: 20px;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--Color-teal-600, #046266);
|
||||
background: var(--Color-teal-800, #002224);
|
||||
}
|
||||
|
||||
.tutorial-tooltip-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #a6ffea;
|
||||
margin: 0 0 8px;
|
||||
align-self: stretch;
|
||||
color: rgba(255, 255, 255, 0.70);
|
||||
/* 14 */
|
||||
font-family: Pretendard;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
}
|
||||
|
||||
.tutorial-tooltip-desc {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
margin: 0 0 14px;
|
||||
display: flex;
|
||||
padding-bottom: 16px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
align-self: stretch;
|
||||
line-height: 1.5;
|
||||
white-space: pre-line;
|
||||
align-self: stretch;
|
||||
color: var(--Color-white, #FFF);
|
||||
font-family: Pretendard;
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 24px; /* 133.333% */
|
||||
letter-spacing: -0.108px;
|
||||
}
|
||||
|
||||
.tutorial-tooltip-note {
|
||||
align-self: stretch;
|
||||
color: rgba(255, 255, 255, 0.60);
|
||||
font-family: Pretendard;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 100%;
|
||||
letter-spacing: -0.072px;
|
||||
margin: -8px 0 14px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.tutorial-tooltip-footer {
|
||||
|
|
@ -10620,23 +10692,35 @@
|
|||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tutorial-tooltip-footer-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tutorial-tooltip-counter {
|
||||
color: rgba(255, 255, 255, 0.60);
|
||||
font-family: Pretendard;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 100%; /* 12px */
|
||||
letter-spacing: -0.072px;
|
||||
}
|
||||
|
||||
.tutorial-tooltip-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tutorial-btn-skip {
|
||||
color: rgba(255, 255, 255, 0.60);
|
||||
font-family: Pretendard;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 100%; /* 12px */
|
||||
letter-spacing: -0.072px;
|
||||
}
|
||||
|
||||
.tutorial-btn-skip:hover {
|
||||
|
|
@ -10644,13 +10728,20 @@
|
|||
}
|
||||
|
||||
.tutorial-btn-prev {
|
||||
display: flex;
|
||||
padding: 8px 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-radius: 4px;
|
||||
background: var(--Color-teal-200, #9BCACC);
|
||||
color: var(--Color-teal-800, #002224);
|
||||
font-family: Pretendard;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
padding: 5px 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 100%; /* 13px */
|
||||
letter-spacing: -0.078px;
|
||||
}
|
||||
|
||||
.tutorial-btn-prev:hover {
|
||||
|
|
@ -10658,17 +10749,66 @@
|
|||
}
|
||||
|
||||
.tutorial-btn-next {
|
||||
display: flex;
|
||||
padding: 8px 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-radius: 4px;
|
||||
background: var(--Color-teal-200, #9BCACC);
|
||||
color: var(--Color-teal-800, #002224);
|
||||
font-family: Pretendard;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
background: #6366f1;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
padding: 5px 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 100%; /* 13px */
|
||||
letter-spacing: -0.078px;
|
||||
}
|
||||
|
||||
.tutorial-btn-next:hover {
|
||||
background: #4f46e5;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
|
||||
/* 말풍선 variant — 색상은 기본 tooltip과 동일, 꼬리만 추가 */
|
||||
.tutorial-tooltip-bubble {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* 꼬리 (화살표) */
|
||||
.tutorial-tooltip-bubble::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 10px solid transparent;
|
||||
}
|
||||
|
||||
.tutorial-tooltip-bubble--bottom::before {
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-bottom-color: #1a2630;
|
||||
}
|
||||
|
||||
.tutorial-tooltip-bubble--top::before {
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-top-color: #1a2630;
|
||||
}
|
||||
|
||||
.tutorial-tooltip-bubble--right::before {
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-right-color: #1a2630;
|
||||
}
|
||||
|
||||
.tutorial-tooltip-bubble--left::before {
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-left-color: #1a2630;
|
||||
}
|
||||
|
||||
.tutorial-tooltip-action-hint {
|
||||
|
|
|
|||
|
|
@ -440,6 +440,7 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||
onNext={tutorial.nextHint}
|
||||
onPrev={tutorial.prevHint}
|
||||
onSkip={tutorial.skipTutorial}
|
||||
groupProgress={tutorial.groupProgress}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -565,48 +566,60 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||
<label className="social-posting-label">
|
||||
{t('social.postTitleLabel')} <span className="required">*</span>
|
||||
</label>
|
||||
<div className="seo-input-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder={isLoadingAutoDescription ? t('social.autoSeoTitle') : t('social.postTitlePlaceholder')}
|
||||
// placeholder={t('social.postTitlePlaceholder')}
|
||||
placeholder={isLoadingAutoDescription ? '' : t('social.postTitlePlaceholder')}
|
||||
className="social-posting-input"
|
||||
maxLength={100}
|
||||
disabled={isLoadingAutoDescription}
|
||||
/>
|
||||
{isLoadingAutoDescription && (
|
||||
<span className="seo-shimmer-text">{t('social.autoSeoTitle')}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="social-posting-char-count">{title.length}/100</span>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="social-posting-field">
|
||||
<label className="social-posting-label">{t('social.postContentLabel')}</label>
|
||||
<div className="seo-input-wrapper">
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder={t(isLoadingAutoDescription ? t('social.autoSeoDescription') : t('social.postContentPlaceholder'))}
|
||||
// placeholder={t('social.postContentPlaceholder')}
|
||||
placeholder={isLoadingAutoDescription ? '' : t('social.postContentPlaceholder')}
|
||||
className="social-posting-textarea"
|
||||
maxLength={5000}
|
||||
rows={4}
|
||||
disabled={isLoadingAutoDescription}
|
||||
/>
|
||||
{isLoadingAutoDescription && (
|
||||
<span className="seo-shimmer-text">{t('social.autoSeoDescription')}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="social-posting-char-count">{description.length}/5,000</span>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="social-posting-field">
|
||||
<label className="social-posting-label">{t('social.tagsLabel')}</label>
|
||||
<div className="seo-input-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
value={tags}
|
||||
onChange={(e) => setTags(e.target.value)}
|
||||
placeholder={t(isLoadingAutoDescription ? t('social.autoSeoTags') : t('social.tagsPlaceholder'))}
|
||||
// placeholder={t('social.tagsPlaceholder')}
|
||||
placeholder={isLoadingAutoDescription ? '' : t('social.tagsPlaceholder')}
|
||||
className="social-posting-input"
|
||||
maxLength={500}
|
||||
disabled={isLoadingAutoDescription}
|
||||
/>
|
||||
{isLoadingAutoDescription && (
|
||||
<span className="seo-shimmer-text">{t('social.autoSeoTags')}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="social-posting-char-count">{tags.length}/500</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -811,6 +824,7 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||
onNext={tutorial.nextHint}
|
||||
onPrev={tutorial.prevHint}
|
||||
onSkip={tutorial.skipTutorial}
|
||||
groupProgress={tutorial.groupProgress}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ interface TutorialOverlayProps {
|
|||
onNext: () => void;
|
||||
onPrev: () => void;
|
||||
onSkip: () => void;
|
||||
groupProgress?: { groupTotal: number; groupOffset: number } | null;
|
||||
}
|
||||
|
||||
const PADDING = 8;
|
||||
|
|
@ -53,9 +54,7 @@ function getSpotlightRect(
|
|||
};
|
||||
}
|
||||
|
||||
function calcTooltipPos(rect: Rect, position: TutorialHint['position']): TooltipPos {
|
||||
const tooltipW = 300;
|
||||
const tooltipH = 140;
|
||||
function calcTooltipPos(rect: Rect, position: TutorialHint['position'], tooltipW = 300, tooltipH = 160): TooltipPos {
|
||||
|
||||
switch (position) {
|
||||
case 'bottom':
|
||||
|
|
@ -87,12 +86,17 @@ const TutorialOverlay: React.FC<TutorialOverlayProps> = ({
|
|||
onNext,
|
||||
onPrev,
|
||||
onSkip,
|
||||
groupProgress,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [targetRect, setTargetRect] = useState<Rect | null>(null);
|
||||
const tooltipRef = React.useRef<HTMLDivElement>(null);
|
||||
const [tooltipSize, setTooltipSize] = useState({ w: 300, h: 160 });
|
||||
|
||||
const hint = hints[currentIndex];
|
||||
const isLast = currentIndex === hints.length - 1;
|
||||
// 그룹이 있으면 그룹의 마지막 키 + 마지막 힌트일 때만 완료, 그룹 없으면 기존대로
|
||||
const isFinish = isLast && (groupProgress ? groupProgress.isLastKeyInGroup : true);
|
||||
|
||||
const updateRect = useCallback(() => {
|
||||
if (!hint) return;
|
||||
|
|
@ -155,17 +159,32 @@ const TutorialOverlay: React.FC<TutorialOverlayProps> = ({
|
|||
};
|
||||
}, [hint, updateRect, onNext]);
|
||||
|
||||
// 툴팁 DOM 크기 측정 — 힌트가 바뀔 때만 재측정
|
||||
useEffect(() => {
|
||||
const el = tooltipRef.current;
|
||||
if (!el) return;
|
||||
const { offsetWidth, offsetHeight } = el;
|
||||
if (offsetWidth && offsetHeight) {
|
||||
setTooltipSize(prev =>
|
||||
prev.w === offsetWidth && prev.h === offsetHeight
|
||||
? prev
|
||||
: { w: offsetWidth, h: offsetHeight }
|
||||
);
|
||||
}
|
||||
}, [currentIndex, hints]);
|
||||
|
||||
if (!hint) return null;
|
||||
|
||||
const tooltipPos: TooltipPos = targetRect
|
||||
? calcTooltipPos(targetRect, hint.position)
|
||||
: {
|
||||
top: window.innerHeight / 2 - 70,
|
||||
left: window.innerWidth / 2 - 150,
|
||||
};
|
||||
|
||||
const spotlightPadding = hint.spotlightPadding ?? PADDING;
|
||||
const spotlightRect = targetRect ? getSpotlightRect(targetRect, spotlightPadding, hint.spotlightPaddingOverride) : null;
|
||||
const spotlightRect = (targetRect && !hint.noSpotlight) ? getSpotlightRect(targetRect, spotlightPadding, hint.spotlightPaddingOverride) : null;
|
||||
|
||||
// 툴팁은 spotlightRect 기준으로 배치해야 스포트라이트와 겹치지 않음
|
||||
const tooltipPos: TooltipPos = (spotlightRect ?? targetRect)
|
||||
? calcTooltipPos((spotlightRect ?? targetRect)!, hint.position, tooltipSize.w, tooltipSize.h)
|
||||
: {
|
||||
top: window.innerHeight / 2 - tooltipSize.h / 2,
|
||||
left: window.innerWidth / 2 - tooltipSize.w / 2,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tutorial-overlay-root">
|
||||
|
|
@ -212,21 +231,23 @@ const TutorialOverlay: React.FC<TutorialOverlayProps> = ({
|
|||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="tutorial-overlay-blocker" style={{ inset: 0 }} />
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className="tutorial-tooltip"
|
||||
ref={tooltipRef}
|
||||
className={`tutorial-tooltip${hint.variant === 'bubble' ? ` tutorial-tooltip-bubble tutorial-tooltip-bubble--${hint.position}` : ''}`}
|
||||
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>
|
||||
{hint.noteKey && (
|
||||
<p className="tutorial-tooltip-note">{t(hint.noteKey)}</p>
|
||||
)}
|
||||
|
||||
<div className="tutorial-tooltip-footer">
|
||||
<span className="tutorial-tooltip-counter">
|
||||
{currentIndex + 1} / {hints.length}
|
||||
{groupProgress ? groupProgress.groupOffset + 1 : currentIndex + 1} / {groupProgress ? groupProgress.groupTotal : hints.length}
|
||||
</span>
|
||||
<div className="tutorial-tooltip-actions">
|
||||
<button className="tutorial-btn-skip" onClick={onSkip}>
|
||||
|
|
@ -237,9 +258,9 @@ const TutorialOverlay: React.FC<TutorialOverlayProps> = ({
|
|||
{t('tutorial.prev')}
|
||||
</button>
|
||||
)}
|
||||
{(hint.clickToAdvance !== true || isLast) && (
|
||||
{(hint.clickToAdvance !== true || isFinish) && (
|
||||
<button className="tutorial-btn-next" onClick={onNext}>
|
||||
{isLast ? t('tutorial.finish') : t('tutorial.next')}
|
||||
{isFinish ? t('tutorial.finish') : t('tutorial.next')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ export interface TutorialHint {
|
|||
titleKey: string;
|
||||
descriptionKey: string;
|
||||
position: 'top' | 'bottom' | 'left' | 'right';
|
||||
noteKey?: string;
|
||||
variant?: 'bubble';
|
||||
clickToAdvance?: boolean;
|
||||
advanceSelector?: string;
|
||||
noSpotlight?: boolean;
|
||||
spotlightPadding?: number;
|
||||
spotlightPaddingOverride?: { top?: number; right?: number; bottom?: number; left?: number };
|
||||
}
|
||||
|
|
@ -16,8 +19,6 @@ export interface TutorialStepDef {
|
|||
|
||||
export const TUTORIAL_KEYS = {
|
||||
LANDING: 'landing',
|
||||
LANDING_URL: 'landingUrl',
|
||||
LANDING_NAME: 'landingName',
|
||||
ANALYSIS: 'analysis',
|
||||
ASSET: 'asset',
|
||||
SOUND: 'sound',
|
||||
|
|
@ -34,105 +35,94 @@ export const TUTORIAL_KEYS = {
|
|||
FEEDBACK: 'feedback',
|
||||
} as const;
|
||||
|
||||
// 같은 페이지에 속하는 튜토리얼 키 그룹 — 진행 카운터를 합산해서 표시
|
||||
export const TUTORIAL_PAGE_GROUPS: string[][] = [
|
||||
[TUTORIAL_KEYS.SOUND, TUTORIAL_KEYS.SOUND_LYRICS, TUTORIAL_KEYS.SOUND_AUDIO],
|
||||
[TUTORIAL_KEYS.GENERATING, TUTORIAL_KEYS.COMPLETION],
|
||||
[TUTORIAL_KEYS.UPLOAD_MODAL, TUTORIAL_KEYS.UPLOAD_FORM],
|
||||
];
|
||||
|
||||
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-container',
|
||||
titleKey: 'tutorial.landing.intro.title',
|
||||
descriptionKey: 'tutorial.landing.intro.desc',
|
||||
position: 'top',
|
||||
clickToAdvance: true,
|
||||
},
|
||||
{
|
||||
targetSelector: '.hero-dropdown-menu',
|
||||
targetSelector: '.hero-dropdown-trigger',
|
||||
titleKey: 'tutorial.landing.dropdown.title',
|
||||
descriptionKey: 'tutorial.landing.dropdown.desc',
|
||||
position: 'top',
|
||||
clickToAdvance: true,
|
||||
position: 'left',
|
||||
clickToAdvance: false,
|
||||
noSpotlight: true,
|
||||
variant: 'bubble',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: TUTORIAL_KEYS.LANDING_URL,
|
||||
hints: [
|
||||
{
|
||||
targetSelector: '.hero-input-wrapper',
|
||||
titleKey: 'tutorial.landing.url.title',
|
||||
descriptionKey: 'tutorial.landing.url.desc',
|
||||
position: 'bottom',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
{
|
||||
targetSelector: '.hero-button',
|
||||
titleKey: 'tutorial.landing.button.title',
|
||||
descriptionKey: 'tutorial.landing.button.desc',
|
||||
position: 'bottom',
|
||||
clickToAdvance: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: TUTORIAL_KEYS.LANDING_NAME,
|
||||
hints: [
|
||||
{
|
||||
targetSelector: '.hero-input-wrapper',
|
||||
titleKey: 'tutorial.landing.name.title',
|
||||
descriptionKey: 'tutorial.landing.name.desc',
|
||||
position: 'top',
|
||||
clickToAdvance: false,
|
||||
spotlightPaddingOverride: { top: 0, right: 0, bottom: 290, left: -105 },
|
||||
},
|
||||
{
|
||||
targetSelector: '.hero-button',
|
||||
titleKey: 'tutorial.landing.button.title',
|
||||
descriptionKey: 'tutorial.landing.button.desc',
|
||||
position: 'bottom',
|
||||
clickToAdvance: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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',
|
||||
titleKey: 'tutorial.landing.field.title',
|
||||
descriptionKey: 'tutorial.landing.field.desc',
|
||||
position: 'right',
|
||||
clickToAdvance: true,
|
||||
clickToAdvance: false,
|
||||
noSpotlight: true,
|
||||
variant: 'bubble',
|
||||
},
|
||||
{
|
||||
targetSelector: '.hero-button',
|
||||
titleKey: 'tutorial.landing.button.title',
|
||||
descriptionKey: 'tutorial.landing.button.desc',
|
||||
position: 'right',
|
||||
clickToAdvance: false,
|
||||
noSpotlight: true,
|
||||
variant: 'bubble',
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// 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: [
|
||||
|
|
@ -174,7 +164,7 @@ export const tutorialSteps: TutorialStepDef[] = [
|
|||
titleKey: 'tutorial.sound.genre.title',
|
||||
descriptionKey: 'tutorial.sound.genre.desc',
|
||||
position: 'top',
|
||||
clickToAdvance: true,
|
||||
clickToAdvance: false,
|
||||
},
|
||||
{
|
||||
targetSelector: '.language-selector-wrapper',
|
||||
|
|
@ -244,8 +234,10 @@ export const tutorialSteps: TutorialStepDef[] = [
|
|||
targetSelector: '.myinfo-social-buttons',
|
||||
titleKey: 'tutorial.myInfo.connect.title',
|
||||
descriptionKey: 'tutorial.myInfo.connect.desc',
|
||||
noteKey: 'tutorial.myInfo.connect.note',
|
||||
position: 'top',
|
||||
clickToAdvance: true,
|
||||
spotlightPaddingOverride: { right: -505},
|
||||
},
|
||||
{
|
||||
targetSelector: '.myinfo-connected-accounts',
|
||||
|
|
@ -273,6 +265,20 @@ export const tutorialSteps: TutorialStepDef[] = [
|
|||
position: 'right',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
{
|
||||
targetSelector: '.content-upload-btn',
|
||||
titleKey: 'tutorial.ado2.download.title',
|
||||
descriptionKey: 'tutorial.ado2.download.desc',
|
||||
position: 'right',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
{
|
||||
targetSelector: '.content-delete-btn',
|
||||
titleKey: 'tutorial.ado2.delete.title',
|
||||
descriptionKey: 'tutorial.ado2.delete.desc',
|
||||
position: 'right',
|
||||
clickToAdvance: false,
|
||||
},
|
||||
{
|
||||
targetSelector: '.content-download-btn',
|
||||
titleKey: 'tutorial.ado2.upload.title',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,21 @@
|
|||
import React, { useState, useCallback } from 'react';
|
||||
import { tutorialSteps, TutorialHint } from './tutorialSteps';
|
||||
import { tutorialSteps, TutorialHint, TUTORIAL_PAGE_GROUPS } from './tutorialSteps';
|
||||
|
||||
// 현재 키가 속한 그룹의 전체 힌트 수와 현재까지의 offset 반환
|
||||
function getGroupProgress(key: string, currentIndex: number): { groupTotal: number; groupOffset: number; isLastKeyInGroup: boolean } | null {
|
||||
const group = TUTORIAL_PAGE_GROUPS.find(g => g.includes(key));
|
||||
if (!group) return null;
|
||||
let offset = 0;
|
||||
let total = 0;
|
||||
for (const k of group) {
|
||||
const step = tutorialSteps.find(s => s.key === k);
|
||||
const count = step?.hints.length ?? 0;
|
||||
if (k === key) offset = total;
|
||||
total += count;
|
||||
}
|
||||
const isLastKeyInGroup = group[group.length - 1] === key;
|
||||
return { groupTotal: total, groupOffset: offset + currentIndex, isLastKeyInGroup };
|
||||
}
|
||||
|
||||
const SEEN_KEY = 'ado2_tutorial_seen';
|
||||
const PROGRESS_KEY = 'ado2_tutorial_progress';
|
||||
|
|
@ -54,6 +70,7 @@ interface UseTutorialReturn {
|
|||
currentHintIndex: number;
|
||||
hints: TutorialHint[];
|
||||
tutorialKey: string | null;
|
||||
groupProgress: { groupTotal: number; groupOffset: number } | null;
|
||||
startTutorial: (key: string, onComplete?: () => void, forceFromStart?: boolean) => void;
|
||||
nextHint: () => void;
|
||||
prevHint: () => void;
|
||||
|
|
@ -155,6 +172,7 @@ export function useTutorial(): UseTutorialReturn {
|
|||
currentHintIndex,
|
||||
hints,
|
||||
tutorialKey,
|
||||
groupProgress: tutorialKey ? getGroupProgress(tutorialKey, currentHintIndex) : null,
|
||||
startTutorial,
|
||||
nextHint,
|
||||
prevHint,
|
||||
|
|
|
|||
|
|
@ -22,11 +22,10 @@
|
|||
"prev": "Back",
|
||||
"finish": "Done",
|
||||
"landing": {
|
||||
"intro": { "title": "Welcome to ADO2 Tutorial", "desc": "We'll guide you through ADO2 step by step.\nFirst, please select your search method." },
|
||||
"dropdown": { "title": "Choose Search Type", "desc": "Click the dropdown and select URL or business name as your preferred search method." },
|
||||
"url": { "title": "Enter Naver Place URL", "desc": "Search for a place on Naver Maps, click Share, and paste the URL here." },
|
||||
"name": { "title": "Enter Business Name", "desc": "Type a business name and the autocomplete list will appear.\nChoose your business from the list." },
|
||||
"button": { "title": "Start Brand Analysis", "desc": "Click to let AI analyze your brand." }
|
||||
"intro": { "title": "Welcome to ADO2 Tutorial", "desc": "We'll guide you through ADO2 step by step." },
|
||||
"dropdown": { "title": "Choose Search Type", "desc": "Select URL or business name from the dropdown." },
|
||||
"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." },
|
||||
|
|
@ -52,12 +51,14 @@
|
|||
},
|
||||
"myInfo": {
|
||||
"myInfo": { "title": "My Info", "desc": "In My Info, you can manage your social connections and view connected accounts." },
|
||||
"connect": { "title": "Connect Now", "desc": "Click the YouTube connect button.\n(Instagram connection is under development.)" },
|
||||
"connect": { "title": "Connect Now", "desc": "Click the YouTube connect button.", "note": "Instagram connection is coming soon." },
|
||||
"connected": { "title": "Connected Accounts", "desc": "Your linked social accounts appear here.\nCheck after connecting." },
|
||||
"ado2": { "title": "ADO2 Contents", "desc": "You can now upload the generated video.\nClick to navigate." }
|
||||
},
|
||||
"ado2": {
|
||||
"list": { "title": "Generated Videos", "desc": "View all AI-created videos here." },
|
||||
"download": { "title": "Download", "desc": "Download the video to your device." },
|
||||
"delete": { "title": "Delete", "desc": "Remove videos you no longer need." },
|
||||
"upload": { "title": "Upload to Social Media", "desc": "Select a video and upload it to social media." }
|
||||
},
|
||||
"completion": {
|
||||
|
|
@ -67,8 +68,8 @@
|
|||
"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." },
|
||||
"seo": { "title": "Title & Description", "desc": "AI is generating the title and description for your video. Please wait a moment." },
|
||||
"required": { "title": "Required Fields", "desc": "Fields marked with * are required.\nPlease check them 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." }
|
||||
},
|
||||
|
|
@ -133,9 +134,9 @@
|
|||
"invalidVideoInfo": "Video information is invalid. (missing video_id)",
|
||||
"uploadStartFailed": "Failed to start upload.",
|
||||
"uploadFailed": "Upload failed.",
|
||||
"autoSeoTitle": "This will be automatically generated. please wait.",
|
||||
"autoSeoDescription": "This will be automatically generated. please wait.",
|
||||
"autoSeoTags": "This will be automatically generated. please wait."
|
||||
"autoSeoTitle": "Auto-generating... (30–60 sec)",
|
||||
"autoSeoDescription": "Auto-generating... (30–60 sec)",
|
||||
"autoSeoTags": "Auto-generating... (30–60 sec)"
|
||||
},
|
||||
"upload": {
|
||||
"title": "YouTube Upload",
|
||||
|
|
@ -193,7 +194,7 @@
|
|||
"urlInput": {
|
||||
"searchTypeBusinessName": "Business Name",
|
||||
"placeholderBusinessName": "Enter a business name",
|
||||
"guideUrl": "Select a place on Naver Maps, click Share,\nand paste the URL that appears.",
|
||||
"guideUrl": "Enter a Naver Place URL.",
|
||||
"guideBusinessName": "Search by business name to retrieve information.",
|
||||
"searchButton": "Search",
|
||||
"searching": "Searching...",
|
||||
|
|
@ -238,8 +239,8 @@
|
|||
"videoGenerating": "Generating Video",
|
||||
"noBusinessInfo": "No business information. Please try again.",
|
||||
"noImageUploadInfo": "No image upload information. Please go back to the previous step and try again.",
|
||||
"generatingLyrics": "Generating lyrics...",
|
||||
"generatingSong": "Generating song...",
|
||||
"generatingLyrics": "Generating lyrics...\n(30–60 sec)",
|
||||
"generatingSong": "Generating music...\n(1–2 min)",
|
||||
"songQueued": "Song generation queued...",
|
||||
"retryMessage": "Regenerating due to timeout... ({{count}}/{{max}})",
|
||||
"lyricGenerationFailed": "Lyrics generation request failed.",
|
||||
|
|
|
|||
|
|
@ -22,10 +22,9 @@
|
|||
"prev": "이전",
|
||||
"finish": "완료",
|
||||
"landing": {
|
||||
"intro": { "title": "ADO2 튜토리얼 시작", "desc": "ADO2 사용 방법을 단계별로 안내해 드릴게요.\n먼저 검색 방식을 선택해 주세요." },
|
||||
"dropdown": { "title": "검색 방식 선택", "desc": "URL 또는 업체명 중 원하는 방식을 선택하세요." },
|
||||
"url": { "title": "네이버 Place URL 입력", "desc": "네이버 지도에서 장소를 검색하고 공유를 클릭하여 나온 URL을 붙여넣으세요." },
|
||||
"name": { "title": "업체명 입력", "desc": "업체명을 입력하면 자동완성 목록이 나타나요.\n목록에서 원하는 업체를 선택하세요." },
|
||||
"intro": { "title": "ADO2 튜토리얼 시작", "desc": "ADO2 사용 방법을 단계별로 안내해 드릴게요." },
|
||||
"dropdown": { "title": "검색 방식 선택", "desc": "드롭다운에서 URL 또는 업체명 중 원하는 방식을 선택하세요." },
|
||||
"field": { "title": "입력하기", "desc": "URL 방식이라면 네이버 지도 공유 URL,\n업체명 방식이라면 업체명을 입력하고 목록에서 선택하세요." },
|
||||
"button": { "title": "브랜드 분석 시작", "desc": "버튼을 누르면 AI가 브랜드를 분석하기 시작해요." }
|
||||
},
|
||||
"analysis": {
|
||||
|
|
@ -44,31 +43,33 @@
|
|||
"sound": {
|
||||
"genre": { "title": "장르 선택", "desc": "영상에 어울리는 음악 장르를 선택하세요." },
|
||||
"language": { "title": "언어 선택", "desc": "사운드의 언어를 선택할 수 있어요. \n이미 선택된 한국어로 진행해볼까요?" },
|
||||
"generate": { "title": "사운드 생성", "desc": "AI가 선택한 장르와 언어로 음악을 생성해요." },
|
||||
"generate": { "title": "사운드 생성", "desc": "버튼을 클릭하면 AI가 선택한 장르와 언어로 가사와 음악을 생성해요."},
|
||||
"lyrics": { "title": "가사 생성 완료", "desc": "AI가 선택한 언어로 가사를 만들었어요.\n생성된 가사를 확인하세요." },
|
||||
"lyricsWait": { "title": "음악 생성 중", "desc": "가사를 바탕으로 AI가 음악을 만들고 있어요.\n잠시만 기다려 주세요." },
|
||||
"audioPlayer": { "title": "음악 미리 듣기", "desc": "음악 생성이 완료되었어요.\n재생 버튼을 눌러 생성된 음악을 미리 들어보세요." },
|
||||
"audioPlayer": { "title": "음악 미리 듣기", "desc": "음악 생성이 완료되었어요.\n재생 버튼을 눌러 생성된 음악을 들어보세요." },
|
||||
"video": { "title": "영상 생성", "desc": "버튼을 클릭해서 영상 생성을 시작하세요." }
|
||||
},
|
||||
"myInfo": {
|
||||
"myInfo": { "title": "내 정보", "desc": "내 정보에서는 소셜 연결과 연결된 계정을 확인 할 수 있어요." },
|
||||
"connect": { "title": "연결하기", "desc": "YouTube 연결 버튼을 클릭하세요. \n(Instaram연결은 개발 중입니다.)" },
|
||||
"connect": { "title": "연결하기", "desc": "YouTube 연결 버튼을 누르면 연결 페이지로 이동합니다.", "note": "Instargram 연결은 오픈 예정입니다." },
|
||||
"connected": { "title": "연결 계정", "desc": "연결된 소셜 계정 목록이에요. \n연결 후 여기서 확인할 수 있어요." },
|
||||
"ado2": { "title": "ADO2 콘텐츠", "desc": "이제 생성된 영상을 업로드할 수 있어요. \n클릭해서 이동하세요." }
|
||||
},
|
||||
"ado2": {
|
||||
"list": { "title": "생성된 영상 목록", "desc": "ADO2에서 만든 영상들을 확인할 수 있어요." },
|
||||
"download": { "title": "다운로드", "desc": "영상을 다운로드 할 수 있어요." },
|
||||
"delete": { "title": "삭제", "desc": "필요없는 영상을 삭제할 수 있어요." },
|
||||
"upload": { "title": "소셜 업로드", "desc": "선택해서 소셜미디어에 업로드하세요." }
|
||||
},
|
||||
"completion": {
|
||||
"contentInfo": { "title": "콘텐츠 정보", "desc": "콘텐츠의 제목,장르,규격,가사를 확인하세요." },
|
||||
"generating": { "title": "영상 제작 중", "desc": "AI가 영상을 만들고 있어요. \n잠시만 기다려 주세요." },
|
||||
"completion": { "title": "영상 완성!", "desc": "영상 제작이 완료되었어요. \n영상을 확인해 볼까요?" },
|
||||
"myInfo": { "title": "소셜 계정 연동", "desc": "영상을 유튜브에 업로드하려면 내 정보에서 소셜 계정을 연동해야 해요. 클릭해서 이동하세요." }
|
||||
"myInfo": { "title": "소셜 계정 연동", "desc": "영상을 유튜브에 업로드하려면 내 정보에서 소셜 계정을 연동해야 해요. \n클릭해서 이동하세요." }
|
||||
},
|
||||
"upload": {
|
||||
"seo": { "title": "제목 및 설명", "desc": "영상에 표시될 제목과 설명을 AI가 만들고 있어요. \n잠시만 기다려 주세요." },
|
||||
"required": { "title": "필수 항목", "desc": "영상을 업로드 하기 전 *는 필수항목으로 반드시 확인해 주세요." },
|
||||
"seo": { "title": "제목 및 설명", "desc": "영상의 제목과 설명을 AI가 만들고 있어요. 잠시만 기다려 주세요." },
|
||||
"required": { "title": "필수 항목", "desc": "영상을 업로드 하기 전 *는 필수 항목으로 \n반드시 확인해 주세요." },
|
||||
"schedule": { "title": "업로드 예약", "desc": "지금 게시하거나 원하는 시간에 예약할 수 있어요." },
|
||||
"submit": { "title": "업로드 시작", "desc": "게시 버튼을 눌러 업로드를 시작하세요." }
|
||||
},
|
||||
|
|
@ -133,9 +134,9 @@
|
|||
"invalidVideoInfo": "영상 정보가 올바르지 않습니다. (video_id 누락)",
|
||||
"uploadStartFailed": "업로드 시작에 실패했습니다.",
|
||||
"uploadFailed": "업로드에 실패했습니다.",
|
||||
"autoSeoTitle": "자동으로 작성중입니다. 기다려주세요.",
|
||||
"autoSeoDescription": "자동으로 작성중입니다. 기다려주세요.",
|
||||
"autoSeoTags": "자동으로 작성중입니다. 기다려주세요."
|
||||
"autoSeoTitle": "자동으로 작성중입니다. (30~60초 소요)",
|
||||
"autoSeoDescription": "자동으로 작성중입니다. (30~60초 소요)",
|
||||
"autoSeoTags": "자동으로 작성중입니다. (30~60초 소요)"
|
||||
},
|
||||
"upload": {
|
||||
"title": "YouTube 업로드",
|
||||
|
|
@ -157,7 +158,7 @@
|
|||
"hero": {
|
||||
"searchTypeBusinessName": "업체명",
|
||||
"placeholderBusinessName": "업체명을 입력하세요.",
|
||||
"guideUrl": "네이버 Place URL을 입력하세요.",
|
||||
"guideUrl": "네이버지도에서 장소를 검색하고 공유 선택, \n나오는 URL을 붙여 넣어 주세요.",
|
||||
"guideBusinessName": "업체명으로 검색하여 정보를 가져옵니다.",
|
||||
"errorUrlRequired": "URL을 입력해주세요.",
|
||||
"errorNameRequired": "업체명을 입력해주세요.",
|
||||
|
|
@ -175,7 +176,7 @@
|
|||
"feature1Title": "비즈니스 핵심 정보 분석",
|
||||
"feature1Desc": "홈페이지, 네이버 지도, 블로그 등의\nURL을 입력하세요.",
|
||||
"feature2Title": "홍보 콘텐츠 자동 제작",
|
||||
"feature2Desc": "분석된 정보를 바탕으로\n비즈니스에 맞는 음악, 자막, 노래, 영상을\n자동으로 제작해요",
|
||||
"feature2Desc": "분석된 정보를 바탕으로\n비즈니스에 맞는 음악, 자막, 영상을\n자동으로 제작해요",
|
||||
"feature3Title": "멀티채널 자동 배포",
|
||||
"feature3Desc": "완성된 영상은 다운로드하거나\n바로 SNS에 업로드 할 수 있어요"
|
||||
},
|
||||
|
|
@ -193,7 +194,7 @@
|
|||
"urlInput": {
|
||||
"searchTypeBusinessName": "업체명",
|
||||
"placeholderBusinessName": "업체명을 입력하세요.",
|
||||
"guideUrl": "네이버 Place URL을 입력하세요.",
|
||||
"guideUrl": "네이버지도에서 장소를 검색하고 공유 선택, \n나오는 URL을 붙여 넣어 주세요.",
|
||||
"guideBusinessName": "업체명으로 검색하여 정보를 가져옵니다.",
|
||||
"searchButton": "검색하기",
|
||||
"searching": "검색 중...",
|
||||
|
|
@ -238,9 +239,9 @@
|
|||
"videoGenerating": "영상 생성 중",
|
||||
"noBusinessInfo": "비즈니스 정보가 없습니다. 다시 시도해주세요.",
|
||||
"noImageUploadInfo": "이미지 업로드 정보가 없습니다. 이전 단계로 돌아가 다시 시도해주세요.",
|
||||
"generatingLyrics": "가사를 생성하고 있습니다...",
|
||||
"generatingSong": "노래를 생성하고 있습니다...",
|
||||
"songQueued": "노래 생성 대기 중...",
|
||||
"generatingLyrics": "가사를 생성하고 있습니다.\n(30~60초 소요)",
|
||||
"generatingSong": "음악을 생성하고 있습니다.\n(1~2분 소요)",
|
||||
"songQueued": "음악 생성 대기 중...",
|
||||
"retryMessage": "시간 초과로 재생성 중... ({{count}}/{{max}})",
|
||||
"lyricGenerationFailed": "가사 생성 요청에 실패했습니다.",
|
||||
"lyricNotReceived": "가사를 받지 못했습니다.",
|
||||
|
|
@ -269,7 +270,7 @@
|
|||
"statusPlanned": "예약됨",
|
||||
"statusWaiting": "대기 중",
|
||||
"statusTranscribing": "트랜스크립션 중",
|
||||
"statusRendering": "렌더링 중",
|
||||
"statusRendering": "생성 중 (2~3분 소요)",
|
||||
"statusSucceeded": "완료",
|
||||
"statusDefault": "처리 중...",
|
||||
"aiOptimization": "AI 최적화",
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@ const AnalysisResultSection: React.FC<AnalysisResultSectionProps> = ({ onBack, o
|
|||
onNext={tutorial.nextHint}
|
||||
onPrev={tutorial.prevHint}
|
||||
onSkip={tutorial.skipTutorial}
|
||||
groupProgress={tutorial.groupProgress}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -463,6 +463,7 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
|||
onNext={tutorial.nextHint}
|
||||
onPrev={tutorial.prevHint}
|
||||
onSkip={tutorial.skipTutorial}
|
||||
groupProgress={tutorial.groupProgress}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -506,8 +506,8 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
|||
// 스크롤이 필요한 페이지: 대시보드, 비즈니스 설정, 브랜드 분석(0), ADO2 콘텐츠, 내 정보
|
||||
const needsScroll = activeItem === '대시보드' || activeItem === '비즈니스 설정' || activeItem === 'ADO2 콘텐츠' || activeItem === '내 정보' || activeItem === '콘텐츠 캘린더' || isBrandAnalysis;
|
||||
|
||||
// 현재 화면에 맞는 튜토리얼 키 반환
|
||||
const getCurrentTutorialKey = (): string => {
|
||||
// 현재 화면에 맞는 튜토리얼 키 반환 (없으면 null)
|
||||
const getCurrentTutorialKey = (): string | null => {
|
||||
if (activeItem === '내 정보') return TUTORIAL_KEYS.MY_INFO;
|
||||
if (activeItem === 'ADO2 콘텐츠') return TUTORIAL_KEYS.ADO2_CONTENTS;
|
||||
if (activeItem === '대시보드') return TUTORIAL_KEYS.DASHBOARD;
|
||||
|
|
@ -516,15 +516,16 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
|||
if (wizardStep === 1) return TUTORIAL_KEYS.ASSET;
|
||||
if (wizardStep === 2) return TUTORIAL_KEYS.SOUND;
|
||||
}
|
||||
return TUTORIAL_KEYS.ASSET;
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleTutorialRestart = () => {
|
||||
tutorial.showRestartPopup(getCurrentTutorialKey());
|
||||
tutorial.showRestartPopup(getCurrentTutorialKey()!);
|
||||
};
|
||||
|
||||
const tutorialUI = (
|
||||
<>
|
||||
{getCurrentTutorialKey() && (
|
||||
<button
|
||||
className="tutorial-restart-fab"
|
||||
onClick={handleTutorialRestart}
|
||||
|
|
@ -536,6 +537,7 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
|||
</svg>
|
||||
<span>{t('sidebar.tutorialRestart')}</span>
|
||||
</button>
|
||||
)}
|
||||
{tutorial.isActive && (
|
||||
<TutorialOverlay
|
||||
hints={tutorial.hints}
|
||||
|
|
@ -543,6 +545,7 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
|||
onNext={tutorial.nextHint}
|
||||
onPrev={tutorial.prevHint}
|
||||
onSkip={tutorial.skipTutorial}
|
||||
groupProgress={tutorial.groupProgress}
|
||||
/>
|
||||
)}
|
||||
{tutorial.isRestartPopupVisible && (
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ interface UrlInputContentProps {
|
|||
const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomplete, onTestData, error }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [searchType, setSearchType] = useState<SearchType>('url');
|
||||
const [searchType, setSearchType] = useState<SearchType>('name');
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [autocompleteResults, setAutocompleteResults] = useState<AccommodationSearchItem[]>([]);
|
||||
const [isAutocompleteLoading, setIsAutocompleteLoading] = useState(false);
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ const orbConfigs: OrbConfig[] = [
|
|||
const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, onTestData, onNext, error: externalError, scrollProgress = 0 }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [searchType, setSearchType] = useState<SearchType>('url');
|
||||
const [searchType, setSearchType] = useState<SearchType>('name');
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [localError, setLocalError] = useState('');
|
||||
const [isLoadingTest, setIsLoadingTest] = useState(false);
|
||||
|
|
@ -97,10 +97,9 @@ const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, on
|
|||
const debounceRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const tutorial = useTutorial();
|
||||
|
||||
// 첫 방문 시 랜딩 튜토리얼 시작 (URL/업체명 분기 튜토리얼도 아직 안 본 경우)
|
||||
// 첫 방문 시 랜딩 튜토리얼 시작
|
||||
useEffect(() => {
|
||||
const neitherBranchSeen = !tutorial.hasSeen(TUTORIAL_KEYS.LANDING_URL) && !tutorial.hasSeen(TUTORIAL_KEYS.LANDING_NAME);
|
||||
if (!tutorial.hasSeen(TUTORIAL_KEYS.LANDING) && neitherBranchSeen) {
|
||||
if (!tutorial.hasSeen(TUTORIAL_KEYS.LANDING)) {
|
||||
const timer = setTimeout(() => {
|
||||
tutorial.startTutorial(TUTORIAL_KEYS.LANDING);
|
||||
}, 800);
|
||||
|
|
@ -381,11 +380,6 @@ const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, on
|
|||
onClick={() => {
|
||||
setSearchType(option.value);
|
||||
setIsDropdownOpen(false);
|
||||
if (option.value === 'url' && !tutorial.hasSeen(TUTORIAL_KEYS.LANDING_URL)) {
|
||||
setTimeout(() => tutorial.startTutorial(TUTORIAL_KEYS.LANDING_URL, undefined, true), 300);
|
||||
} else if (option.value === 'name' && !tutorial.hasSeen(TUTORIAL_KEYS.LANDING_NAME)) {
|
||||
setTimeout(() => tutorial.startTutorial(TUTORIAL_KEYS.LANDING_NAME, undefined, true), 300);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
|
|
@ -534,6 +528,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, on
|
|||
onNext={tutorial.nextHint}
|
||||
onPrev={tutorial.prevHint}
|
||||
onSkip={tutorial.skipTutorial}
|
||||
groupProgress={tutorial.groupProgress}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue