'use client'; import { useEffect, useState, useCallback } from 'react'; type RangeKey = '7' | '30' | '90'; interface Summary { users: number; sessions: number; pageviews: number; } interface AnalyticsData { summary: { today: Summary; yesterday: Summary; week: Summary; period: Summary; }; daily: Array<{ date: string; users: number; sessions: number; pageviews: number }>; topPages: Array<{ page: string; views: number; users: number }>; sources: Array<{ channel: string; sessions: number }>; devices: Array<{ device: string; sessions: number }>; } const RANGE_LABELS: Record = { '7': '최근 7일', '30': '최근 30일', '90': '최근 90일', }; const CHANNEL_KO: Record = { 'Organic Search': '검색 (유기)', 'Direct': '직접 방문', 'Organic Social': '소셜 미디어', 'Referral': '외부 링크', 'Paid Search': '검색 광고', 'Email': '이메일', 'Unassigned': '미분류', }; const DEVICE_KO: Record = { 'desktop': 'PC', 'mobile': '모바일', 'tablet': '태블릿', }; const CHANNEL_COLORS: Record = { 'Organic Search': '#22c55e', 'Direct': '#3b82f6', 'Organic Social': '#a855f7', 'Referral': '#f59e0b', 'Paid Search': '#ef4444', 'Email': '#06b6d4', 'Unassigned': '#64748b', }; function fmt(n: number) { if (n >= 1000) return (n / 1000).toFixed(1) + 'k'; return n.toString(); } function fmtDate(yyyymmdd: string) { const m = yyyymmdd.slice(4, 6); const d = yyyymmdd.slice(6, 8); return `${parseInt(m)}/${parseInt(d)}`; } // 인라인 막대 차트 function BarChart({ data }: { data: AnalyticsData['daily'] }) { const max = Math.max(...data.map((d) => d.users), 1); const w = 600; const h = 160; const padL = 36; const padR = 12; const padT = 12; const padB = 32; const chartW = w - padL - padR; const chartH = h - padT - padB; const barW = Math.max(2, chartW / data.length - 2); // Y축 눈금 const ticks = [0, Math.ceil(max / 2), max]; return ( {/* Y 눈금선 */} {ticks.map((t) => { const y = padT + chartH - (t / max) * chartH; return ( {fmt(t)} ); })} {/* 막대 + X 레이블 */} {data.map((d, i) => { const x = padL + (i / data.length) * chartW + (chartW / data.length - barW) / 2; const barH = Math.max(2, (d.users / max) * chartH); const y = padT + chartH - barH; const showLabel = data.length <= 14 || i % Math.ceil(data.length / 10) === 0; return ( {showLabel && ( {fmtDate(d.date)} )} {`${fmtDate(d.date)}: ${d.users}명`} ); })} ); } // 유입 경로 가로 바 function SourceBar({ channel, sessions, total }: { channel: string; sessions: number; total: number }) { const pct = total > 0 ? (sessions / total) * 100 : 0; const color = CHANNEL_COLORS[channel] ?? '#64748b'; return (
{CHANNEL_KO[channel] ?? channel}
{sessions.toLocaleString()}
{pct.toFixed(1)}%
); } function StatCard({ label, value, sub, trend, }: { label: string; value: number; sub?: string; trend?: number; // 양수: 증가, 음수: 감소 }) { return (

{label}

{value.toLocaleString()}

{sub &&

{sub}

} {trend !== undefined && (

= 0 ? 'text-emerald-400' : 'text-red-400'}`}> {trend >= 0 ? '▲' : '▼'} {Math.abs(trend).toFixed(0)}% vs 어제

)}
); } export default function AnalyticsPage() { const [range, setRange] = useState('30'); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const load = useCallback(async (r: RangeKey) => { setLoading(true); setError(null); try { const res = await fetch(`/api/admin/analytics?range=${r}`); const json = await res.json(); if (!res.ok) throw new Error(json.error ?? '데이터 로드 실패'); setData(json); } catch (e) { setError(e instanceof Error ? e.message : '오류가 발생했습니다.'); } finally { setLoading(false); } }, []); useEffect(() => { load(range); }, [range, load]); const todayTrend = data && data.summary.yesterday.users > 0 ? ((data.summary.today.users - data.summary.yesterday.users) / data.summary.yesterday.users) * 100 : undefined; const totalSessions = data?.sources.reduce((s, c) => s + c.sessions, 0) ?? 0; return (
{/* 헤더 */}

방문자 분석

Google Analytics 4 데이터

{/* 기간 선택 */}
{(Object.keys(RANGE_LABELS) as RangeKey[]).map((r) => ( ))}
{/* 에러 / 설정 안내 */} {error && (

⚠ 데이터를 불러올 수 없습니다

{error}

{(error.includes('GOOGLE_SERVICE_ACCOUNT_JSON') || error.includes('GA4_PROPERTY_ID')) && (

환경변수 설정 필요 (.env.local + Vercel):

GOOGLE_SERVICE_ACCOUNT_JSON={서비스 계정 JSON 전체}

GA4_PROPERTY_ID=숫자로된_속성ID

)}
)} {loading && !error && (
)} {data && !loading && ( <> {/* 요약 카드 */}
0 ? (data.summary.period.pageviews / data.summary.period.users).toFixed(1) : 0} 페이지`} />
{/* 일별 추이 차트 */}

일별 방문자 추이 (활성 사용자)

{data.daily.length > 0 ? ( ) : (

데이터 없음

)}
{/* 유입 경로 */}

유입 경로

{data.sources.length > 0 ? (
{data.sources.slice(0, 7).map((s) => ( ))}
) : (

데이터 없음

)}
{/* 기기 + 상위 페이지 */}
{/* 기기 분포 */}

기기 유형

{data.devices.map((d) => { const pct = totalSessions > 0 ? ((d.sessions / totalSessions) * 100).toFixed(0) : '0'; const icons: Record = { desktop: '🖥', mobile: '📱', tablet: '⬜' }; return (

{icons[d.device] ?? '?'}

{pct}%

{DEVICE_KO[d.device] ?? d.device}

{d.sessions.toLocaleString()} 세션

); })}
{/* 상위 페이지 */}

상위 페이지

{data.topPages.slice(0, 6).map((p, i) => (
{i + 1} {p.page} {p.views.toLocaleString()}
))}

Google Analytics 4 · 데이터 기준 최대 24~48시간 지연 가능

)}
); }