CarboAny / src /pages /admin /ProgramDetailPage.tsx
Esketch's picture
deploy: [P4] Strangler Pattern API Adapter Release (Orphan Clean Build v2)
daaf9d7
Raw
History Blame Contribute Delete
31.8 kB
import { useEffect, useMemo, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import type { Program, EventType, EventVerification, VerificationFraudFlag } from '../../types';
import { findMockProgramById } from '../../data/mockPrograms';
import { getAdminProgram } from '../../lib/admin';
import { CAMPAIGN_STATUS } from '../../lib/campaignStatusConfig';
import TermTip from '../../components/common/TermTip';
import Dn from '../../components/Dn';
const eventTypeLabel: Record<EventType, string> = {
plogging: 'ν”Œλ‘œκΉ…',
afforestation: 'λ‚˜λ¬΄ 심기',
energy_saving: 'μ—λ„ˆμ§€ μ ˆμ•½',
commute_green: 'μΉœν™˜κ²½ 톡근',
livestock_feed: '저메탄 μ‚¬λ£Œ',
};
const programTypeLabel: Record<Program['programType'], string> = {
corporate_event: 'κΈ°μ—… ESG',
public_caas: '곡곡 CaaS',
sponsor: 'μŠ€ν°μ„œ',
livestock: 'μΆ•μ‚°',
};
const verifyTypeLabel: Record<EventVerification['type'], string> = {
photo: '사진',
gps: 'GPS',
weight: '무게 μΈ‘μ •',
combo: '볡합 인증',
};
const verifyStatusConfig: Record<
EventVerification['status'],
{ label: string; className: string }
> = {
pending: { label: 'λŒ€κΈ°', className: 'text-status-warning' },
approved: { label: '승인', className: 'text-status-success' },
rejected: { label: '반렀', className: 'text-status-error' },
};
const fraudFlagLabel: Record<VerificationFraudFlag, string> = {
duplicate_photo: '쀑볡 사진',
gps_mismatch: 'GPS μ΄νƒˆ',
weight_outlier: '무게 μ΄μƒμΉ˜',
rapid_submission: '연속 제좜',
low_confidence: '저신뒰도',
metadata_stripped: '메타데이터 λˆ„λ½',
};
type VerifyFilter = 'all' | 'pending' | 'approved' | 'rejected' | 'flagged';
const formatKRW = (n: number) =>
n === 0 ? 'β€”' : new Intl.NumberFormat('ko-KR').format(n) + '원';
const formatNum = (n: number) =>
n === 0 ? 'β€”' : n.toLocaleString('ko-KR');
export default function ProgramDetailPage() {
const { id } = useParams<{ id: string }>();
const mockProgram = findMockProgramById(id);
const [dbProgram, setDbProgram] = useState<Program | null>(null);
const [loadingProgram, setLoadingProgram] = useState(!mockProgram);
const program = mockProgram ?? dbProgram;
const [filter, setFilter] = useState<VerifyFilter>('all');
const [selected, setSelected] = useState<Set<string>>(new Set());
const [statusOverrides, setStatusOverrides] = useState<
Record<string, EventVerification['status']>
>({});
useEffect(() => {
if (!id || mockProgram) {
setDbProgram(null);
setLoadingProgram(false);
return;
}
let cancelled = false;
setLoadingProgram(true);
getAdminProgram(id)
.then((nextProgram) => {
if (!cancelled) setDbProgram(nextProgram);
})
.finally(() => {
if (!cancelled) setLoadingProgram(false);
});
return () => {
cancelled = true;
};
}, [id, mockProgram]);
const decoratedVerifications = useMemo(() => {
const verifications = program?.verifications ?? [];
return verifications.map((v) => ({
...v,
status: statusOverrides[v.id] ?? v.status,
}));
}, [program?.verifications, statusOverrides]);
const filteredVerifications = useMemo(() => {
if (filter === 'all') return decoratedVerifications;
if (filter === 'flagged')
return decoratedVerifications.filter((v) => (v.fraudFlags?.length ?? 0) > 0);
return decoratedVerifications.filter((v) => v.status === filter);
}, [decoratedVerifications, filter]);
const dailyTrend = useMemo(() => {
const m = new Map<string, { approved: number; pending: number; rejected: number }>();
for (const v of decoratedVerifications) {
const date = v.verifiedAt.slice(0, 10);
const cur = m.get(date) ?? { approved: 0, pending: 0, rejected: 0 };
cur[v.status] += 1;
m.set(date, cur);
}
return Array.from(m.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([date, counts]) => ({ date, ...counts, total: counts.approved + counts.pending + counts.rejected }));
}, [decoratedVerifications]);
const leaderboard = useMemo(() => {
const m = new Map<
string,
{ name: string; team?: string; count: number; approved: number; co2: number; waste: number }
>();
for (const v of decoratedVerifications) {
const key = v.participantName;
const cur = m.get(key) ?? {
name: v.participantName,
team: v.participantTeam,
count: 0,
approved: 0,
co2: 0,
waste: 0,
};
cur.count += 1;
if (v.status === 'approved') cur.approved += 1;
cur.co2 += v.co2ContributionKg ?? 0;
cur.waste += v.wasteKg ?? 0;
m.set(key, cur);
}
return Array.from(m.values())
.sort((a, b) => b.co2 - a.co2 || b.approved - a.approved)
.slice(0, 8);
}, [decoratedVerifications]);
const counts = useMemo(() => {
const c = { all: 0, pending: 0, approved: 0, rejected: 0, flagged: 0 };
for (const v of decoratedVerifications) {
c.all += 1;
c[v.status] += 1;
if ((v.fraudFlags?.length ?? 0) > 0) c.flagged += 1;
}
return c;
}, [decoratedVerifications]);
if (loadingProgram) {
return (
<div className="p-12 text-center text-ink-light font-mono uppercase animate-pulse">
ν”„λ‘œκ·Έλž¨ 상세 데이터 동기화 쀑... [SYNCHRONIZING...]
</div>
);
}
if (!program) {
return (
<div className="p-12 text-center">
<p className="text-ink-light">ν”„λ‘œκ·Έλž¨μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.</p>
<Link to="/admin/programs" className="text-sm text-ink-dark underline mt-2 inline-block">
λͺ©λ‘μœΌλ‘œ λŒμ•„κ°€κΈ°
</Link>
</div>
);
}
const st = CAMPAIGN_STATUS[program.status];
const participationRate =
program.targetParticipants > 0
? ((program.currentParticipants / program.targetParticipants) * 100).toFixed(1)
: 'β€”';
const verificationRate =
program.totalVerifications > 0
? ((program.approvedVerifications / program.totalVerifications) * 100).toFixed(1)
: '0';
const toggleSelect = (vid: string) => {
setSelected((prev) => {
const next = new Set(prev);
if (next.has(vid)) next.delete(vid);
else next.add(vid);
return next;
});
};
const toggleSelectAll = () => {
if (selected.size === filteredVerifications.length) {
setSelected(new Set());
} else {
setSelected(new Set(filteredVerifications.map((v) => v.id)));
}
};
const bulkAction = (status: EventVerification['status']) => {
if (selected.size === 0) return;
setStatusOverrides((prev) => {
const next = { ...prev };
for (const sid of selected) next[sid] = status;
return next;
});
setSelected(new Set());
};
const maxDaily = Math.max(1, ...dailyTrend.map((d) => d.total));
const maxCo2 = Math.max(0.01, ...leaderboard.map((l) => l.co2));
return (
<div>
{/* Header */}
<div className="mb-8">
<Link
to="/admin/programs"
className="text-xs text-ink-light hover:text-ink-dark transition-colors"
>
← ν”„λ‘œκ·Έλž¨ λͺ©λ‘
</Link>
<div className="flex items-center gap-3 mt-2 flex-wrap">
<span className={`text-xs font-bold px-2 py-0.5 ${program.programType === 'corporate_event' ? 'bg-accent-teal text-white' : 'bg-status-info text-paper-white'}`}>
{programTypeLabel[program.programType]}
</span>
{program.activityType && (
<span className="text-xs font-bold px-2 py-0.5 bg-paper-gray text-ink-dark">
{eventTypeLabel[program.activityType]}
</span>
)}
<span className={`text-xs font-bold px-2 py-0.5 ${st.cls}`}>{st.label}</span>
<h2 className="text-2xl font-bold text-ink-black">{program.title}</h2>
</div>
<p className="text-sm text-ink-medium mt-1">
{program.organizationName} Β· {program.startDate} ~ {program.endDate}
</p>
{program.methodology && (
<p className="text-xs text-ink-medium mt-1">
방법둠: <span className="font-semibold">{program.methodology}</span>
{program.methodologyAutoAssigned && (
<span className="ml-2 text-[10px] px-1.5 py-0.5 border border-border-default text-ink-light">μžλ™ν• λ‹Ή</span>
)}
</p>
)}
</div>
{/* Key metrics */}
<div className="grid grid-cols-5 gap-px bg-border-default border border-border-default mb-6">
<div className="bg-paper-white px-5 py-4">
<p className="text-xs text-ink-light">μ°Έμ—¬μœ¨</p>
<p className="text-xl font-bold text-ink-black mt-1">{participationRate !== 'β€”' ? <><Dn>{participationRate}</Dn>%</> : 'β€”'}</p>
{program.targetParticipants > 0 && (
<p className="text-xs text-ink-medium mt-0.5">
{program.currentParticipants.toLocaleString()}/{program.targetParticipants.toLocaleString()}λͺ…
</p>
)}
</div>
<div className="bg-paper-white px-5 py-4">
<p className="text-xs text-ink-light">인증 승인율</p>
<p className="text-xl font-bold text-ink-black mt-1"><Dn>{verificationRate}</Dn>%</p>
<p className="text-xs text-ink-medium mt-0.5">
{program.approvedVerifications.toLocaleString()}/{program.totalVerifications.toLocaleString()}건
</p>
</div>
<div className="bg-paper-white px-5 py-4">
<p className="text-xs text-ink-light">COβ‚‚ κ°μΆ•λŸ‰</p>
<p className="text-xl font-bold text-ink-black mt-1">
{program.co2ReductionKg > 0 ? program.co2ReductionKg.toLocaleString() : 'β€”'}
</p>
<p className="text-xs text-ink-medium mt-0.5">
{program.co2ReductionKg > 0 ? 'kg COβ‚‚e' : 'μ‚°μ • 쀑'}
</p>
</div>
<div className="bg-paper-white px-5 py-4">
<p className="text-xs text-ink-light">포인트</p>
<p className="text-xl font-bold text-ink-black mt-1">
{program.pointsDistributed > 0 ? program.pointsDistributed.toLocaleString() : 'β€”'}
</p>
<p className="text-xs text-ink-medium mt-0.5">
{program.pointsDistributed > 0 ? 'P μ§€κΈ‰' : ''}
</p>
</div>
<div className="bg-paper-white px-5 py-4">
<p className="text-xs text-ink-light">
{program.programType === 'corporate_event' ? 'SaaS 수수료' : 'μ˜ˆμ‚°'}
</p>
<p className="text-xl font-bold text-ink-black mt-1">
{program.programType === 'corporate_event'
? formatKRW(program.saasFeePaid ?? 0)
: formatKRW(program.budgetKRW)}
</p>
</div>
</div>
{/* Public CaaS extras: API + Fraud */}
{program.programType === 'public_caas' && (
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="border border-border-default bg-paper-white rounded-lg shadow-sm p-4">
<p className="text-xs text-ink-light">총 API 호좜</p>
<p className="text-lg font-bold text-ink-black mt-1">{formatNum(program.apiCallsTotal ?? 0)}</p>
</div>
<div className="border border-border-default bg-paper-white rounded-lg shadow-sm p-4">
<p className="text-xs text-ink-light">금일 API</p>
<p className="text-lg font-bold text-ink-black mt-1">{formatNum(program.apiCallsToday ?? 0)}</p>
</div>
<div className="border border-border-default bg-paper-white rounded-lg shadow-sm p-4">
<p className="text-xs text-ink-light">λΆ€μ •μˆ˜κΈ‰ 감지</p>
<p className={`text-lg font-bold mt-1 ${(program.fraudDetected ?? 0) > 0 ? 'text-status-error' : 'text-ink-light'}`}>
{program.fraudDetected ?? 0}건
</p>
</div>
</div>
)}
{/* Fraud / Integrity Summary */}
{counts.flagged > 0 && (
<div className="border border-status-warning bg-paper-cream px-4 py-3 mb-6 flex items-center justify-between">
<div className="text-xs text-ink-dark">
<span className="font-semibold text-status-warning">이상 μ§•ν›„ {counts.flagged}건 감지</span>
<span className="text-ink-medium ml-2">
AI 신뒰도·GPS·무게·제좜 νŒ¨ν„΄ 기반 μžλ™ 탐지. κ²€ν†  ν•„μš” ν•­λͺ©μ„ μš°μ„  ν™•μΈν•˜μ„Έμš”.
</span>
</div>
<button
type="button"
onClick={() => setFilter('flagged')}
className="px-3 py-1 text-xs font-semibold border border-status-warning text-status-warning hover:bg-paper-white transition-colors"
>
이상 μ§•ν›„ ν•„ν„°
</button>
</div>
)}
{/* Daily Trend + Leaderboard */}
{(dailyTrend.length > 0 || leaderboard.length > 0) && (
<div className="grid grid-cols-5 gap-6 mb-6">
<div className="col-span-3 border border-border-default bg-paper-white rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-border-default flex items-center justify-between">
<div>
<h3 className="text-sm font-semibold text-ink-black">μΌμžλ³„ 인증 좔이</h3>
<p className="text-xs text-ink-light mt-0.5">승인/λŒ€κΈ°/반렀 λˆ„μ  뢄포</p>
</div>
<div className="flex items-center gap-3 text-[10px] text-ink-medium">
<span className="flex items-center gap-1"><span className="w-2 h-2 bg-status-success inline-block" />승인</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 bg-status-warning inline-block" />λŒ€κΈ°</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 bg-status-error inline-block" />반렀</span>
</div>
</div>
<div className="p-6">
{dailyTrend.length === 0 ? (
<p className="text-sm text-ink-light text-center py-8">데이터 μ—†μŒ</p>
) : (
<div className="flex items-end gap-3 h-40">
{dailyTrend.map((d) => {
const heightPct = (d.total / maxDaily) * 100;
const aPct = d.total > 0 ? (d.approved / d.total) * heightPct : 0;
const pPct = d.total > 0 ? (d.pending / d.total) * heightPct : 0;
const rPct = d.total > 0 ? (d.rejected / d.total) * heightPct : 0;
return (
<div key={d.date} className="flex-1 flex flex-col items-center">
<div className="w-full h-32 flex flex-col justify-end border-b border-border-default relative">
<div style={{ height: `${rPct}%` }} className="bg-status-error w-full" />
<div style={{ height: `${pPct}%` }} className="bg-status-warning w-full" />
<div style={{ height: `${aPct}%` }} className="bg-status-success w-full" />
</div>
<p className="text-[10px] text-ink-light mt-1.5 font-mono">
{d.date.slice(5)}
</p>
<p className="text-xs font-semibold text-ink-black">{d.total}</p>
</div>
);
})}
</div>
)}
</div>
</div>
<div className="col-span-2 border border-border-default bg-paper-white rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-border-default">
<h3 className="text-sm font-semibold text-ink-black">μ°Έμ—¬μž λ¦¬λ”λ³΄λ“œ</h3>
<p className="text-xs text-ink-light mt-0.5">COβ‚‚ 기여도 Top 8</p>
</div>
<div className="p-4">
{leaderboard.length === 0 ? (
<p className="text-sm text-ink-light text-center py-8">데이터 μ—†μŒ</p>
) : (
<div className="space-y-2">
{leaderboard.map((l, idx) => {
const widthPct = (l.co2 / maxCo2) * 100;
return (
<div key={l.name} className="flex items-center gap-3">
<span className="text-xs font-mono text-ink-light w-5 text-right">
{String(idx + 1).padStart(2, '0')}
</span>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between text-xs">
<span className="font-medium text-ink-dark truncate">
{l.name}
{l.team && <span className="text-ink-light ml-1.5">Β· {l.team}</span>}
</span>
<span className="font-mono text-ink-black ml-2 whitespace-nowrap">
<Dn>{l.co2.toFixed(1)}</Dn> kg
</span>
</div>
<div className="h-1 bg-paper-gray mt-1 relative">
<div
className="h-full bg-ink-dark"
style={{ width: `${widthPct}%` }}
/>
</div>
<p className="text-[10px] text-ink-light mt-0.5">
{l.approved}/{l.count} 승인 · 수거 <Dn>{l.waste.toFixed(1)}</Dn> kg
</p>
</div>
</div>
);
})}
</div>
)}
</div>
</div>
</div>
)}
{/* Event-specific metrics (waste, trees) */}
{program.programType === 'corporate_event' &&
((program.wasteCollectedKg != null && program.wasteCollectedKg > 0) ||
(program.treesPlanted != null && program.treesPlanted! > 0)) && (
<div className="border border-border-default bg-paper-white rounded-lg shadow-sm p-6 mb-6">
<h3 className="text-sm font-semibold text-ink-black mb-4">ν™œλ™ νŠΉν™” μ§€ν‘œ</h3>
<div className="grid grid-cols-3 gap-6">
{program.wasteCollectedKg != null && program.wasteCollectedKg > 0 && (
<div>
<p className="text-xs text-ink-light">총 μˆ˜κ±°λŸ‰</p>
<p className="text-2xl font-bold text-ink-black mt-1">
{program.wasteCollectedKg.toLocaleString()} <span className="text-sm font-normal text-ink-medium">kg</span>
</p>
<p className="text-xs text-ink-light mt-1">
인당 평균 <Dn>{(program.wasteCollectedKg / Math.max(1, program.currentParticipants)).toFixed(1)}</Dn> kg
</p>
</div>
)}
{program.treesPlanted != null && program.treesPlanted > 0 && (
<>
<div>
<p className="text-xs text-ink-light">μ‹μž¬ λ‚˜λ¬΄</p>
<p className="text-2xl font-bold text-ink-black mt-1">
{program.treesPlanted} <span className="text-sm font-normal text-ink-medium">그루</span>
</p>
</div>
<div>
<p className="text-xs text-ink-light">μ˜ˆμƒ μ—°κ°„ COβ‚‚ ν‘μˆ˜λŸ‰</p>
<p className="text-2xl font-bold text-ink-black mt-1">
{(program.treesPlanted * 22).toLocaleString()} <span className="text-sm font-normal text-ink-medium">kg/λ…„</span>
</p>
<p className="text-xs text-ink-light mt-1">μˆ˜μ’… 평균 22kg COβ‚‚/λ…„ κΈ°μ€€</p>
</div>
</>
)}
</div>
</div>
)}
{/* Verification management */}
{decoratedVerifications.length > 0 && (
<div className="border border-border-default bg-paper-white rounded-lg shadow-sm mb-6">
<div className="px-6 py-4 border-b border-border-default flex items-center justify-between">
<div>
<h3 className="text-sm font-semibold text-ink-black">인증 관리</h3>
<p className="text-xs text-ink-light mt-0.5">
AI μžλ™ νŒμ • + <TermTip termKey="fraudFlag">이상 μ§•ν›„</TermTip> 탐지 + μˆ˜λ™ κ²€ν† 
</p>
</div>
<div className="flex items-center gap-1 text-xs">
{(
[
{ key: 'all', label: '전체', count: counts.all },
{ key: 'pending', label: 'λŒ€κΈ°', count: counts.pending },
{ key: 'approved', label: '승인', count: counts.approved },
{ key: 'rejected', label: '반렀', count: counts.rejected },
{ key: 'flagged', label: '이상 μ§•ν›„', count: counts.flagged },
] as { key: VerifyFilter; label: string; count: number }[]
).map((tab) => (
<button
key={tab.key}
type="button"
onClick={() => setFilter(tab.key)}
className={`px-2.5 py-1 border transition-colors ${
filter === tab.key
? 'border-accent-teal bg-accent-teal text-white'
: 'border-border-default text-ink-medium hover:text-ink-dark'
}`}
>
{tab.label} <span className="font-mono ml-1">{tab.count}</span>
</button>
))}
</div>
</div>
{selected.size > 0 && (
<div className="px-6 py-3 bg-paper-cream border-b border-border-default flex items-center justify-between">
<p className="text-xs text-ink-dark">
<span className="font-semibold">{selected.size}건</span> 선택됨
</p>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => bulkAction('approved')}
className="px-3 py-1 text-xs font-semibold border border-status-success text-status-success hover:bg-paper-white transition-colors"
>
일괄 승인
</button>
<button
type="button"
onClick={() => bulkAction('rejected')}
className="px-3 py-1 text-xs font-semibold border border-status-error text-status-error hover:bg-paper-white transition-colors"
>
일괄 반렀
</button>
<button
type="button"
onClick={() => setSelected(new Set())}
className="px-3 py-1 text-xs text-ink-medium hover:text-ink-dark transition-colors"
>
선택 ν•΄μ œ
</button>
</div>
</div>
)}
{filteredVerifications.length === 0 ? (
<div className="p-8 text-center">
<p className="text-sm text-ink-light">
ν˜„μž¬ ν•„ν„° 쑰건에 ν•΄λ‹Ήν•˜λŠ” ν•­λͺ©μ΄ μ—†μŠ΅λ‹ˆλ‹€.
</p>
</div>
) : (
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border-default text-xs text-ink-light uppercase tracking-wider">
<th className="px-4 py-3 w-8 text-center">
<input
type="checkbox"
className="accent-ink-black"
checked={
filteredVerifications.length > 0 &&
selected.size === filteredVerifications.length
}
onChange={toggleSelectAll}
/>
</th>
<th className="text-left px-4 py-3 font-medium">μ°Έμ—¬μž</th>
<th className="text-left px-4 py-3 font-medium">인증 μΌμ‹œ</th>
<th className="text-left px-4 py-3 font-medium">방식</th>
<th className="text-left px-4 py-3 font-medium">λ‚΄μš©</th>
<th className="text-center px-4 py-3 font-medium">
<TermTip termKey="confidenceScore">신뒰도</TermTip>
</th>
<th className="text-left px-4 py-3 font-medium">ν”Œλž˜κ·Έ</th>
<th className="text-center px-4 py-3 font-medium">μƒνƒœ</th>
</tr>
</thead>
<tbody>
{filteredVerifications.map((v) => {
const vst = verifyStatusConfig[v.status];
const flags = v.fraudFlags ?? [];
const isSelected = selected.has(v.id);
const confidence = v.confidenceScore ?? 0;
const confClass =
confidence >= 90
? 'text-status-success'
: confidence >= 60
? 'text-status-warning'
: 'text-status-error';
return (
<tr
key={v.id}
className={`border-b border-border-default last:border-b-0 ${
isSelected ? 'bg-paper-cream' : ''
}`}
>
<td className="px-4 py-3 text-center">
<input
type="checkbox"
className="accent-ink-black"
checked={isSelected}
onChange={() => toggleSelect(v.id)}
/>
</td>
<td className="px-4 py-3">
<div className="font-medium text-ink-dark">{v.participantName}</div>
{v.participantTeam && (
<div className="text-[10px] text-ink-light">{v.participantTeam}</div>
)}
</td>
<td className="px-4 py-3 font-mono text-xs text-ink-light whitespace-nowrap">
{v.verifiedAt}
</td>
<td className="px-4 py-3">
<span className="text-xs font-bold px-2 py-0.5 bg-paper-gray text-ink-dark">
{verifyTypeLabel[v.type]}
</span>
</td>
<td className="px-4 py-3 text-ink-medium max-w-xs">
<div className="truncate">{v.value}</div>
{v.locationName && (
<div className="text-[10px] text-ink-light mt-0.5">
πŸ“ {v.locationName}
</div>
)}
{v.rejectionReason && (
<div className="text-[10px] text-status-error mt-0.5">
μ‚¬μœ : {v.rejectionReason}
</div>
)}
</td>
<td className="px-4 py-3 text-center">
{v.confidenceScore != null ? (
<span className={`font-mono text-xs font-semibold ${confClass}`}>
{v.confidenceScore}
</span>
) : (
<span className="text-xs text-ink-light">β€”</span>
)}
</td>
<td className="px-4 py-3">
<div className="flex flex-wrap gap-1">
{flags.length === 0 ? (
<span className="text-[10px] text-ink-light">β€”</span>
) : (
flags.map((f) => (
<span
key={f}
className="text-[10px] font-semibold px-1.5 py-0.5 border border-status-warning text-status-warning bg-paper-cream"
>
{fraudFlagLabel[f]}
</span>
))
)}
</div>
</td>
<td
className={`px-4 py-3 text-center font-semibold text-xs ${vst.className}`}
>
{vst.label}
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
)}
{/* ESG Report Preview */}
<div className="border-2 border-accent-teal bg-paper-white rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-border-strong flex items-center justify-between">
<div>
<h3 className="text-sm font-bold text-ink-black">
ESG κ²°κ³Ό λ³΄κ³ μ„œ
</h3>
<p className="text-xs text-ink-medium mt-0.5">
μžλ™ 생성 β€” {program.programType === 'corporate_event' ? 'κΈ°μ—… ν™λ³΄μžλ£Œ 및 ESG κ³΅μ‹œ' : 'NGMS 행정보고 및 감사'} ν™œμš© κ°€λŠ₯
</p>
</div>
<button
type="button"
className="px-3 py-1.5 text-xs font-semibold border border-accent-teal text-ink-black hover:bg-paper-gray transition-colors"
>
PDF λ‹€μš΄λ‘œλ“œ
</button>
</div>
<div className="p-6">
<div className="text-center mb-6 pb-6 border-b border-border-default">
<p className="text-xs text-ink-light uppercase tracking-widest mb-2">ESG IMPACT REPORT</p>
<h4 className="text-lg font-bold text-ink-black">{program.title}</h4>
<p className="text-sm text-ink-medium">{program.organizationName} Β· {program.startDate} ~ {program.endDate}</p>
</div>
<div className="grid grid-cols-3 gap-6 mb-6">
<div className="text-center p-4 border border-border-default">
<p className="text-3xl font-bold text-ink-black">{program.currentParticipants.toLocaleString()}</p>
<p className="text-xs text-ink-light mt-1">μ°Έμ—¬ 인원</p>
</div>
<div className="text-center p-4 border border-border-default">
<p className="text-3xl font-bold text-ink-black">
{program.co2ReductionKg > 0
? program.co2ReductionKg.toLocaleString()
: program.treesPlanted ?? 0}
</p>
<p className="text-xs text-ink-light mt-1">
{program.co2ReductionKg > 0 ? 'kg COβ‚‚e 감좕' : '그루 μ‹μž¬'}
</p>
</div>
<div className="text-center p-4 border border-border-default">
<p className="text-3xl font-bold text-ink-black">{program.approvedVerifications.toLocaleString()}</p>
<p className="text-xs text-ink-light mt-1">인증 μ™„λ£Œ 건수</p>
</div>
</div>
<p className="text-xs text-ink-light text-center">
λ³Έ λ³΄κ³ μ„œλŠ” Carboany ν•˜λ„€μŠ€ μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμœΌλ©°,
ꡭ제 νƒ„μ†Œν¬λ ˆλ”§ 방법둠에 κΈ°λ°˜ν•˜μ—¬ 데이터가 μ •λŸ‰ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
</p>
</div>
</div>
</div>
);
}