Files
Aislo/M01_MasterData/M01_MasterData_UI_Logic_Flow_Model.ts
T
eomsangdonandClaude Opus 5 98e40f4496 feat(M01): 일위대가 로직 기본 보기를 흐름 그림으로 · 상자에서 보고 고치기
방식 C 를 테스트 컨테이너에서 꺼내 로직 화면 기본 보기로 올림(옛 호표 표는 「고급」).
상자를 누르면 값·출처에 더해 쉬운 말 풀이와 원문 표 미리보기(걸린 줄 강조)가 펼쳐짐.
자체 로직은 [고치기] 로 상자 안에서 고치고 줄을 더하거나 지움 — 정본은 「본떠 만들기」 뒤.

- UI_Test_C·_Model·_Style → UI_Logic_Flow·_Model·_Style
- UI_Test_Pick 의 재료·기계 바꿔 고르기를 UI_Logic_Pick 에 합침
- 방식 B 의 설명 카드 로직을 UI_Logic_Note 로 옮겨 흐름 그림과 같이 씀

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ
2026-09-21 09:35:33 +09:00

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_Flow_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.출처],
]),
},
],
};
}