Files
jaengseung-made/app/gyeol/page.tsx
gahusb 262d6c3ed1 feat(gyeol): /gyeol 단일 페이지 통합 — 9 step state, localStorage 복구
- layout: radial 그라데이션 배경 + metadata (robots noindex)
- page: step state + Q1~Q7 컴포넌트 조합
- 진입 시 localStorage 복구 + step 변경 시 저장 + 제출 시 clear
- 최종 제출: completion_seconds, user_agent, referrer, utm_* 자동 수집
- 에러 토스트 표시

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 05:31:54 +09:00

165 lines
5.0 KiB
TypeScript

'use client';
import { useEffect, useRef, useState } from 'react';
import IntroStep from './components/IntroStep';
import Q1Step from './components/Q1Step';
import Q2Step from './components/Q2Step';
import Q3Step from './components/Q3Step';
import Q4Step from './components/Q4Step';
import Q5Step from './components/Q5Step';
import Q6Step from './components/Q6Step';
import Q7Step from './components/Q7Step';
import ThanksStep from './components/ThanksStep';
import type { SurveyResponse, SurveyStep } from '@/lib/survey/types';
import { loadProgress, saveProgress, clearProgress } from '@/lib/survey/storage';
export default function GyeolPage() {
const [step, setStep] = useState<SurveyStep>('intro');
const [response, setResponse] = useState<Partial<SurveyResponse>>({});
const [submitting, setSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const startedAtRef = useRef<number | null>(null);
// 진입 시 localStorage 복구
useEffect(() => {
const saved = loadProgress();
if (saved) {
setStep(saved.step);
setResponse(saved.response);
startedAtRef.current = saved.startedAt;
}
}, []);
// step 변경 시 진행 상태 저장 (질문 step만)
useEffect(() => {
if (step !== 'intro' && step !== 'thanks' && startedAtRef.current) {
saveProgress({
step,
response,
startedAt: startedAtRef.current,
});
}
}, [step, response]);
function handleStart() {
if (!startedAtRef.current) {
startedAtRef.current = Date.now();
}
setStep('q1');
}
function applyPartialAndAdvance(partial: Partial<SurveyResponse>, nextStep: SurveyStep) {
setResponse((prev) => ({ ...prev, ...partial }));
setStep(nextStep);
}
function goBack(prevStep: SurveyStep) {
setStep(prevStep);
}
async function handleFinalSubmit(partial: Partial<SurveyResponse>) {
const finalResponse: SurveyResponse = {
...response,
...partial,
completion_seconds: startedAtRef.current
? Math.floor((Date.now() - startedAtRef.current) / 1000)
: undefined,
user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,
referrer: typeof document !== 'undefined' ? (document.referrer || undefined) : undefined,
utm_source: typeof window !== 'undefined'
? (new URLSearchParams(window.location.search).get('utm_source') ?? undefined)
: undefined,
utm_medium: typeof window !== 'undefined'
? (new URLSearchParams(window.location.search).get('utm_medium') ?? undefined)
: undefined,
utm_campaign: typeof window !== 'undefined'
? (new URLSearchParams(window.location.search).get('utm_campaign') ?? undefined)
: undefined,
};
setSubmitting(true);
setSubmitError(null);
try {
const res = await fetch('/api/survey', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(finalResponse),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error ?? '제출 실패');
}
clearProgress();
setResponse(finalResponse);
setStep('thanks');
} catch (e) {
setSubmitError(e instanceof Error ? e.message : '제출 실패');
} finally {
setSubmitting(false);
}
}
return (
<>
{step === 'intro' && <IntroStep onStart={handleStart} />}
{step === 'q1' && (
<Q1Step
initial={response}
onPrev={() => setStep('intro')}
onNext={(p) => applyPartialAndAdvance(p, 'q2')}
/>
)}
{step === 'q2' && (
<Q2Step
initial={response}
onPrev={() => goBack('q1')}
onNext={(p) => applyPartialAndAdvance(p, 'q3')}
/>
)}
{step === 'q3' && (
<Q3Step
initial={response}
onPrev={() => goBack('q2')}
onNext={(p) => applyPartialAndAdvance(p, 'q4')}
/>
)}
{step === 'q4' && (
<Q4Step
initial={response}
onPrev={() => goBack('q3')}
onNext={(p) => applyPartialAndAdvance(p, 'q5')}
/>
)}
{step === 'q5' && (
<Q5Step
initial={response}
onPrev={() => goBack('q4')}
onNext={(p) => applyPartialAndAdvance(p, 'q6')}
/>
)}
{step === 'q6' && (
<Q6Step
initial={response}
onPrev={() => goBack('q5')}
onNext={(p) => applyPartialAndAdvance(p, 'q7')}
/>
)}
{step === 'q7' && (
<Q7Step
initial={response}
onPrev={() => goBack('q6')}
onSubmit={handleFinalSubmit}
submitting={submitting}
/>
)}
{step === 'thanks' && <ThanksStep emailEntered={!!response.email} />}
{submitError && (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 px-4 py-3 rounded-xl bg-red-500/20 border border-red-400/40 text-red-200 text-sm">
{submitError}
</div>
)}
</>
);
}