diff --git a/app/admin/survey/page.tsx b/app/admin/survey/page.tsx new file mode 100644 index 0000000..49fab39 --- /dev/null +++ b/app/admin/survey/page.tsx @@ -0,0 +1,221 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +interface SurveyRow { + id: string; + created_at: string; + age_range: string | null; + status: string | null; + awareness_freq: string | null; + tools_used: string[] | null; + tools_other: string | null; + cost_range: string | null; + best_tool: string | null; + best_satisfy: number | null; + free_opinion: string | null; + email: string | null; + user_agent: string | null; + referrer: string | null; + utm_source: string | null; + completion_seconds: number | null; +} + +interface Stats { + age_range: Record; + status: Record; + awareness_freq: Record; + cost_range: Record; + best_tool: Record; + satisfy_avg: string; + email_rate: string; + completion_seconds_median: number; +} + +type Range = 'all' | 'today' | 'week'; + +export default function AdminSurveyPage() { + const [range, setRange] = useState('all'); + const [total, setTotal] = useState(0); + const [stats, setStats] = useState(null); + const [rows, setRows] = useState([]); + const [loading, setLoading] = useState(true); + const [selected, setSelected] = useState(null); + + useEffect(() => { + async function load(r: Range) { + setLoading(true); + try { + const res = await fetch(`/api/admin/survey?range=${r}`); + const data = await res.json(); + setTotal(data.total ?? 0); + setStats(data.stats ?? null); + setRows(data.responses ?? []); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + } + load(range); + }, [range]); + + function downloadCsv() { + window.location.href = `/api/admin/survey?range=${range}&format=csv`; + } + + function fmtCount(counts: Record | undefined): string { + if (!counts) return ''; + return Object.entries(counts) + .sort((a, b) => b[1] - a[1]) + .map(([k, v]) => `${k} ${v}`) + .join(' · '); + } + + return ( +
+
+
+

설문 응답

+

+ CONTOUR PMF 설문 — 총 {total}건 +

+
+
+ {(['all', 'today', 'week'] as Range[]).map((r) => ( + + ))} + +
+
+ + {/* 통계 카드 */} + {stats && ( +
+
+

Q2 자각 빈도

+

{fmtCount(stats.awareness_freq) || '데이터 없음'}

+
+
+

Q4 비용

+

{fmtCount(stats.cost_range) || '데이터 없음'}

+
+
+

Q5 만족도 평균

+

{stats.satisfy_avg} / 5

+
+
+

Q7 이메일률 / 완료 시간 (중간값)

+

+ {stats.email_rate}% · {stats.completion_seconds_median}s +

+
+
+ )} + + {/* 응답 리스트 */} + {loading ? ( +

불러오는 중...

+ ) : rows.length === 0 ? ( +

응답이 없습니다.

+ ) : ( +
+ + + + + + + + + + + + + + {rows.map((r) => ( + + + + + + + + + + ))} + +
시각나이/상황Q4 비용Q5 만족Q6 자유의견 (미리보기)이메일
{new Date(r.created_at).toLocaleString('ko-KR')}{r.age_range} · {r.status}{r.cost_range ?? '-'}{r.best_satisfy ?? '-'} + {r.free_opinion ?? } + {r.email ?? '-'} + +
+
+ )} + + {/* 상세 modal */} + {selected && ( +
setSelected(null)} + > +
e.stopPropagation()} + > +
+
+

응답 상세

+

{new Date(selected.created_at).toLocaleString('ko-KR')}

+
+ +
+
+ {[ + ['Q1 나이대', selected.age_range], + ['Q1 상황', selected.status], + ['Q2 자각 빈도', selected.awareness_freq], + ['Q3 도구', selected.tools_used?.join(', ')], + ['Q3 기타', selected.tools_other], + ['Q4 비용', selected.cost_range], + ['Q5 최고 도구', selected.best_tool], + ['Q5 만족도', selected.best_satisfy != null ? `${selected.best_satisfy} / 5` : null], + ['Q6 자유 의견', selected.free_opinion], + ['Q7 이메일', selected.email], + ['user_agent', selected.user_agent], + ['referrer', selected.referrer], + ['utm_source', selected.utm_source], + ['완료 시간', selected.completion_seconds != null ? `${selected.completion_seconds}초` : null], + ].map(([k, v]) => ( +
+
{k}
+
{(v as string) || }
+
+ ))} +
+
+
+ )} +
+ ); +}