From 91e4f04af0316024891c05cbf8bdfbd22b154ffb Mon Sep 17 00:00:00 2001 From: yolophg Date: Tue, 3 Nov 2026 16:41:03 -0500 Subject: [PATCH 1/5] feat: polish survey UX with required marks, analyzing step, and fortune ticket --- questions/2026.yaml | 2 +- src/components/DevTypeCard.tsx | 69 ++++++++++++++++++++++++++++++--- src/components/QuestionStep.tsx | 16 +++++--- src/components/ShareButton.tsx | 3 +- src/components/StarBurst.tsx | 60 ++++++++++++++++++++++++++++ src/lib/devType.ts | 28 +++++++++---- src/routes/$edition.survey.tsx | 65 +++++++++++++++++++++++-------- 7 files changed, 206 insertions(+), 37 deletions(-) create mode 100644 src/components/StarBurst.tsx diff --git a/questions/2026.yaml b/questions/2026.yaml index 052cb47..915eb06 100644 --- a/questions/2026.yaml +++ b/questions/2026.yaml @@ -10,7 +10,7 @@ status: draft sections: - id: about - title: 나에 대해 + title: 기본 정보 questions: - id: status fixed: true diff --git a/src/components/DevTypeCard.tsx b/src/components/DevTypeCard.tsx index 413b16b..524cb75 100644 --- a/src/components/DevTypeCard.tsx +++ b/src/components/DevTypeCard.tsx @@ -1,4 +1,4 @@ -import { Heading, Text } from 'daleui' +import { Heading, Icon, Text } from 'daleui' import type { DevType } from '@/lib/devType' export function DevTypeCard({ type, label }: { type: DevType; label: string }) { @@ -32,21 +32,78 @@ export function DevTypeCard({ type, label }: { type: DevType; label: string }) { {type.description} + {/* 운세 쪽지: 양옆에 홈이 파인 티켓 모양 */}
- - 2027년 커리어 한 줄 +
+
+ + + 2027 개발 운세 + +
+ + No.2027 + +
+ + {type.fortune} - {type.fortune} +
+ {/* 절취선은 양옆 홈 사이에만 */} + + {(['left', 'right'] as const).map((side) => ( + + ))} +
+
+ {[ + ['귀인', type.ally], + ['조심', type.caution], + ].map(([k, v]) => ( +
+
+ + {k} + +
+
+ {v} +
+
+ ))} +
diff --git a/src/components/QuestionStep.tsx b/src/components/QuestionStep.tsx index dcf1056..80ce720 100644 --- a/src/components/QuestionStep.tsx +++ b/src/components/QuestionStep.tsx @@ -18,12 +18,18 @@ export function QuestionStep({ question: q, value, error, onPick, onToggle, onTe return (
-
- {q.section} - {!q.required && 건너뛰어도 돼요} -
+ {q.section} {q.label} + {q.required ? ( + + {' *'} + + ) : ( + + {' 선택'} + + )} {q.help && ( @@ -78,7 +84,7 @@ export function QuestionStep({ question: q, value, error, onPick, onToggle, onTe {q.type === 'multi' && ( - {picked.length}개 골랐어요. {q.limit ? `최대 ${q.limit}개까지 고를 수 있어요.` : '여러 개 골라도 돼요.'} + {picked.length}개 골랐어요 )} diff --git a/src/components/ShareButton.tsx b/src/components/ShareButton.tsx index 6a41535..710b6ef 100644 --- a/src/components/ShareButton.tsx +++ b/src/components/ShareButton.tsx @@ -37,7 +37,8 @@ export function ShareButton({ type, edition }: { type: DevType; edition: { id: s }, []) async function copy() { - await navigator.clipboard.writeText(`${text}\n${url}`) + // 주소만 복사 + await navigator.clipboard.writeText(url) setCopied(true) } diff --git a/src/components/StarBurst.tsx b/src/components/StarBurst.tsx new file mode 100644 index 0000000..a4b89f2 --- /dev/null +++ b/src/components/StarBurst.tsx @@ -0,0 +1,60 @@ +import { Icon } from 'daleui' +import { useState } from 'react' + +// 유형 결과가 처음 나올 때 한 번 터지는 별. 모션 줄이기 설정에서는 숨긴다. +const COUNT = 28 +const TONES = ['brand', 'warning', 'info'] as const + +export function StarBurst() { + // 제출한 뒤 브라우저에서만 그려지므로 Math.random 이 하이드레이션과 어긋나지 않는다 + const [stars] = useState(() => + Array.from({ length: COUNT }, (_, i) => { + const angle = (i / COUNT) * Math.PI * 2 + Math.random() * 0.4 + const dist = 140 + Math.random() * 220 + return { + x: Math.cos(angle) * dist, + y: Math.sin(angle) * dist * 0.7 - 60, + r: (Math.random() - 0.5) * 540, + delay: Math.random() * 150, + tone: TONES[i % TONES.length], + size: Math.random() < 0.3 ? ('md' as const) : ('sm' as const), + } + }), + ) + return ( +
+ + {stars.map((s, i) => ( + + + + ))} +
+ ) +} diff --git a/src/lib/devType.ts b/src/lib/devType.ts index b222a07..8f85c35 100644 --- a/src/lib/devType.ts +++ b/src/lib/devType.ts @@ -6,8 +6,10 @@ export interface DevType { slug: string name: string description: string - // 에반의 "내년 개발 커리어 운세" + // 에반의 "내년 개발 커리어 운세". 신년운세처럼 총운(fortune)에 귀인(ally)과 조심할 것(caution)을 붙인다. fortune: string + ally: string + caution: string } export const DEV_TYPES: DevType[] = [ @@ -15,37 +17,49 @@ export const DEV_TYPES: DevType[] = [ slug: 'ai-explorer', name: 'AI 네이티브 탐험가', description: 'AI를 매일 쓰면서, 새로운 걸 배우는 데 가장 큰 가치를 둬요.', - fortune: '내년엔 에이전트에게 일을 맡기는 법을 가르치고 있을지도 몰라요.', + fortune: '에이전트에게 일을 맡기는 법을 가르치게 될 운이에요.', + ally: '프롬프트를 같이 다듬어 주는 동료', + caution: '안 읽고 머지한 AI 코드', }, { slug: 'ai-pragmatist', name: 'AI 실속파', description: 'AI를 매일 쓰지만, 결국 중요한 건 일이 잘 굴러가는 거라고 믿어요.', - fortune: '내년엔 아낀 시간만큼 퇴근이 조금 빨라질 거예요.', + fortune: '아낀 시간만큼 퇴근이 빨라질 운이에요.', + ally: '자동화할 거리를 물어 오는 동료', + caution: '월말에 날아오는 AI 구독료', }, { slug: 'careful-experimenter', name: '신중한 실험가', description: 'AI를 가끔 꺼내 써 보며, 어디까지 믿어도 될지 직접 확인하는 중이에요.', - fortune: '내년엔 나만의 AI 활용법 하나가 손에 붙을 거예요.', + fortune: '나만의 AI 활용법 하나가 손에 붙을 운이에요.', + ally: '써 본 도구 후기를 솔직하게 들려주는 동료', + caution: '써 보기도 전에 쌓이는 북마크', }, { slug: 'balanced-realist', name: '균형 잡힌 실용주의자', description: '새 도구는 필요할 때만 써요. 일과 삶의 균형을 지키며 꾸준히 가요.', - fortune: '내년엔 무리하지 않고도 한 단계 올라서 있을 거예요.', + fortune: '무리하지 않고도 한 단계 올라설 운이에요.', + ally: '칼퇴를 응원해 주는 팀장님', + caution: '"이것만 하고 퇴근"이라는 생각', }, { slug: 'handcraft-artisan', name: '손코딩 장인', description: 'AI 없이 직접 부딪히며 실력을 쌓는 걸 즐겨요.', - fortune: '내년엔 그 기본기가 남들과 다른 무기가 될 거예요.', + fortune: '다져 온 기본기가 남다른 무기가 될 운이에요.', + ally: '리뷰를 꼼꼼히 해 주는 사수', + caution: '바퀴를 직접 다시 만들고 싶은 마음', }, { slug: 'steady-veteran', name: '묵묵한 베테랑', description: '유행보다 검증된 방법을 믿고, 맡은 일을 끝까지 해내요.', - fortune: '내년엔 그 꾸준함을 알아보는 사람이 늘어날 거예요.', + fortune: '그 꾸준함을 알아보는 사람이 늘어날 운이에요.', + ally: '회고 때 내 이름을 꺼내 주는 동료', + caution: '금요일 오후 배포', }, ] diff --git a/src/routes/$edition.survey.tsx b/src/routes/$edition.survey.tsx index f5a1e5a..d8524df 100644 --- a/src/routes/$edition.survey.tsx +++ b/src/routes/$edition.survey.tsx @@ -8,6 +8,7 @@ import { DevTypeCard } from '@/components/DevTypeCard' import { SiteFooter } from '@/components/SiteFooter' import { SiteHeader } from '@/components/SiteHeader' import { ShareButton } from '@/components/ShareButton' +import { StarBurst } from '@/components/StarBurst' import { type DevType, devTypeOf, findDevType } from '@/lib/devType' import { findEdition, isBeta, isClosed } from '@/editions' import { koDate } from '@/lib/date' @@ -68,6 +69,41 @@ const quietAction: React.CSSProperties = { cursor: 'pointer', } +// 제출하는 동안 보여 주는 분석 화면. 유형을 정하는 두 문항(ai_frequency, company_criteria)을 짚어서 +// 결과가 내 답에서 나온다는 걸 보여 준다. 제출 대기와 겹치므로 실제로 늘어나는 시간은 짧다. +const ANALYSIS_STEPS = ['AI 사용 습관 읽는 중', '커리어 우선순위 확인 중', '2027 운세 뽑는 중'] +const ANALYSIS_STEP_MS = 900 +const ANALYSIS_MIN_MS = ANALYSIS_STEPS.length * ANALYSIS_STEP_MS + 300 + +function Analyzing() { + const [done, setDone] = useState(0) + useEffect(() => { + const t = setInterval(() => setDone((n) => Math.min(n + 1, ANALYSIS_STEPS.length)), ANALYSIS_STEP_MS) + return () => clearInterval(t) + }, []) + return ( +
+
+ 두구두구… +
    + {ANALYSIS_STEPS.map((step, i) => ( +
  • + {/* 끝난 단계에만 체크. 자리는 비워 둬서 글자가 움직이지 않게 한다 */} + + + + {i === done ? `${step}…` : step} +
  • + ))} +
+
+
+ ) +} + // 응답을 마쳤거나 마감된 화면. 설문 진행 중이 아니라서 다른 화면으로 갈 수 있게 헤더와 하단을 둔다. function EndPage({ children }: { children: React.ReactNode }) { return ( @@ -96,7 +132,7 @@ function Finished({ saved, title, lead, devType, edition, onRetake }: {
- 응답을 저장했어요 + 익명으로 저장했어요
)} @@ -107,6 +143,8 @@ function Finished({ saved, title, lead, devType, edition, onRetake }: { {devType && ( <> + {/* 방금 응답을 마쳤을 때만. "이미 답했어요" 화면에서는 터뜨리지 않는다 */} + {saved && } @@ -181,7 +219,11 @@ function Survey() { } setPhase('sending') try { - await submitResponse({ data: { edition: edition.id, answers: final, ref, consented: true, turnstileToken } }) + await Promise.all([ + submitResponse({ data: { edition: edition.id, answers: final, ref, consented: true, turnstileToken } }), + // 분석 화면이 바로 사라지지 않게 최소 시간을 기다린다 + edition.devTypes && new Promise((r) => setTimeout(r, ANALYSIS_MIN_MS)), + ]) const type = edition.devTypes ? devTypeOf(final) : undefined // 다시 들어와도 유형을 보여 줄 수 있게 유형 이름만 남긴다 localStorage.setItem(DONE_KEY, type?.slug ?? 'done') @@ -225,8 +267,8 @@ function Survey() { return ( + const value = answers[current.id] const last = pos === visible.length - 1 const progress = Math.round((pos / visible.length) * 100) @@ -344,19 +388,6 @@ function Survey() { > {last ? '제출하기' : '다음'} - {!current.required && ( - - )}
From 47edd882768798a9cbd47cfe4e094c141b8a9166 Mon Sep 17 00:00:00 2001 From: yolophg Date: Wed, 4 Nov 2026 08:26:27 -0500 Subject: [PATCH 2/5] feat: preview dev type and fortune on survey intro and simplify copy --- src/routes/$edition.index.tsx | 2 +- src/routes/$edition.survey.tsx | 23 +++++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/routes/$edition.index.tsx b/src/routes/$edition.index.tsx index 17b8fb7..f20dec1 100644 --- a/src/routes/$edition.index.tsx +++ b/src/routes/$edition.index.tsx @@ -214,7 +214,7 @@ function EditionPage() { 끝까지 답하면 나의 개발자 유형을 알려 드려요 - 여섯 가지 유형 가운데 하나와 함께, 2027년 커리어를 내다보는 한 줄도 받아 볼 수 있어요. 친구에게 공유해 보세요. + 여섯 가지 유형 가운데 하나와 함께, 2027 커리어 운세도 받아 볼 수 있어요. 친구에게 공유해 보세요!