// ──────────────────────────────────────────────────────────────────── // AW-020 · Settlement Statement — issue modal // ──────────────────────────────────────────────────────────────────── const STMT_DATA = { partner: 'Osaka Stay Collection', partnerId: 'PRT-2024-0048', contact: 'finance@osakastay.example', cat: 'Accommodation', type: 'B2C', period: '2026-03-01 ~ 2026-03-31', issuedAt: '2026-04-22 14:08:11 KST', stmtNo: 'STMT-B2C-2026-03-0048', settleId: 'STL-2026-03-0048', fuRows: [ { date: '2026-03-04 11:22', id: 'VCH-2026-0481011', svc: 'Standard Twin · 2박 × 2', orig: 2400, pct: 100, net: 2400 }, { date: '2026-03-09 09:48', id: 'VCH-2026-0481088', svc: 'Deluxe King · 1박', orig: 1200, pct: 100, net: 1200 }, { date: '2026-03-14 16:03', id: 'VCH-2026-0481145', svc: 'Family Suite · 3박', orig: 3600, pct: 100, net: 3600 }, { date: '2026-03-22 10:11', id: 'VCH-2026-0481209', svc: 'Standard Twin · 1박', orig: 1200, pct: 100, net: 1200 }, { date: '2026-03-28 14:55', id: 'VCH-2026-0481288', svc: 'Deluxe King · 2박', orig: 2400, pct: 100, net: 2400 }, ], fuMore: 91, fuTotal: 122400, exRows: [ { date: '2026-03-31 24:00', id: 'VCH-2026-0480200', svc: 'Standard Twin · 1박', orig: 800, pct: 30, net: 240 }, { date: '2026-03-31 24:00', id: 'VCH-2026-0480211', svc: 'Standard Twin · 1박', orig: 800, pct: 30, net: 240 }, { date: '2026-03-31 24:00', id: 'VCH-2026-0480245', svc: 'Deluxe King · 1박', orig: 800, pct: 30, net: 240 }, ], exMore: 19, exTotal: 5280, rvRows: [ { date: '2026-04-18 11:24', id: 'IVH-2026-A0480112', svc: 'Standard Twin · 1박', orig: 640, pct: 100, net: -640, srcStl: 'STL-2026-02-0048', reason: 'CANCELLED — 소비자 이의 제기 (시설 청결 불만)', memo: 'CS팀 #cs-9281 검토 중', dday: null, status: 'DISPUTED' }, { date: '2026-04-08 09:11', id: 'IVH-2026-A0480245', svc: 'Family Suite · 1박', orig: 320, pct: 100, net: -320, srcStl: 'STL-2026-02-0048', reason: 'CANCELLED — 체크인 무산', memo: '예약일 변경 협의 중', dday: 6, status: 'GRACE_PERIOD' }, { date: '2026-04-12 15:32', id: 'IVH-2026-A0480501', svc: 'Standard Twin · 1박', orig: 320, pct: 100, net: -320, srcStl: 'STL-2026-02-0048', reason: 'CANCELLED — 시설 이용 불가', memo: '파트너 응대 진행', dday: 2, status: 'GRACE_PERIOD' }, { date: '2026-04-19 10:20', id: 'IVH-2026-A0480398', svc: 'Standard Twin · 1박', orig: 320, pct: 100, net: -320, srcStl: 'STL-2026-02-0048', reason: 'CANCELLED — 소비자 철회', memo: '환불 철회 처리 (정상)', dday: null, status: 'WITHDRAWN' }, ], // computed exDeductable: 5280, // shown net (30%) exFull: 17600, // 100% face value rvTotal: -1600, fee: 0, }; // helpers const fmtMon = v => `USD ${Number(v).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; const fmtSign = v => (v < 0 ? `−USD ${Math.abs(v).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : `USD ${Number(v).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`); // ── KARE logo ─────────────────────────────────────────────────── const KareLogo = ({ size = 28, dark = false }) => (
K
KARE
); // ── Segmented control for output mode ─────────────────────────── const ModeSegment = ({ value, onChange }) => { const opts = [ { id: 'screen', label: '화면 보기', icon: }, { id: 'print', label: '인쇄', icon: }, { id: 'pdf', label: 'PDF', icon: }, { id: 'csv', label: 'CSV', icon: }, ]; return (
{opts.map(o => { const active = value === o.id; return ( ); })}
); }; // ── Section A · Statement header ─────────────────────────────── const StmtHeader = ({ d, mode }) => (
Settlement Statement
{d.type === 'B2C' ? 'Partner Settlement (B2C)' : 'Corporate Settlement (B2B)'} · {d.cat}
거래처{d.partner}
파트너 ID{d.partnerId}
정산 기간{d.period}
발행 일시{d.issuedAt}
명세서 번호{d.stmtNo}
정산 ID {d.settleId}
); const metaTd = { padding: '2px 12px 2px 0', color: 'var(--n-500)', fontFamily: 'Inter', textAlign: 'left', fontWeight: 500, whiteSpace: 'nowrap', verticalAlign: 'top' }; const metaTdV = { padding: '2px 0', color: 'var(--n-900)', fontFamily: 'Inter', fontWeight: 600 }; const metaTdVMono = { padding: '2px 0', color: 'var(--n-900)', fontFamily: 'JetBrains Mono, monospace', fontWeight: 600, fontSize: 11 }; // ── Section B · Summary box ──────────────────────────────────── const StmtSummary = ({ d, finalAmount }) => (
A. 정산 요약 (SUMMARY)
); const SummaryCell = ({ label, sub, amount, count, negative, primary, big, note }) => (
{label}
{sub &&
{sub}
}
{fmtSign(amount)}
{count != null && (
{count}건{note ? ` · ${note}` : ''}
)} {note && count == null && (
{note}
)}
); // ── Section C · Voucher detail table ─────────────────────────── const StmtTable = ({ d, mode }) => (
B. VOUCHER 명세 (LINE ITEMS)
통화: USD · 정렬: 처리 일시 ↑
{/* FULLY_USED group */} {d.fuRows.map((r, i) => )} {d.fuMore > 0 && s + r.net, 0)}/>} {/* EXPIRED group */} {d.exRows.map((r, i) => )} {d.exMore > 0 && s + r.net, 0)}/>} {/* REVERSAL group */} {d.rvRows.length > 0 && ( <> {d.rvRows.map((r, i) => )} )} {/* Grand total */}
처리 일시 Voucher ID Item · 서비스 원금 (Face) 조정율 정산액 (Net)
합계 (Grand Total) {fmtSign(d.fuTotal + d.exDeductable + d.rvTotal)}
); const GroupHeader = ({ label, count, subtotal, dot, reversal }) => ( {label} {count}건 소계 {fmtSign(subtotal)} ); const StmtRow = ({ r, idx, mode }) => ( {r.date} {r.id} {r.svc} {fmtMon(r.orig)} {r.pct}% {fmtMon(r.net)} ); const StmtRowReversal = ({ r, idx, mode }) => ( <> {r.date} {r.id} {mode === 'print' && (REVERSAL)} {r.svc} {fmtMon(r.orig)} {fmtSign(r.net)} 원본 정산: {r.srcStl} · 사유: {r.reason} {r.dday != null && ( <> · 유예 D-{r.dday} )} · 관리자 메모: {r.memo} ); const MoreRow = ({ count, amount }) => ( 외 {count}건 (생략) · 전체는 PDF/CSV에서 확인 {fmtMon(amount)} ); const stmtTable = { width: '100%', borderCollapse: 'collapse', fontFamily: 'Inter', fontSize: 11.5 }; const stmtTh = { textAlign: 'left', padding: '8px 8px', fontFamily: 'Inter', fontSize: 10.5, fontWeight: 700, color: 'var(--n-700)', letterSpacing: '0.04em' }; const stmtThR = { textAlign: 'right' }; const stmtTd = { padding: '8px 8px', color: 'var(--n-700)', verticalAlign: 'middle' }; const stmtTdR = { padding: '8px 8px', color: 'var(--n-900)', textAlign: 'right', fontFamily: 'JetBrains Mono, monospace', fontVariantNumeric: 'tabular-nums' }; // ── Section D · REVERSAL roll-forward ────────────────────────── const ReversalRoll = ({ d, finalAmount }) => { const positive = d.fuTotal + d.exDeductable; return (
C. REVERSAL 역산 내역 (ROLL-FORWARD)
{d.rvRows.length}건 · {fmtSign(d.rvTotal)}
{/* Equation */}
정산 대상 (FU + EX) {fmtMon(positive)}
REVERSAL 확정 차감 {fmtMon(Math.abs(d.rvTotal))}
=
최종 지급액 {fmtMon(finalAmount)}
유예 중 잔여 2건 (다음 기로 이월)
{/* Per-item roll list */} {d.rvRows.map((r, i) => ( ))}
역산 IVH ID 원본 정산 상태 / 유예 차감액
{r.id} {r.srcStl} {fmtSign(r.net)}
); }; const RvStatusInline = ({ status, dday }) => { const map = { GRACE_PERIOD: { color: '#92400E', bg: '#FFFBEB', border: '#FDE68A', label: `유예 중 D-${dday}` }, DISPUTED: { color: '#6D28D9', bg: '#F5F3FF', border: '#DDD6FE', label: '이의 제기 (DSP)' }, WITHDRAWN: { color: '#6B7280', bg: '#F3F4F6', border: '#D1D5DB', label: '철회' }, CONFIRMED: { color: '#991B1B', bg: '#FEF2F2', border: '#FCA5A5', label: '확정' }, }; const s = map[status]; return ( {s.label} ); }; // ── Section E · Footer notes (always shown on doc) ───────────── const StmtNotes = ({ d, mode }) => (
D. 안내 (NOTES)
KARE Settlement System · BD-007 · v1.2 · 본 문서는 KARE Inc. 와 {d.partner} 간 정산 참조 자료입니다.
Page 1 / 1 · 발행자: 김운영 (admin@kare.example)
); // ── CSV preview ──────────────────────────────────────────────── const CsvPreview = ({ d }) => { const lines = [ `# KARE Settlement Statement — ${d.partner}`, `# stmt_no=${d.stmtNo} | period=${d.period} | issued_at=${d.issuedAt}`, `# encoding=UTF-8 BOM | delimiter=, | quote="`, ``, `processed_at,voucher_id,group,service,face_amount_usd,adjust_pct,net_amount_usd,reversal_source,reason`, ...d.fuRows.slice(0, 3).map(r => `${r.date},${r.id},FULLY_USED,"${r.svc}",${r.orig.toFixed(2)},${r.pct},${r.net.toFixed(2)},,`), `# ... ${d.fuRows.length + d.fuMore - 3} more FULLY_USED rows`, ...d.exRows.slice(0, 2).map(r => `${r.date},${r.id},EXPIRED,"${r.svc}",${r.orig.toFixed(2)},${r.pct},${r.net.toFixed(2)},,`), `# ... ${d.exRows.length + d.exMore - 2} more EXPIRED rows`, ...d.rvRows.slice(0, 2).map(r => `${r.date},${r.id},REVERSAL,"${r.svc}",${r.orig.toFixed(2)},,-${Math.abs(r.net).toFixed(2)},${r.srcStl},"${r.reason}"`), `# ... ${d.rvRows.length - 2} more REVERSAL rows`, ``, `# SUMMARY`, `# fully_used_total,${d.fuTotal.toFixed(2)}`, `# expired_total,${d.exDeductable.toFixed(2)}`, `# reversal_total,${d.rvTotal.toFixed(2)}`, `# final_payout,${(d.fuTotal + d.exDeductable + d.rvTotal).toFixed(2)}`, ]; return (
CSV PREVIEW UTF-8 BOM · ,(comma) · 첫 처리 항목 일부 + 요약 메타 라인 포함
{lines.map((l, i) => (
        
{i + 1} {l || ' '}
))}
); }; // ── Main statement document ──────────────────────────────────── const StatementDoc = ({ d, mode, error }) => { const finalAmount = d.fuTotal + d.exDeductable + d.rvTotal; if (mode === 'csv') { return ; } return (
{d.rvRows.length > 0 && } {mode === 'pdf' && (
PDF PREVIEW · A4
)}
); }; // ── Modal frame ──────────────────────────────────────────────── const StatementIssueModal = ({ variant = 'screen' }) => { const d = variant === 'no-rv' ? { ...STMT_DATA, rvRows: [], rvTotal: 0 } : STMT_DATA; const error = variant === 'error'; const isPrint = variant === 'print'; const isPdf = variant === 'pdf'; const isCsv = variant === 'csv'; const mode = isPrint ? 'print' : isPdf ? 'pdf' : isCsv ? 'csv' : 'screen'; const [fmt, setFmt] = React.useState('PDF'); // PRINT → no chrome if (isPrint) { return (
{/* Print marker */}
PRINT MODE · chrome 제거 · 흑백 우선
); } return (
{/* Header */}
정산 집계 명세서 발행
정산 명세서 — {d.period} · {d.partner}
{}}/>
{/* Body — paper background */}
{error ? ( ) : (
)}
{/* Footer */}
KARE_Settlement_{d.type}_{d.partnerId.replace('PRT-', '')}_2026-03.{fmt.toLowerCase()} · 명세서 SHA: a8f3…2d11 · ~ 184 KB
SETTLED 상태 — 발행 가능
); }; // ── Error state ──────────────────────────────────────────────── const ErrorState = () => (
ERR-016
명세서 생성에 실패했습니다
PDF 렌더링 워커가 응답하지 않습니다 (timeout 30s · 재시도 3회 실패). 잠시 후 다시 시도하거나, CSV 형식으로 먼저 다운로드하세요.
RUN ID
job-stmt-2026-04-22-1408-0048 · worker-pdf-04 · ECONNRESET
); const StatementIssueFrame = ({ variant }) => (
); Object.assign(window, { StatementIssueModal, StatementIssueFrame });