From ae10bdc0b98d39be8d01b332080935d17c0b9594 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 16 May 2026 05:07:09 +0900 Subject: [PATCH] =?UTF-8?q?docs(plan):=20CONTOUR=20PMF=20=EC=84=A4?= =?UTF-8?q?=EB=AC=B8=20=EC=82=AC=EC=9D=B4=ED=8A=B8=20implementation=20plan?= =?UTF-8?q?=20=E2=80=94=2019=20task,=205=20phase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5 phase 구성: - A (4): supabase migration + lib types/questions/storage - B (8): UI 컴포넌트 — Intro/Q1-Q7/Thanks/ProgressBar/QuestionLayout - C (4): page+layout 통합 + /api/survey POST + standalone shell + /api/admin/survey - D (2): /admin/survey 대시보드 + AdminSidebar 메뉴 - E (1): build/lint/시각 회귀/CEO 운영 안내 (메모리 갱신 선택) 핵심 패턴: - 단일 페이지 + step state (URL 불변, localStorage 진행 저장) - /gyeol standalone (TopNav/푸터/카카오 모두 숨김) - DB RLS — anon INSERT만, admin SELECT - Resend 즉시 확인 메일 (이메일 입력 시만) - UTM·referrer 자동 수집 → 9 채널 CPM 분석 - 각 task 마지막에 git log -3 직접 검증 (Phase 2 sandbox 이슈 대비) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plans/2026-05-16-contour-pmf-survey.md | 2514 +++++++++++++++++ 1 file changed, 2514 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-16-contour-pmf-survey.md diff --git a/docs/superpowers/plans/2026-05-16-contour-pmf-survey.md b/docs/superpowers/plans/2026-05-16-contour-pmf-survey.md new file mode 100644 index 0000000..49a9e2d --- /dev/null +++ b/docs/superpowers/plans/2026-05-16-contour-pmf-survey.md @@ -0,0 +1,2514 @@ +# CONTOUR — PMF 인터뷰 설문 사이트 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** PMF 검증용 7-질문 설문 사이트 `/gyeol` 구축 + DB 저장 + Resend 즉시 확인 메일 + admin 대시보드(`/admin/survey`). 불특정 다수 익명 응답, 단일 페이지 + step state. + +**Architecture:** Next.js 16 App Router 클라이언트 컴포넌트로 단일 페이지 안에 step 9개(intro/q1-q7/thanks) state 관리. localStorage progress 저장. Supabase RLS — anon INSERT만, SELECT는 service role(admin) 전용. POST /api/survey가 보존 + Resend 확인 메일. Admin은 기존 admin HMAC 패턴. + +**Tech Stack:** Next.js 16 App Router + TS + Tailwind v4, Supabase Auth (`@supabase/ssr`), Resend(`resend` 패키지), `lib/security.ts` 의 sanitizeStr/isValidEmail/checkRateLimit/getClientIp 차용. + +**Spec:** `docs/superpowers/specs/2026-05-16-contour-pmf-survey-design.md` + +**⚠️ Subagent commit 주의**: Phase 2에서 일부 subagent의 commit이 sandboxing으로 git에 반영 안 되는 이슈 있었음. 모든 task 마지막 step에 `git log --oneline -3` 직접 확인 필수. HEAD가 본인 commit 아니면 BLOCKED 보고. + +--- + +## File Structure + +### 신규 생성 + +| 파일 | 책임 | +|---|---| +| `supabase/migrations/2026-05-16-create-survey-responses.sql` | DB 테이블 + RLS + index | +| `lib/survey/types.ts` | TypeScript types: SurveyStep, SurveyResponse | +| `lib/survey/questions.ts` | 7 질문 옵션 정의 (UI 데이터 SSOT) | +| `lib/survey/storage.ts` | localStorage progress save/restore | +| `app/gyeol/page.tsx` | 단일 페이지 (step state, 9 step 전환) | +| `app/gyeol/layout.tsx` | metadata + OG + robots: noindex | +| `app/gyeol/components/IntroStep.tsx` | step 'intro' — CONTOUR 로고 + 시작 버튼 | +| `app/gyeol/components/QuestionLayout.tsx` | 질문 단계 공통 wrapper (progress + 헤더 + 본문 + 이전/다음) | +| `app/gyeol/components/ProgressBar.tsx` | 진행률 (현재/7) | +| `app/gyeol/components/Q1Step.tsx` | Q1 식별 (드롭다운 2개) | +| `app/gyeol/components/Q2Step.tsx` | Q2 자각 빈도 (라디오 5) | +| `app/gyeol/components/Q3Step.tsx` | Q3 도구 사용 (멀티 체크 + 기타) | +| `app/gyeol/components/Q4Step.tsx` | Q4 비용 (라디오 6) | +| `app/gyeol/components/Q5Step.tsx` | Q5 만족도 (라디오 8 + 1-5점) | +| `app/gyeol/components/Q6Step.tsx` | Q6 자유 의견 (textarea) | +| `app/gyeol/components/Q7Step.tsx` | Q7 이메일 (옵션 + 입력) | +| `app/gyeol/components/ThanksStep.tsx` | step 'thanks' — 감사 메시지 | +| `app/api/survey/route.ts` | POST 응답 저장 + Resend 확인 메일 | +| `app/admin/survey/page.tsx` | admin 대시보드 (목록 + 카운트 + CSV) | +| `app/api/admin/survey/route.ts` | GET 목록(JSON) + CSV | + +### 수정 + +| 파일 | 변경 | +|---|---| +| `app/components/DashboardShell.tsx` | `STANDALONE_PATHS = ['/login', '/signup', '/admin']` → `[..., '/gyeol']` 1줄 추가 | +| `app/admin/components/AdminSidebar.tsx` | NAV_ITEMS 배열 끝에 "설문 응답" 추가 | + +--- + +## Task 순서 + 의존성 + +``` +Phase A — 기반 (4 task) + A1 (migration) → A2 (types) → A3 (questions) → A4 (storage) + +Phase B — UI 컴포넌트 (8 task) + B1 (ProgressBar) → B2 (QuestionLayout) + → B3 (IntroStep) + → B4 (Q1+Q2 묶음, 라디오/드롭다운 패턴 정착) + → B5 (Q3, 멀티 체크 + 기타) + → B6 (Q4+Q5 묶음, 라디오 패턴 재사용) + → B7 (Q6+Q7 묶음, textarea + 이메일 옵션) + → B8 (ThanksStep) + +Phase C — 통합 + API (4 task) + C1 (app/gyeol/page.tsx, layout.tsx — 단일 페이지 통합) + → C2 (/api/survey POST) + → C3 (DashboardShell standalone 추가) + → C4 (/api/admin/survey GET + CSV) + +Phase D — Admin UI (2 task) + D1 (/admin/survey/page.tsx) + → D2 (AdminSidebar 메뉴 추가) + +Phase E — 검증 (1 task) + E1 (build + lint + 시각 회귀 + CEO 운영 안내) +``` + +총 19 task. 각 task 작음 (10~80 LOC). UI 컴포넌트 묶음으로 task 압축. + +--- + +## Phase A — 기반 + +## Task A1: Supabase migration — survey_responses 테이블 + +**Files:** +- Create: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\supabase\migrations\2026-05-16-create-survey-responses.sql` + +- [ ] **Step 1: SQL 파일 작성** + +```sql +-- CONTOUR PMF 설문 응답 저장. +-- 불특정 다수 익명 응답: anon INSERT 허용, SELECT는 service role(admin)만. + +create table public.survey_responses ( + id uuid primary key default gen_random_uuid(), + created_at timestamptz not null default now(), + + -- Q1 식별 + age_range text, + status text, + + -- Q2 자각 빈도 + awareness_freq text, + + -- Q3 도구 사용 (멀티) + tools_used text[], + tools_other text, + + -- Q4 비용 + cost_range text, + + -- Q5 만족도 + best_tool text, + best_satisfy int, + + -- Q6 자유 의견 (핵심 자발 발화) + free_opinion text, + + -- Q7 이메일 (옵션) + email text, + email_confirmation_sent boolean default false, + + -- 메타 + user_agent text, + referrer text, + utm_source text, + utm_medium text, + utm_campaign text, + + -- 분석용 + completion_seconds int +); + +create index idx_survey_created on public.survey_responses(created_at desc); +create index idx_survey_email on public.survey_responses(email) where email is not null; + +-- RLS +alter table public.survey_responses enable row level security; + +-- anon insert 허용 (불특정 다수 응답 받기) +create policy "anon insert survey" on public.survey_responses + for insert to anon + with check (true); + +-- SELECT 정책 없음 → service role(admin)만 조회 가능 +``` + +- [ ] **Step 2: 적용은 운영자 수동 (CEO Supabase 콘솔 SQL Editor)** + +이 task는 SQL 파일 commit만. 실제 supabase 적용은 CEO 수동 단계 (E1에서 안내). + +- [ ] **Step 3: 커밋** + +```bash +git add supabase/migrations/2026-05-16-create-survey-responses.sql +git commit -m "$(cat <<'EOF' +feat(db): survey_responses 테이블 마이그레이션 — CONTOUR PMF 설문 + +- anon INSERT 허용 (불특정 다수 응답) +- SELECT 정책 없음 → service role(admin)만 조회 가능 +- index: created_at desc + email partial +- 메타: user_agent, referrer, utm_*, completion_seconds + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: ⚠️ git log -3 직접 확인** + +```bash +git log --oneline -3 +``` + +기대: HEAD = 본인 commit, 직전 = `82feb14` (CONTOUR spec). + +--- + +## Task A2: lib/survey/types.ts + +**Files:** +- Create: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\lib\survey\types.ts` + +- [ ] **Step 1: 작성** + +```ts +/** + * CONTOUR 설문 타입. + * survey_responses 테이블 스키마와 1:1 대응. + */ + +export type SurveyStep = + | 'intro' + | 'q1' + | 'q2' + | 'q3' + | 'q4' + | 'q5' + | 'q6' + | 'q7' + | 'thanks'; + +export const QUESTION_STEPS: SurveyStep[] = ['q1', 'q2', 'q3', 'q4', 'q5', 'q6', 'q7']; +export const TOTAL_QUESTIONS = QUESTION_STEPS.length; // 7 + +export interface SurveyResponse { + // Q1 + age_range?: string; + status?: string; + // Q2 + awareness_freq?: string; + // Q3 + tools_used?: string[]; + tools_other?: string; + // Q4 + cost_range?: string; + // Q5 + best_tool?: string; + best_satisfy?: number; + // Q6 + free_opinion?: string; + // Q7 + email?: string; + // 메타 (제출 시 자동 채워짐) + user_agent?: string; + referrer?: string; + utm_source?: string; + utm_medium?: string; + utm_campaign?: string; + completion_seconds?: number; +} + +export interface SavedProgress { + step: SurveyStep; + response: SurveyResponse; + startedAt: number; // ms epoch +} +``` + +- [ ] **Step 2: 린트** + +```bash +npx eslint lib/survey/types.ts +``` +Expected: exit 0. + +- [ ] **Step 3: 커밋** + +```bash +git add lib/survey/types.ts +git commit -m "$(cat <<'EOF' +feat(survey): lib/survey/types — SurveyStep, SurveyResponse, SavedProgress + +7 질문 step 정의 + 응답 객체 타입. survey_responses 테이블과 1:1 대응. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: ⚠️ git log -3 직접 확인** + +--- + +## Task A3: lib/survey/questions.ts — 7 질문 옵션 정의 + +**Files:** +- Create: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\lib\survey\questions.ts` + +- [ ] **Step 1: 작성** + +```ts +/** + * CONTOUR 설문 7 질문 옵션 SSOT. + * UI 컴포넌트(Q1Step ~ Q7Step)가 이 데이터를 import. + * spec markdown의 7 질문과 1:1 대응. + */ + +export const AGE_RANGES = ['10대', '20대', '30대', '40대', '50대+'] as const; + +export const STATUSES = [ + '직장인', + '학생', + '자영업', + '프리랜서', + '취업준비', + '휴식 중', + '기타', +] as const; + +export const AWARENESS_FREQS = [ + '거의 매일 그래요', + '자주 그래요', + '가끔 그래요', + '별로 없어요', + '한 번도 없어요', +] as const; + +export const TOOLS_OPTIONS = [ + '사주 / 타로', + 'MBTI / 성격 검사', + '심리 상담', + '자기계발 책 / 강의', + '친구·가족과 대화', + '일기 / 글쓰기', + '검색 / 유튜브', + '그냥 시간이 풀어줌', + '아무것도 안 함', +] as const; + +export const COST_RANGES = [ + '0원', + '1만원 이하', + '1~5만원', + '5~10만원', + '10~30만원', + '30만원 이상', +] as const; + +export const BEST_TOOLS = [ + '사주 / 타로', + 'MBTI', + '심리 상담', + '책 / 강의', + '대화', + '일기 / 글쓰기', + '시간', + '도움 된 게 없음', +] as const; + +export const SATISFY_SCALE = [1, 2, 3, 4, 5] as const; + +// 질문 헤더 카피 +export const QUESTION_HEADERS: Record = { + q1: { + title: '안녕하세요. 짧게 자기 소개부터.', + subtitle: '나이대와 지금 상황을 알려주세요.', + }, + q2: { + title: "최근 1년 안에 '내가 뭘 원하는지 모르겠다'고 느낀 적 있어요?", + }, + q3: { + title: '그럴 때 어떻게 풀어가시나요?', + subtitle: '해본 거 모두 골라주세요. (복수 선택)', + }, + q4: { + title: '지난 1년간 자기 이해·심리 영역에 돈 쓴 거 다 합쳐서 얼마쯤?', + }, + q5: { + title: '그중 가장 도움 됐던 거 + 만족도', + }, + q6: { + title: "혹시 '내가 진짜 알고 싶었던 건 이런 거였는데...' 하는 게 있나요?", + subtitle: '자유롭게 적어주세요. 안 적으셔도 괜찮아요.', + }, + q7: { + title: '이런 도구가 나오면 알려드릴까요?', + subtitle: '결과를 받고 싶으시면 이메일을 남겨주세요.', + }, +}; +``` + +- [ ] **Step 2: 린트** + +```bash +npx eslint lib/survey/questions.ts +``` + +- [ ] **Step 3: 커밋** + +```bash +git add lib/survey/questions.ts +git commit -m "$(cat <<'EOF' +feat(survey): lib/survey/questions — 7 질문 옵션 SSOT + +각 질문의 라디오/체크/드롭다운 옵션 배열 + 헤더 카피. +spec markdown의 7 질문 그대로 반영 (단어 '결' 등 한글 컨셉어 제거 — CONTOUR 영문 단독). + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: ⚠️ git log -3** + +--- + +## Task A4: lib/survey/storage.ts — localStorage helpers + +**Files:** +- Create: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\lib\survey\storage.ts` + +- [ ] **Step 1: 작성** + +```ts +import type { SavedProgress } from './types'; + +const STORAGE_KEY = 'gyeol_survey_progress_v1'; + +/** + * Progress 저장. 새로고침 후 복구용. + * SSR 환경에서는 noop. + */ +export function saveProgress(progress: SavedProgress): void { + if (typeof window === 'undefined') return; + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(progress)); + } catch { + // storage quota exceeded 등 — 무시 (응답 자체에 영향 X) + } +} + +/** + * Progress 복구. 없거나 파싱 실패 시 null. + */ +export function loadProgress(): SavedProgress | null { + if (typeof window === 'undefined') return null; + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return null; + return JSON.parse(raw) as SavedProgress; + } catch { + return null; + } +} + +/** + * 제출 성공 시 progress 삭제. + */ +export function clearProgress(): void { + if (typeof window === 'undefined') return; + try { + localStorage.removeItem(STORAGE_KEY); + } catch { + /* noop */ + } +} +``` + +- [ ] **Step 2: 린트** + +```bash +npx eslint lib/survey/storage.ts +``` + +- [ ] **Step 3: 커밋** + +```bash +git add lib/survey/storage.ts +git commit -m "$(cat <<'EOF' +feat(survey): lib/survey/storage — localStorage progress save/load/clear + +새로고침 시 step + response 복구. SSR safe (window 체크). + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: ⚠️ git log -3** + +--- + +# Phase B — UI 컴포넌트 (8 task) + +## Task B1: ProgressBar 컴포넌트 + +**Files:** +- Create: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\app\gyeol\components\ProgressBar.tsx` + +- [ ] **Step 1: 작성** + +```tsx +import { QUESTION_STEPS, TOTAL_QUESTIONS, type SurveyStep } from '@/lib/survey/types'; + +interface Props { + step: SurveyStep; +} + +/** + * 상단 진행률 바. + * intro/thanks에서는 렌더링 안 됨 (질문 step 만 표시). + */ +export default function ProgressBar({ step }: Props) { + const idx = QUESTION_STEPS.indexOf(step as 'q1'); + if (idx < 0) return null; + + const current = idx + 1; + const percent = (current / TOTAL_QUESTIONS) * 100; + + return ( +
+
+ {current}/{TOTAL_QUESTIONS} +
+
+
+
+
+ ); +} +``` + +- [ ] **Step 2: 린트** + +```bash +npx eslint app/gyeol/components/ProgressBar.tsx +``` + +- [ ] **Step 3: 커밋** + +```bash +git add app/gyeol/components/ProgressBar.tsx +git commit -m "$(cat <<'EOF' +feat(gyeol): ProgressBar — 진행률 (보라/시안 그라데이션 라인) + +intro/thanks step에서는 미렌더. q1~q7만 표시. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: ⚠️ git log -3** + +--- + +## Task B2: QuestionLayout 공통 wrapper + +**Files:** +- Create: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\app\gyeol\components\QuestionLayout.tsx` + +- [ ] **Step 1: 작성** + +```tsx +'use client'; + +import type { ReactNode } from 'react'; +import ProgressBar from './ProgressBar'; +import type { SurveyStep } from '@/lib/survey/types'; +import { QUESTION_HEADERS } from '@/lib/survey/questions'; + +interface Props { + step: SurveyStep; + children: ReactNode; // 본문 (옵션 입력 컴포넌트) + onPrev?: () => void; // 이전 (없으면 미렌더) + onNext: () => void; // 다음 + nextLabel?: string; // 기본 '다음' + nextDisabled?: boolean; + submitting?: boolean; // Q7 전송 중 표시 +} + +export default function QuestionLayout({ + step, + children, + onPrev, + onNext, + nextLabel = '다음', + nextDisabled = false, + submitting = false, +}: Props) { + const header = QUESTION_HEADERS[step]; + + return ( +
+ + +
+ {/* 질문 헤더 */} +
+

