Files
Aislo/M01_MasterData/M01_MasterData_UI_Logic_CView_Model.ts
T

239 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* M01_MasterData_UI_Logic_CView_Model.ts
* 방식 C(흐름 그림)의 뼈대 — 로직 한 줄 + 시험 계산 답 → 왼쪽에서 오른쪽 칸의 상자.
* 설계 값 → 표 찾기 → 수량 → × 단가 → 할증·덧줄 → 비목 합계 → 계
*
* 순수 — DOM·서버를 안 씀(시험이 이 파일만 떼어 Node 로 돌림).
* 값은 모두 시험 계산 답(`POST /calc`)에서 옴 — 여기서 새로 셈하지 않음.
* ========================================================================== */
import type {
CalcAnswer,
CalcLine,
ElementBrief,
HoLine,
LogicRow,
NamedFormula,
} from "./M01_MasterData_UI_Logic_Api";
export type FlowKind = "입력" | "중간" | "수량" | "단가" | "덧줄" | "비목" | "계";
/** 흐름 상자 하나 — 누르면 `detail`(값과 출처) */
export interface FlowBox {
/** 다시 그려도 같은 id — 펼친 상자를 그대로 둠 */
id: string;
kind: FlowKind;
label: string;
value: string;
note?: string;
detail: [string, string][];
/** 로직이 로직을 부르는 줄 — 그 로직 키(펼치기) */
logic?: string;
/** 비목 — 상자가 많은 로직에서 묶기·접기 */
cost?: string;
/** 값이 없어 막힌 상자 */
bad?: boolean;
}
export interface FlowColumn {
kind: FlowKind;
boxes: FlowBox[];
}
export const COSTS = ["노무비", "재료비", "경비"];
export function fmt(value: unknown): string {
if (typeof value === "number") return value.toLocaleString("ko-KR", { maximumFractionDigits: 4 });
if (value === null || value === undefined) return "";
return typeof value === "object" ? JSON.stringify(value) : String(value);
}
/** 로직 줄의 결과가 돈인지 — 돈이면 호표·비목·계, 아니면 결과 식 하나 */
export const isMoney = (row: LogicRow): boolean =>
!("결과" in row) && (row.결과단위 ?? "").startsWith("원");
/** `로직(GC000268, 기계='…')` 에서 부르는 로직 키 */
export function logicRef(element: string): string | undefined {
return /로직\(\s*([A-Z]{1,2}\w+)/.exec(element)?.[1];
}
const keep = (pairs: [string, unknown][]): [string, string][] =>
pairs.filter(([, v]) => v !== undefined && v !== null && v !== "").map(([k, v]) => [k, fmt(v)]);
/** 파일에 있으나 화면 틀에 없는 칸(입력·호표의 출처·비고) */
const extra = (item: object, name: string): string | undefined =>
(item as Record<string, string | undefined>)[name];
export function buildFlow(
row: LogicRow,
answer: CalcAnswer | null,
prices: Record<string, ElementBrief | null>,
values: Record<string, string>,
): FlowColumn[] {
const ok = answer?.ok ? answer : null;
const lines = ok?.lines ?? [];
const middle = ok?.middle ?? {};
const columns: FlowColumn[] = [inputColumn(row, values)];
const mid = middleColumn(row, middle);
if (mid.boxes.length) columns.push(mid);
if (!isMoney(row)) {
columns.push(resultColumn(row, ok?.result));
return columns;
}
const ho = row.호표 ?? [];
columns.push(qtyColumn(ho, lines), priceColumn(ho, lines, prices));
const plus = extraColumn(row.덧줄 ?? [], lines, ho.length);
if (plus.boxes.length) columns.push(plus);
const sums = ok?.sums ?? null;
columns.push(costColumn(sums, lines), totalColumn(row, sums));
return columns;
}
function inputColumn(row: LogicRow, values: Record<string, string>): FlowColumn {
const boxes = (row.입력 ?? []).map((spec, i) => ({
id: `입력:${i}`,
kind: "입력" as const,
label: spec.이름,
value: values[spec.이름] ?? "",
note: spec.단위,
detail: keep([
["단위", spec.단위],
["고르기", (spec.고르기 ?? []).join(" · ")],
["범위", spec.범위 ? `${spec.범위[0]} ${spec.범위[1]}` : ""],
["출처", extra(spec, "출처")],
]),
}));
return { kind: "입력", boxes };
}
function middleColumn(row: LogicRow, middle: Record<string, unknown>): FlowColumn {
const boxes = (row.중간 ?? []).map((step, i) => ({
id: `중간:${i}`,
kind: "중간" as const,
label: step.이름,
value: fmt(middle[step.이름]),
note: step.식.startsWith("찾기(") ? "표 찾기" : "식",
detail: keep([
["식", step.],
["출처", step.출처],
]),
}));
return { kind: "중간", boxes };
}
function qtyColumn(ho: HoLine[], lines: CalcLine[]): FlowColumn {
const boxes = ho.map((item, i) => ({
id: `수량:${i}`,
kind: "수량" as const,
label: item.이름 || item.요소,
value: lines[i] ? fmt(lines[i].수량) : "",
note: item.단위,
cost: item.비목,
detail: keep([
["식", item.수량],
["종류", item.종류],
["단위", item.단위],
["비고", extra(item, "비고")],
]),
}));
return { kind: "수량", boxes };
}
function priceColumn(
ho: HoLine[],
lines: CalcLine[],
prices: Record<string, ElementBrief | null>,
): FlowColumn {
const boxes = ho.map((item, i) => {
const line = lines[i];
const brief = prices[item.요소];
const price = line ? line.단가 : brief?.값;
return {
id: `단가:${i}`,
kind: "단가" as const,
label: item.이름 || item.요소,
value: line ? fmt(line.금액) : "",
note: price === undefined || price === null ? "단가 —" : `× ${fmt(price)}`,
cost: item.비목,
logic: logicRef(item.요소),
bad: !line && brief === null,
detail: keep([
["요소", item.요소],
["단가", price],
["금액", line?.금액],
["비목", item.비목 ?? Object.keys(line?.비목 ?? {}).join(" · ")],
["출처", line?.출처 ?? [brief?.이름, brief?.규격].filter(Boolean).join(" ")],
]),
};
});
return { kind: "단가", boxes };
}
function extraColumn(list: NamedFormula[], lines: CalcLine[], from: number): FlowColumn {
const boxes = list.map((item, i) => ({
id: `덧줄:${i}`,
kind: "덧줄" as const,
label: item.이름,
value: fmt(lines[from + i]?.금액),
note: item.비목,
cost: item.비목,
detail: keep([
["식", item.],
["비목", item.비목],
["출처", item.출처],
]),
}));
return { kind: "덧줄", boxes };
}
function costColumn(sums: Record<string, number> | null, lines: CalcLine[]): FlowColumn {
const boxes = COSTS.map((cost) => ({
id: `비목:${cost}`,
kind: "비목" as const,
label: cost,
value: fmt(sums?.[cost]),
cost,
detail: lines
.filter((line) => line.비목?.[cost] !== undefined)
.map((line) => [line.이름, fmt(line.비목[cost])] as [string, string]),
}));
return { kind: "비목", boxes };
}
function totalColumn(row: LogicRow, sums: Record<string, number> | null): FlowColumn {
return {
kind: "계",
boxes: [
{
id: "계",
kind: "계",
label: row.결과단위 || "계",
value: fmt(sums?.),
detail: keep([
...COSTS.map((c) => [c, sums?.[c]] as [string, unknown]),
["끝수", row.끝수],
["출처", row.출처],
]),
},
],
};
}
function resultColumn(row: LogicRow, result: unknown): FlowColumn {
return {
kind: "계",
boxes: [
{
id: "계",
kind: "계",
label: row.결과단위 || "결과",
value: fmt(result),
detail: keep([
["식", row.결과?.],
["출처", row.출처],
]),
},
],
};
}