diff --git a/app/api/admin/survey/route.ts b/app/api/admin/survey/route.ts new file mode 100644 index 0000000..6b2c9a8 --- /dev/null +++ b/app/api/admin/survey/route.ts @@ -0,0 +1,164 @@ +import { NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; +import { createAdminClient } from '@/lib/supabase/admin'; +import { verifyAdminTokenNode } from '@/lib/admin-auth'; + +export const runtime = 'nodejs'; + +async function checkAuth() { + const cookieStore = await cookies(); + const token = cookieStore.get('admin_token')?.value; + return token && verifyAdminTokenNode(token); +} + +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; + utm_medium: string | null; + utm_campaign: string | null; + completion_seconds: number | null; +} + +export async function GET(request: Request) { + if (!(await checkAuth())) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const url = new URL(request.url); + const range = url.searchParams.get('range') ?? 'all'; + const format = url.searchParams.get('format') ?? 'json'; + + const supabase = createAdminClient(); + let query = supabase + .from('survey_responses') + .select('*') + .order('created_at', { ascending: false }); + + if (range === 'today') { + const today = new Date(); + today.setHours(0, 0, 0, 0); + query = query.gte('created_at', today.toISOString()); + } else if (range === 'week') { + const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); + query = query.gte('created_at', weekAgo.toISOString()); + } + + const { data, error } = await query; + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + + const rows: SurveyRow[] = (data ?? []) as SurveyRow[]; + + if (format === 'csv') { + const csv = toCsv(rows); + return new Response(csv, { + headers: { + 'Content-Type': 'text/csv; charset=utf-8', + 'Content-Disposition': `attachment; filename="contour-survey-${range}-${new Date().toISOString().slice(0, 10)}.csv"`, + }, + }); + } + + return NextResponse.json({ + total: rows.length, + stats: computeStats(rows), + responses: rows, + }); +} + +function toCsv(rows: SurveyRow[]): string { + if (rows.length === 0) return 'id,created_at\n'; + const headers: (keyof SurveyRow)[] = [ + 'id', + 'created_at', + 'age_range', + 'status', + 'awareness_freq', + 'tools_used', + 'tools_other', + 'cost_range', + 'best_tool', + 'best_satisfy', + 'free_opinion', + 'email', + 'user_agent', + 'referrer', + 'utm_source', + 'utm_medium', + 'utm_campaign', + 'completion_seconds', + ]; + // BOM for Excel UTF-8 호환 + const bom = ''; + const lines = [headers.join(',')]; + for (const r of rows) { + lines.push( + headers + .map((h) => { + const v = r[h]; + if (v == null) return ''; + if (Array.isArray(v)) return `"${v.join('|').replace(/"/g, '""')}"`; + return `"${String(v).replace(/"/g, '""').replace(/\r?\n/g, ' ')}"`; + }) + .join(',') + ); + } + return bom + lines.join('\n'); +} + +function counts(rows: SurveyRow[], key: keyof SurveyRow): Record { + return rows.reduce((acc, r) => { + const v = r[key]; + if (v != null && typeof v === 'string') { + acc[v] = (acc[v] ?? 0) + 1; + } + return acc; + }, {} as Record); +} + +function computeStats(rows: SurveyRow[]) { + const satisfyValues = rows + .map((r) => r.best_satisfy) + .filter((n): n is number => typeof n === 'number'); + const satisfyAvg = + satisfyValues.length > 0 + ? (satisfyValues.reduce((s, n) => s + n, 0) / satisfyValues.length).toFixed(2) + : '0'; + + const completionValues = rows + .map((r) => r.completion_seconds) + .filter((n): n is number => typeof n === 'number'); + const completionMedian = median(completionValues); + + return { + age_range: counts(rows, 'age_range'), + status: counts(rows, 'status'), + awareness_freq: counts(rows, 'awareness_freq'), + cost_range: counts(rows, 'cost_range'), + best_tool: counts(rows, 'best_tool'), + satisfy_avg: satisfyAvg, + email_rate: rows.length === 0 ? '0' : ((rows.filter((r) => r.email).length / rows.length) * 100).toFixed(1), + completion_seconds_median: completionMedian, + }; +} + +function median(arr: number[]): number { + if (arr.length === 0) return 0; + const sorted = [...arr].sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2); +}