/* ============================================================================= * M02_DF_Round.ts * 끝수 기본값 — 금액 단위 표준(산림품셈 1-2-2 「금액의 단위표준」)을 읽어 * 출력(비용) 기본 = 일위대가표 금액란 · 원가 합계 기본 = 일위대가표 계금 (모두 미만 버림) * 표 읽기 = 서버 `GET /api/m02/df/standards`(Api `cfStandards`) → {unit, money, saved} * 모양은 docs/raw/M01_쉬운단가/흐름도_정함.md 11 · 12. 이 폴더째 지워도 됨. * ========================================================================== */ import { cfStandards } from "./M02_DF_Api"; import type { CfRound } from "./M02_DF_Model"; /** 표 한 줄 — 원문 그대로(종목 · 단위 · 지위 · 비고) */ export interface CfMoneyRow { 종목: string; 단위?: string; 지위: number; 비고?: string; } /** 표 모양 둘(`{줄: […]}` · 줄 목록)을 줄 목록으로 */ const rowsOf = (x: unknown): CfMoneyRow[] => { const list = Array.isArray(x) ? x : (x as { 줄?: unknown } | null)?.줄; return Array.isArray(list) ? (list as CfMoneyRow[]) : []; }; let loaded: Promise | null = null; /** 금액 단위 표준 줄 — 서버가 고친 것(`_data`)이 있으면 그것 · 없으면 처음 값을 줌 */ export function loadMoneyStandard(): Promise { loaded ??= cfStandards().then( (s) => rowsOf(s.money), () => { loaded = null; // 못 읽었으면 다음에 다시 — 지금은 정해 둔 기본값으로 return []; }, ); return loaded; } /** 표를 다시 읽게(표를 고친 뒤) */ export function resetStandard(): void { loaded = null; } /** 표 줄 → 끝수 — 지위 0.1 = 소수 1자리 · 1 이상 = 0자리 · 비고 「미만버림」 */ export function roundOf(row: CfMoneyRow): CfRound { const step = row.지위 > 0 ? row.지위 : 1; const method = /올림/.test(row.비고 ?? "") && !/버림/.test(row.비고 ?? "") ? "올림" : /반올림/.test(row.비고 ?? "") ? "반올림" : "버림"; return { places: step >= 1 ? 0 : Math.round(-Math.log10(step)), method }; } const find = (rows: CfMoneyRow[], word: string): CfMoneyRow | undefined => rows.find((r) => r.종목.includes("일위대가표") && r.종목.includes(word)); /** 출력(비용) 기본 — 일위대가표 금액란 0.1원 미만 버림 */ export async function defaultItemRound(): Promise { const row = find(await loadMoneyStandard(), "금액란"); return row ? roundOf(row) : { places: 1, method: "버림" }; } /** 원가 합계 기본 — 일위대가표 계금 1원 미만 버림 */ export async function defaultTotalRound(): Promise { const row = find(await loadMoneyStandard(), "계금"); return row ? roundOf(row) : { places: 0, method: "버림" }; } /** 끝수 적용 — 소수 자리 · 방법(부동소수 오차는 아주 작은 값으로 눌러 줌) */ export function roundBy(r: CfRound, v: number): number { const k = 10 ** r.places; const x = v * k; const eps = Math.abs(x) * 1e-12 + 1e-9; const f = r.method === "올림" ? Math.ceil(x - eps) : r.method === "반올림" ? Math.round(x + eps) : Math.floor(x + eps); return f / k; } /** 「0.1원 미만 버림」 꼴 쉬운 말 */ export function roundText(r: CfRound): string { const step = r.places > 0 ? Number((10 ** -r.places).toFixed(r.places)) : 1; return `${step}원 미만 ${r.method}`; } /** 출력 기본 끝수(표를 읽어 오기 전에는 금액란 0.1원 미만 버림) — 화면이 바로 쓰는 값 */ let itemNow: CfRound = { places: 1, method: "버림" }; void defaultItemRound().then((r) => (itemNow = r)); export const itemRoundNow = (): CfRound => itemNow; /** 끝수대로 자른 값을 그 자리 수만큼 보임(3,216.6) — 끝수가 없으면 소수 여섯째 자리까지 */ export function showRounded(v: number, r?: CfRound): string { if (!r) return v.toLocaleString("ko-KR", { maximumFractionDigits: 6 }); return roundBy(r, v).toLocaleString("ko-KR", { minimumFractionDigits: 0, maximumFractionDigits: Math.max(0, r.places), }); } /** 원가 합계 기본 끝수(표를 읽어 오기 전에는 계금 1원 미만 버림) — 화면이 바로 쓰는 값 */ let totalNow: CfRound = { places: 0, method: "버림" }; void defaultTotalRound().then((r) => (totalNow = r)); export const totalRoundNow = (): CfRound => totalNow; /** 그 로직의 합계 끝수 — 비용 출력 상자에 끝수가 실려 있으면 그것(옛 로직을 옮긴 흐름은 자신의 끝수) · * 없으면 기본(새 로직 = 일위대가표 계금 1원 미만 버림) */ export function totalRoundOf( lg: { boxes: { kind: string; output?: { cost: string; round?: CfRound } }[] }, fallback: CfRound, ): CfRound { const hit = lg.boxes.find( (b) => b.kind === "output" && b.output?.cost !== "값" && b.output?.round, ); return hit?.output?.round ?? fallback; }