feat(714-2): 공용 단가산출 계산서 틀 — A00_Common/calc_sheet.ts(704-2 계산서 줄 올림 · 묶음 머리 변수 칸 · 바꾸면 onInput) · M02 계산서 보기 · M01 「인력·재료·경비」 보기가 한 벌로 씀(장비 한 줄 · 기준단위 · 환산 근거는 714-1 뒤)
- 704-2 Labor_Sheet 줄 · 소계 · 계 = calcSheetRows(표본 셋 DOM 전후 같음) - 새 M01_MasterData_UI_LogicLab_Est.ts — /text 묶음 그대로 · 묶음이 쓰는 시험 입력(714-3 usedInputs) = 머리 변수 칸 · 「준비 중」 자리 걷음 - 낡은 시험 test_704_logic_standard 고침 — 계산서가 계산 줄 부품을 공용 계산서 안에서 씀(import 자리만 바뀜) - 새 지킴 시험 test_714_2_calc_sheet Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
/* =============================================================================
|
||||
* 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<string, string>;
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user