CarboAny / src /lib /admin.ts
Esketch's picture
deploy: [P4] Strangler Pattern API Adapter Release (Orphan Clean Build v2)
daaf9d7
Raw
History Blame Contribute Delete
7.73 kB
import { supabase } from './supabase';
import type { CampaignStatus } from './database.types';
import type { Program } from '../types';
import { mockPrograms, resolveMockProgramId } from '../data/mockPrograms';
export interface AdminProgramSummary {
id: string;
title: string;
sponsorName: string;
status: string;
startDate: string;
endDate?: string;
budgetKrw: number;
targetCo2Kg: number;
currentCo2Kg: number;
totalSubmissions: number;
pendingSubmissions: number;
participantCount: number;
achievementRate: number;
source: 'db' | 'demo';
}
export interface AdminSettlementStat {
asset: 'KRW' | 'POINT' | 'TON';
category: string;
totalAmount: number;
txCount: number;
lastTxAt: string;
}
function toSettlementAsset(value: unknown): AdminSettlementStat['asset'] {
return value === 'POINT' || value === 'TON' ? value : 'KRW';
}
function isMissingSchemaError(error: { code?: string } | null) {
return error?.code === 'PGRST205' || error?.code === '42P01';
}
function normalizeStatus(value: unknown): CampaignStatus {
if (
value === 'draft' ||
value === 'pending_review' ||
value === 'active' ||
value === 'paused' ||
value === 'completed'
) {
return value;
}
return 'draft';
}
function inferOrganizationType(
sponsorName: string
): Program['organizationType'] {
if (/๊ตฐ์ฒญ|์‹œ์ฒญ|๋„์ฒญ|๊ตฌ์ฒญ|ํŠน๋ณ„์‹œ|๊ด‘์—ญ์‹œ/.test(sponsorName)) return 'municipality';
if (/๊ณต์‚ฌ|๊ณต๋‹จ|์žฌ๋‹จ|๊ณต๊ณต|์ •๋ถ€|๋ถ€์ฒ˜|์ฒญ$/.test(sponsorName)) return 'public_corp';
return 'enterprise';
}
function dbSummaryToProgram(row: AdminProgramSummary): Program {
const approvedVerifications = Math.max(
0,
row.totalSubmissions - row.pendingSubmissions
);
return {
id: row.id,
title: row.title,
programType: inferOrganizationType(row.sponsorName) === 'enterprise'
? 'corporate_event'
: 'public_caas',
organizationName: row.sponsorName || '๋ฏธ๋“ฑ๋ก ์Šคํฐ์„œ',
organizationType: inferOrganizationType(row.sponsorName || ''),
status: normalizeStatus(row.status),
startDate: row.startDate,
endDate: row.endDate || '์ง„ํ–‰ ์ค‘',
targetParticipants: row.participantCount,
currentParticipants: row.participantCount,
totalVerifications: row.totalSubmissions,
approvedVerifications,
rejectedVerifications: 0,
pendingVerifications: row.pendingSubmissions,
co2ReductionKg: row.currentCo2Kg,
budgetKRW: row.budgetKrw,
pointsDistributed: 0,
methodology: 'DB ์บ ํŽ˜์ธ โ€” ์ƒ์„ธ ๋ฐฉ๋ฒ•๋ก  ์ •๋ณด ๋ฏธ๋“ฑ๋ก',
methodologyAutoAssigned: false,
apiCallsTotal: row.totalSubmissions,
apiCallsToday: 0,
fraudDetected: 0,
verifications: [],
};
}
function simulatedAdminPrograms(): AdminProgramSummary[] {
return mockPrograms.map(p => ({
id: p.id,
title: p.title,
sponsorName: p.organizationName,
status: p.status,
startDate: p.startDate,
endDate: p.endDate,
budgetKrw: p.budgetKRW || 0,
targetCo2Kg: p.co2ReductionKg || 0,
currentCo2Kg: p.co2ReductionKg || 0,
totalSubmissions: p.totalVerifications || 0,
pendingSubmissions: p.pendingVerifications || 0,
participantCount: p.currentParticipants || 0,
achievementRate: 0,
source: 'demo',
}));
}
function mapAdminProgramRow(row: Record<string, unknown>): AdminProgramSummary {
const campaignId = String(row.campaign_id ?? '');
return {
id: resolveMockProgramId(campaignId) ?? campaignId,
title: String(row.title ?? '์ œ๋ชฉ ๋ฏธ๋“ฑ๋ก ์บ ํŽ˜์ธ'),
sponsorName: String(row.sponsor_name ?? '๋ฏธ๋“ฑ๋ก ์Šคํฐ์„œ'),
status: String(row.status ?? 'draft'),
startDate: String(row.start_date ?? ''),
endDate: row.end_date ? String(row.end_date) : undefined,
budgetKrw: Number(row.budget_krw ?? 0),
targetCo2Kg: Number(row.target_co2_kg ?? 0),
currentCo2Kg: Number(row.current_co2_kg ?? 0),
totalSubmissions: Number(row.total_submissions ?? 0),
pendingSubmissions: Number(row.pending_submissions ?? 0),
participantCount: Number(row.participant_count ?? 0),
achievementRate: Number(row.achievement_rate ?? 0),
source: 'db',
};
}
/**
* ์ „์ฒด ํ”„๋กœ๊ทธ๋žจ(์บ ํŽ˜์ธ) ๋ชฉ๋ก ๋ฐ ์š”์•ฝ ์ •๋ณด ์กฐํšŒ
*/
export async function getAdminPrograms(): Promise<AdminProgramSummary[]> {
const { data, error } = await supabase
.from('admin_program_summary')
.select('*')
.order('start_date', { ascending: false });
if (error) {
if (!isMissingSchemaError(error)) console.error('getAdminPrograms error:', error);
return simulatedAdminPrograms();
}
const dbPrograms = (data || []).map(mapAdminProgramRow);
// Mock ๋ฐ์ดํ„ฐ์™€ ๋ณ‘ํ•ฉ (์™„๋„๊ตฐ ํ”„๋กœ์ ํŠธ ๊ฐ•์ œ ๋…ธ์ถœ)
const dbProgramIds = new Set(dbPrograms.map((program) => program.id));
return [
...dbPrograms,
...simulatedAdminPrograms().filter((program) => !dbProgramIds.has(program.id)),
];
}
export async function getAdminProgram(id: string): Promise<Program | null> {
const canonicalId = resolveMockProgramId(id) ?? id;
const mockProgram = mockPrograms.find((program) => program.id === canonicalId);
if (mockProgram) return mockProgram;
const { data, error } = await supabase
.from('admin_program_summary')
.select('*')
.eq('campaign_id', id)
.maybeSingle();
if (error) {
if (!isMissingSchemaError(error)) console.error('getAdminProgram error:', error);
return null;
}
return data ? dbSummaryToProgram(mapAdminProgramRow(data)) : null;
}
/**
* ์ „์—ญ ์ •์‚ฐ ํ˜„ํ™ฉ ๋ฐ์ดํ„ฐ ์กฐํšŒ
*/
export async function getGlobalSettlementStats(): Promise<AdminSettlementStat[]> {
const { data, error } = await supabase
.from('admin_global_settlement_stats')
.select('*');
if (error) {
if (!isMissingSchemaError(error)) console.error('getGlobalSettlementStats error:', error);
return [];
}
return (data || []).map((row) => ({
asset: toSettlementAsset(row.asset),
category: row.category,
totalAmount: Number(row.total_amount),
txCount: row.tx_count,
lastTxAt: row.last_tx_at,
}));
}
export interface AdminVerificationProject {
id: string;
title: string;
sponsorName: string;
registry: string;
methodology: string;
reductionTons: number;
stage: 'pending' | 'submitted' | 'reviewing' | 'approved' | 'rejected';
verifier?: string;
slaRemainingDays?: number;
lastUpdatedAt: string;
}
/**
* ์™ธ๋ถ€ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ(VVB) ๊ฒ€์ฆ ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก ์กฐํšŒ
*/
export async function getVerificationProjects(): Promise<AdminVerificationProject[]> {
// ์‹ค์ œ๋กœ๋Š” external_registry_mapping ํ…Œ์ด๋ธ” ๋“ฑ๊ณผ ์กฐ์ธ ํ•„์š”
// ์šฐ์„ ์€ ์บ ํŽ˜์ธ ๋ชฉ๋ก์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฒ€์ฆ ์ƒํƒœ๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜์—ฌ ๋ฐ˜ํ™˜ (์ถ”ํ›„ ํ…Œ์ด๋ธ” ํ™•์ • ์‹œ ์ˆ˜์ •)
const { data, error } = await supabase
.from('admin_program_summary')
.select('*');
if (error) return [];
return (data || []).map((row) => ({
id: row.campaign_id,
title: row.title,
sponsorName: row.sponsor_name,
registry: 'VCS', // ์ž„์‹œ ๋งคํ•‘
methodology: 'VM0042', // ์ž„์‹œ ๋งคํ•‘
reductionTons: Number(row.current_co2_kg) / 1000,
stage: row.status === 'completed' ? 'approved' : 'reviewing',
verifier: 'KQA (ํ•œ๊ตญํ’ˆ์งˆ์žฌ๋‹จ)',
slaRemainingDays: 14,
lastUpdatedAt: new Date().toISOString(),
}));
}
/**
* ํŠน์ • ์บ ํŽ˜์ธ์˜ ๊ฒ€์ฆ ํ†ต๊ณ„ ์กฐํšŒ
*/
export async function getVerificationStats(campaignId?: string) {
let query = supabase.from('admin_verification_stats').select('*');
if (campaignId) query = query.eq('campaign_id', campaignId);
const { data, error } = await query;
if (error) {
console.error('getVerificationStats error:', error);
return [];
}
return data;
}