import 'server-only'; import type { PointAccount, PointHistoryEntry, } from '@/lib/domain/point-account'; import type { PointAccountQuery } from '@/lib/domain/point-account-query'; // TODO(백엔드): 누적포인트 API가 아직 없다. 아래 mock 생성기를 지우고 다른 Repository처럼 // backendFetch + getSessionAccessToken으로 교체한다. // 이슈: 구분(사이트)은 기획대로 FOX PAY App. 하나뿐이다 — 확장은 백엔드가 사이트를 실어 준 뒤다. const TOTAL_ACCOUNTS = 3290; const SITE_NAME = 'FOX PAY App.'; const SCHOOL_NAMES = [ '폭스고등학교', '여우고등학교', '한빛고등학교', '새롬고등학교', ]; const EVENT_TITLES = ['EVENT-1', 'EVENT-2', 'EVENT-3']; const BASE_DATE = Date.UTC(2026, 1, 5); const DAY_MS = 24 * 60 * 60 * 1000; function dateAt(offsetDays: number): string { return new Date(BASE_DATE - offsetDays * DAY_MS).toISOString().slice(0, 10); } function timestampAt(index: number): string { const hour = 9 + (index % 9); const minute = (index * 7) % 60; const pad = (value: number) => String(value).padStart(2, '0'); return `${dateAt(index % 400)} ${pad(hour)}:${pad(minute)}`; } /** 인덱스로만 만든다 — 난수를 쓰면 새로고침마다 목록이 흔들린다. */ function buildAccount(index: number): PointAccount { const serial = TOTAL_ACCOUNTS - index; const totalPoint = 120 + ((index * 37) % 880); const deductedPoint = (index * 13) % 60; const usedPoint = (index * 29) % 300; return { id: `PA-${String(serial).padStart(5, '0')}`, memberNo: `NO_${String(10000 + serial)}`, loginId: `user_${String(serial).padStart(4, '0')}`, studentName: `학생${serial}`, schoolName: SCHOOL_NAMES[index % SCHOOL_NAMES.length], grade: (index % 3) + 1, classNo: (index % 10) + 1, studentNo: (index % 30) + 1, totalPoint, deductedPoint, usedPoint, currentPoint: totalPoint - deductedPoint - usedPoint, updatedAt: timestampAt(index), siteName: SITE_NAME, }; } const ACCOUNTS: PointAccount[] = Array.from( { length: TOTAL_ACCOUNTS }, (_, index) => buildAccount(index) ); function matchesQuery(account: PointAccount, query: PointAccountQuery): boolean { if ( query.schoolName && !account.schoolName.toLowerCase().includes(query.schoolName.toLowerCase()) ) { return false; } if (query.grade !== null && account.grade !== query.grade) { return false; } if (query.classNo !== null && account.classNo !== query.classNo) { return false; } if (query.studentNo !== null && account.studentNo !== query.studentNo) { return false; } if (query.keyword) { const needle = query.keyword.toLowerCase(); const haystack = query.searchField === 'loginId' ? account.loginId : account.studentName; if (!haystack.toLowerCase().includes(needle)) { return false; } } return true; } /** 기획 PTS10200001 ② — 최근 수정일시로 정렬. */ function byUpdatedAtDesc(a: PointAccount, b: PointAccount): number { return b.updatedAt.localeCompare(a.updatedAt); } export async function fetchPointAccounts( query: PointAccountQuery ): Promise<{ items: PointAccount[]; totalCount: number }> { const filtered = ACCOUNTS.filter((account) => matchesQuery(account, query)); filtered.sort(byUpdatedAtDesc); const start = (query.page - 1) * query.pageSize; return { items: filtered.slice(start, start + query.pageSize), totalCount: filtered.length, }; } export async function fetchPointAccount( id: string ): Promise { return ACCOUNTS.find((account) => account.id === id) ?? null; } function buildHistory(account: PointAccount): PointHistoryEntry[] { const seed = Number(account.id.slice(3)); const length = 12 + (seed % 9); return Array.from({ length }, (_, index) => { const isPurchase = (seed + index) % 4 === 3; return { id: `${account.id}-H${String(index + 1).padStart(3, '0')}`, title: isPurchase ? `배지 이미지 구입 - BADGE_${String(((seed + index) % 20) + 1).padStart(3, '0')}` : EVENT_TITLES[(seed + index) % EVENT_TITLES.length], point: isPurchase ? -100 : 10, siteName: SITE_NAME, date: dateAt(index), }; }); } /** 기획 PTS10300001 ② — 최근 날짜순 정렬. */ export async function fetchPointHistory( accountId: string, page: number, pageSize: number ): Promise<{ items: PointHistoryEntry[]; totalCount: number }> { const account = await fetchPointAccount(accountId); if (!account) { return { items: [], totalCount: 0 }; } const entries = buildHistory(account); entries.sort((a, b) => b.date.localeCompare(a.date)); const start = (page - 1) * pageSize; return { items: entries.slice(start, start + pageSize), totalCount: entries.length, }; }