+ {header?.title} +

+ {header?.subtitle && ( +

{header.subtitle}

+ )} +
+ + {/* 본문 */} +
{children}
+ + {/* 네비게이션 */} +
+ {onPrev && ( + + )} + +
+
+
+ ); +} +``` + +- [ ] **Step 2: 린트** + +```bash +npx eslint app/gyeol/components/QuestionLayout.tsx +``` + +- [ ] **Step 3: 커밋** + +```bash +git add app/gyeol/components/QuestionLayout.tsx +git commit -m "$(cat <<'EOF' +feat(gyeol): QuestionLayout — 질문 단계 공통 wrapper + +ProgressBar + 헤더 + 본문 slot + 이전/다음 네비게이션. +nextDisabled로 validation 제어. submitting 시 버튼 비활성. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: ⚠️ git log -3** + +--- + +## Task B3: IntroStep + +**Files:** +- Create: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\app\gyeol\components\IntroStep.tsx` + +- [ ] **Step 1: 작성** + +```tsx +'use client'; + +interface Props { + onStart: () => void; +} + +/** + * 인트로 step — CONTOUR 로고 + 한글 부제 + 시작 버튼. + * spec design PNG 1번째 화면 참조. + */ +export default function IntroStep({ onStart }: Props) { + return ( +
+ {/* 로고 */} +
+

