import Link from 'next/link'; import { useRouter } from 'next/router'; import { useCallback, useEffect, useState } from 'react'; import AdminLayout from '../../components/admin/AdminLayout'; import AdminTable from '../../components/admin/AdminTable'; import StatusBadge from '../../components/admin/StatusBadge'; import { AdminApiError, fetchAdmin } from '../../lib/admin/api'; import { requireAuth } from '../../lib/admin/requireAuth'; const FILTERS = [ { key: '', label: 'All' }, { key: 'pending', label: 'Pending' }, { key: 'read', label: 'Read' }, { key: 'replied', label: 'Replied' }, ]; const STATUS_OPTIONS = ['pending', 'read', 'replied']; function formatDate(iso) { try { return new Date(iso).toLocaleString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); } catch { return iso; } } function AdminContactInbox({ initialPage, initialStatus }) { const router = useRouter(); // 필터 탭 클릭 → router.push 로 쿼리만 변경 → getServerSideProps 재실행 → 새 props 주입. // optimistic 상태 변경(드롭다운 PATCH) 을 위해 page 는 state 로 들고 있되, // 라우팅 후 내려온 initialPage 로 다시 동기화한다. status 는 파생값이라 props 그대로 사용. const [page, setPage] = useState(initialPage); useEffect(() => { setPage(initialPage); }, [initialPage]); const status = initialStatus; const [pendingId, setPendingId] = useState(null); const [error, setError] = useState(''); const applyFilter = useCallback( (nextStatus) => { router.push( nextStatus ? { pathname: '/admin/contact', query: { status: nextStatus } } : { pathname: '/admin/contact' }, ); }, [router], ); const handleStatusChange = useCallback( async (item, nextStatus) => { if (nextStatus === item.status) return; setPendingId(item.id); setError(''); try { await fetchAdmin(`/api/admin/contact/${item.id}/status`, { method: 'PATCH', body: { status: nextStatus }, }); setPage((prev) => ({ ...prev, items: prev.items.map((row) => row.id === item.id ? { ...row, status: nextStatus } : row, ), })); } catch (err) { if (err instanceof AdminApiError && err.status === 401) { router.replace('/admin/login'); return; } setError(err?.message ?? '상태 변경에 실패했습니다.'); } finally { setPendingId(null); } }, [router], ); return (
{FILTERS.map(({ key, label }) => { const active = (status ?? '') === key; return ( ); })} {page.total}건 (페이지 {page.page})
{error && (

{error}

)} ( {formatDate(row.createdAt)} ), }, { key: 'name', label: '이름' }, { key: 'email', label: '이메일', render: (row) => ( {row.email} ), }, { key: 'subject', label: '주제' }, { key: 'status', label: '상태', render: (row) => (
), }, ]} rows={page.items} actions={(row) => ( 상세 )} emptyMessage="조건에 맞는 제출 내역이 없습니다." />
); } const API_BASE_URL = process.env.API_INTERNAL_URL || 'http://localhost:7341'; export const getServerSideProps = requireAuth(async (ctx) => { const cookieHeader = ctx.req?.headers?.cookie ?? ''; const status = typeof ctx.query.status === 'string' ? ctx.query.status : ''; const params = new URLSearchParams(); if (status) params.set('status', status); const res = await fetch( `${API_BASE_URL}/api/admin/contact?${params.toString()}`, { headers: cookieHeader ? { cookie: cookieHeader } : undefined }, ); if (!res.ok) { throw new Error(`[admin contact] list API returned ${res.status}`); } const body = await res.json(); return { props: { initialPage: body?.data ?? { items: [], total: 0, page: 1, limit: 20 }, initialStatus: status || null, }, }; }); export default AdminContactInbox;