/* ============================================================================= * calc_sheet.ts * 공용 「단가산출 계산서」(PLAN 714-2 · 요구서 내역12 4장 5) — 704-2 M02 계산서 보기를 올린 것. * 쓰는 곳 = M01 「인력·재료·경비」 보기 · M02 기준 목록 계산서 보기(704-2) · (뒤에) 일위대가 모달 · B09 단가산출근거 * 꼴 = 묶음마다 [머리(변수 칸이 있을 때만) · 쓰는 입력 값 줄] · 식 줄(이름 | 단가 × 품 | 금액) · 소계 · 맨 아래 계 * 값 = 서버 셈 그대로 찍음(화면에서 다시 셈 안 함) · 변수 칸 바꾸면 `onInput(이름, 값)` — 부르는 쪽이 다시 셈 * 부품 = `ui_template_calc_row`(값 줄 · 식 줄) · 입력 · 고르기 · 글 — 날 요소 · 페이지 CSS 없음 * ========================================================================== */ import { createCalcFormula, createCalcFormulaRow, createCalcRow } from "@ui/ui_template_calc_row"; import { el } from "@ui/ui_template_elements"; import { createInputField, createSelectField } from "@ui/ui_template_input"; import { createText } from "@ui/ui_template_text"; export interface SheetLine { 이름: string; /** 서버 값 식 「단가 * 품 = 금액」 — 「= 금액」 은 떼고 「*」 → 「×」 */ 글: string; 금액: number | null; } export interface SheetGroup { 비목: string; 소계: number | null; 끝수?: string; 줄: SheetLine[]; } export interface SheetInput { 이름: string; 단위?: string; 고르기?: (string | number)[]; } export interface SheetOptions { 묶음: SheetGroup[]; 계: number | null; /** 맨 아래 계 줄 이름(「계 (원/㎡)」) */ sumLabel: string; /** 변수 칸 — 입력 꼴 · 지금 값 · 묶음이 쓰는 입력 이름 · 바꾼 뒤 할 일(없으면 변수 칸 없음) */ inputs?: SheetInput[]; values?: Record; uses?: (group: SheetGroup, index: number) => string[]; onInput?: (name: string, value: string) => void; } const won = (v: number | null, digits = 0): string => v === null ? "-" : v.toLocaleString("ko-KR", { maximumFractionDigits: digits }); const out = (v: number | null, key = false, digits = 0): HTMLElement => el("span", { className: `ui-calc-row__out${key ? " is-key" : ""}`, text: won(v, digits) }); /** 값 식 → 식 칸 글 — 「268,908 * 0.12 = 32,269」 → 「268,908 × 0.12」 */ export const sheetFormula = (text: string): string => text.replace(/ = [^=]*$/, "").replaceAll(" * ", " × "); /** 변수 칸 하나 — 고르기 있으면 고르기 · 아니면 글 칸(Enter · 떠남 때 넘김) */ function varRow(spec: SheetInput, value: string, onInput: (v: string) => void): HTMLElement { let control: HTMLElement; if (spec.고르기?.length) { control = createSelectField({ options: spec.고르기.map((o) => ({ value: String(o), text: String(o) })), value, compact: true, ariaLabel: spec.이름, onChange: onInput, }).root; } else { const box = createInputField({ value, compact: true }); box.input.setAttribute("aria-label", spec.이름); box.input.addEventListener("change", () => { if (box.input.value.trim() && box.input.value !== value) onInput(box.input.value.trim()); }); control = box.root; } const row = createCalcRow(spec.단위 ? `${spec.이름} (${spec.단위})` : spec.이름, control); row.dataset.sheetVar = spec.이름; return row; } /** 계산서 줄들 — 부르는 쪽 세로 칸(flex column)에 그대로 붙임 */ export function calcSheetRows(o: SheetOptions): HTMLElement[] { const rows: HTMLElement[] = []; const specs = new Map((o.onInput ? (o.inputs ?? []) : []).map((s) => [s.이름, s])); o.묶음.forEach((g, gi) => { const vars = (o.uses?.(g, gi) ?? []).filter((n) => specs.has(n)); if (vars.length && o.onInput) { const onInput = o.onInput; const head = createText(g.비목, "muted", 14); head.dataset.sheetHead = g.비목; rows.push(head); for (const n of vars) rows.push(varRow(specs.get(n)!, o.values?.[n] ?? "", (v) => onInput(n, v))); } for (const line of g.줄) rows.push( createCalcFormulaRow( line.이름, createCalcFormula(sheetFormula(line.글)), out(line.금액, false, 1), ), ); rows.push( createCalcFormulaRow(`${g.비목}${g.끝수 ? ` (${g.끝수})` : ""}`, null, out(g.소계, true)), ); }); rows.push(createCalcFormulaRow(o.sumLabel, null, out(o.계, true))); return rows; }