+ CONTOUR +

+

+ 나를 더 선명하게 이해하는 3분 +

+
+ + {/* 시작 버튼 */} + + +

7 질문 · 약 3분

+
+ ); +} +``` + +- [ ] **Step 2: 린트** + +```bash +npx eslint app/gyeol/components/IntroStep.tsx +``` + +- [ ] **Step 3: 커밋** + +```bash +git add app/gyeol/components/IntroStep.tsx +git commit -m "$(cat <<'EOF' +feat(gyeol): IntroStep — CONTOUR 로고 그라데이션 + 부제 + 시작 버튼 + +영문 단독 브랜드, 한글 부제 "나를 더 선명하게 이해하는 3분". +보라/시안 그라데이션 텍스트, 7 질문 안내. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: ⚠️ git log -3** + +--- + +## Task B4: Q1Step + Q2Step (드롭다운 + 라디오 패턴 정착) + +**Files:** +- Create: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\app\gyeol\components\Q1Step.tsx` +- Create: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\app\gyeol\components\Q2Step.tsx` + +- [ ] **Step 1: Q1Step (드롭다운 2개)** + +```tsx +'use client'; + +import { useState } from 'react'; +import { AGE_RANGES, STATUSES } from '@/lib/survey/questions'; +import type { SurveyResponse } from '@/lib/survey/types'; +import QuestionLayout from './QuestionLayout'; + +interface Props { + initial: Partial; + onPrev: () => void; + onNext: (partial: Partial) => void; +} + +export default function Q1Step({ initial, onPrev, onNext }: Props) { + const [age, setAge] = useState(initial.age_range ?? ''); + const [status, setStatus] = useState(initial.status ?? ''); + + const valid = age && status; + + return ( + onNext({ age_range: age, status })} + nextDisabled={!valid} + > +
+
+ + +
+ +
+ + +
+
+
+ ); +} +``` + +- [ ] **Step 2: Q2Step (라디오 5개)** + +```tsx +'use client'; + +import { useState } from 'react'; +import { AWARENESS_FREQS } from '@/lib/survey/questions'; +import type { SurveyResponse } from '@/lib/survey/types'; +import QuestionLayout from './QuestionLayout'; + +interface Props { + initial: Partial; + onPrev: () => void; + onNext: (partial: Partial) => void; +} + +export default function Q2Step({ initial, onPrev, onNext }: Props) { + const [value, setValue] = useState(initial.awareness_freq ?? ''); + + return ( + onNext({ awareness_freq: value })} + nextDisabled={!value} + > +
+ {AWARENESS_FREQS.map((option) => ( + + ))} +
+
+ ); +} +``` + +- [ ] **Step 3: 린트** + +```bash +npx eslint app/gyeol/components/Q1Step.tsx app/gyeol/components/Q2Step.tsx +``` + +- [ ] **Step 4: 커밋** + +```bash +git add app/gyeol/components/Q1Step.tsx app/gyeol/components/Q2Step.tsx +git commit -m "$(cat <<'EOF' +feat(gyeol): Q1Step (드롭다운) + Q2Step (라디오 5) + +- Q1: 나이대 + 상황 두 드롭다운, 둘 다 선택 시 활성 +- Q2: 자각 빈도 5 라디오, 보라 활성 스타일 +- 라디오 패턴이 이후 Q4/Q5에서 재사용됨 + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 5: ⚠️ git log -3** + +--- + +## Task B5: Q3Step (멀티 체크 + 기타 입력) + +**Files:** +- Create: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\app\gyeol\components\Q3Step.tsx` + +- [ ] **Step 1: 작성** + +```tsx +'use client'; + +import { useState } from 'react'; +import { TOOLS_OPTIONS } from '@/lib/survey/questions'; +import type { SurveyResponse } from '@/lib/survey/types'; +import QuestionLayout from './QuestionLayout'; + +interface Props { + initial: Partial; + onPrev: () => void; + onNext: (partial: Partial) => void; +} + +export default function Q3Step({ initial, onPrev, onNext }: Props) { + const [selected, setSelected] = useState(initial.tools_used ?? []); + const [other, setOther] = useState(initial.tools_other ?? ''); + + function toggle(option: string) { + setSelected((prev) => + prev.includes(option) ? prev.filter((x) => x !== option) : [...prev, option] + ); + } + + // validation: 최소 1개 체크 또는 기타 입력 있음 + const valid = selected.length > 0 || other.trim().length > 0; + + return ( + + onNext({ + tools_used: selected, + tools_other: other.trim() || undefined, + }) + } + nextDisabled={!valid} + > +
+ {TOOLS_OPTIONS.map((option) => { + const checked = selected.includes(option); + return ( + + ); + })} + +
+ + setOther(e.target.value)} + placeholder="예: 명상, 운동" + maxLength={100} + className="w-full px-4 py-3 rounded-xl bg-white/[0.04] border border-white/15 text-white placeholder:text-white/30 focus:border-white/40 focus:outline-none" + /> +
+
+
+ ); +} +``` + +- [ ] **Step 2: 린트** + +```bash +npx eslint app/gyeol/components/Q3Step.tsx +``` + +- [ ] **Step 3: 커밋** + +```bash +git add app/gyeol/components/Q3Step.tsx +git commit -m "$(cat <<'EOF' +feat(gyeol): Q3Step — 멀티 체크 9개 + 기타 자유 입력 + +validation: 최소 1개 체크 또는 기타 입력 있어야 다음 활성. +체크박스 패턴 + 활성 시 보라 + 흰 체크마크. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: ⚠️ git log -3** + +--- + +## Task B6: Q4Step + Q5Step (라디오 패턴 재사용) + +**Files:** +- Create: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\app\gyeol\components\Q4Step.tsx` +- Create: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\app\gyeol\components\Q5Step.tsx` + +- [ ] **Step 1: Q4Step (비용 라디오 6개)** + +```tsx +'use client'; + +import { useState } from 'react'; +import { COST_RANGES } from '@/lib/survey/questions'; +import type { SurveyResponse } from '@/lib/survey/types'; +import QuestionLayout from './QuestionLayout'; + +interface Props { + initial: Partial; + onPrev: () => void; + onNext: (partial: Partial) => void; +} + +export default function Q4Step({ initial, onPrev, onNext }: Props) { + const [value, setValue] = useState(initial.cost_range ?? ''); + + return ( + onNext({ cost_range: value })} + nextDisabled={!value} + > +
+ {COST_RANGES.map((option) => ( + + ))} +
+
+ ); +} +``` + +- [ ] **Step 2: Q5Step (최고 도구 + 만족도 1-5)** + +```tsx +'use client'; + +import { useState } from 'react'; +import { BEST_TOOLS, SATISFY_SCALE } from '@/lib/survey/questions'; +import type { SurveyResponse } from '@/lib/survey/types'; +import QuestionLayout from './QuestionLayout'; + +interface Props { + initial: Partial; + onPrev: () => void; + onNext: (partial: Partial) => void; +} + +export default function Q5Step({ initial, onPrev, onNext }: Props) { + const [tool, setTool] = useState(initial.best_tool ?? ''); + const [satisfy, setSatisfy] = useState(initial.best_satisfy ?? null); + + const valid = tool && satisfy !== null; + + return ( + onNext({ best_tool: tool, best_satisfy: satisfy ?? undefined })} + nextDisabled={!valid} + > +
+
+

가장 도움 됐던 거

+
+ {BEST_TOOLS.map((option) => ( + + ))} +
+
+ +
+

만족도 (5점 만점)

+
+ {SATISFY_SCALE.map((n) => ( + + ))} +
+
+
+
+ ); +} +``` + +- [ ] **Step 3: 린트** + +```bash +npx eslint app/gyeol/components/Q4Step.tsx app/gyeol/components/Q5Step.tsx +``` + +- [ ] **Step 4: 커밋** + +```bash +git add app/gyeol/components/Q4Step.tsx app/gyeol/components/Q5Step.tsx +git commit -m "$(cat <<'EOF' +feat(gyeol): Q4Step (비용 라디오 6) + Q5Step (도구 라디오 8 + 만족도 1-5) + +- Q4: 라디오 패턴 재사용 (Q2와 동일 스타일) +- Q5: 두 입력 한 화면 — 도구 라디오 + 만족도 1-5 버튼 그리드 +- 둘 다 선택 시 다음 활성 + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 5: ⚠️ git log -3** + +--- + +## Task B7: Q6Step + Q7Step (textarea + 이메일 옵션) + +**Files:** +- Create: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\app\gyeol\components\Q6Step.tsx` +- Create: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\app\gyeol\components\Q7Step.tsx` + +- [ ] **Step 1: Q6Step (자유 의견 textarea)** + +```tsx +'use client'; + +import { useState } from 'react'; +import type { SurveyResponse } from '@/lib/survey/types'; +import QuestionLayout from './QuestionLayout'; + +interface Props { + initial: Partial; + onPrev: () => void; + onNext: (partial: Partial) => void; +} + +export default function Q6Step({ initial, onPrev, onNext }: Props) { + const [text, setText] = useState(initial.free_opinion ?? ''); + + // 빈 칸 허용 (skippable) + return ( + onNext({ free_opinion: text.trim() || undefined })} + > +