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}
+
+ )}
+ >
+ );
+}