diff --git a/app/gyeol/layout.tsx b/app/gyeol/layout.tsx new file mode 100644 index 0000000..12d040f --- /dev/null +++ b/app/gyeol/layout.tsx @@ -0,0 +1,36 @@ +import type { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'CONTOUR — 나를 더 선명하게 이해하는 3분', + description: '7 질문, 3분. 자기 이해·심리 영역 짧은 설문에 참여해주세요.', + openGraph: { + title: 'CONTOUR — 나를 더 선명하게 이해하는 3분', + description: '7 질문, 3분. 짧은 설문에 답해주세요.', + url: 'https://jaengseung-made.com/gyeol', + images: [ + { + url: 'https://jaengseung-made.com/og-image.png', + width: 1200, + height: 630, + alt: 'CONTOUR', + }, + ], + }, + robots: { + index: false, + follow: false, + }, +}; + +export default function GyeolLayout({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/app/gyeol/page.tsx b/app/gyeol/page.tsx new file mode 100644 index 0000000..e08e851 --- /dev/null +++ b/app/gyeol/page.tsx @@ -0,0 +1,164 @@ +'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('intro'); + const [response, setResponse] = useState>({}); + const [submitting, setSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(null); + const startedAtRef = useRef(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, nextStep: SurveyStep) { + setResponse((prev) => ({ ...prev, ...partial })); + setStep(nextStep); + } + + function goBack(prevStep: SurveyStep) { + setStep(prevStep); + } + + async function handleFinalSubmit(partial: Partial) { + 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' && } + {step === 'q1' && ( + setStep('intro')} + onNext={(p) => applyPartialAndAdvance(p, 'q2')} + /> + )} + {step === 'q2' && ( + goBack('q1')} + onNext={(p) => applyPartialAndAdvance(p, 'q3')} + /> + )} + {step === 'q3' && ( + goBack('q2')} + onNext={(p) => applyPartialAndAdvance(p, 'q4')} + /> + )} + {step === 'q4' && ( + goBack('q3')} + onNext={(p) => applyPartialAndAdvance(p, 'q5')} + /> + )} + {step === 'q5' && ( + goBack('q4')} + onNext={(p) => applyPartialAndAdvance(p, 'q6')} + /> + )} + {step === 'q6' && ( + goBack('q5')} + onNext={(p) => applyPartialAndAdvance(p, 'q7')} + /> + )} + {step === 'q7' && ( + goBack('q6')} + onSubmit={handleFinalSubmit} + submitting={submitting} + /> + )} + {step === 'thanks' && } + + {submitError && ( +
+ {submitError} +
+ )} + + ); +}