출력 상자의 받은 값 · 비용 종류 · 단위 · 이름 판, 원가 합계(노무비 · 재료비 · 경비 · 계) · 출력 상자별 줄 · 값 출력 · 빈 받는 자리 · 오류 · 예시값. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr
196 lines
6.6 KiB
TypeScript
196 lines
6.6 KiB
TypeScript
/* =============================================================================
|
|
* M02_CF_OutputPanel.ts
|
|
* 단가산출 다이어그램 — 출력 상자 판. 출력 상자를 고르면 아래에 뜸
|
|
* 받은 값(어디서 왔나 · 값 · 단위) · 비용 종류 고르기(노무비 · 재료비 · 경비 · 값(비용 아님)) · 단위 · 이름
|
|
* 모양은 docs/raw/M01_쉬운단가/원가다이어그램_정함.md 「상자 · 선 약속」 · Model 타입 그대로. 이 폴더째 지워도 됨.
|
|
* ========================================================================== */
|
|
|
|
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
|
import { createSelectField, el } from "@ui/ui_template_elements";
|
|
import { formatMoney } from "../../M01_MasterData/M01_MasterData_UI_Logic_Money";
|
|
import { evalLogic, type CfEvalResult, type CfValue } from "./M02_CF_Eval";
|
|
import { portsOut, type CfBox, type CfCost, type CfLogic } from "./M02_CF_Model";
|
|
import "./M02_CF_OutputPanel.css";
|
|
|
|
const TEXT = {
|
|
Title: ["출력", "Output"],
|
|
Name: ["이름", "Name"],
|
|
Cost: ["비용 종류", "Cost kind"],
|
|
Labor: ["노무비", "Labor cost"],
|
|
Material: ["재료비", "Material cost"],
|
|
Expense: ["경비", "Expense"],
|
|
Plain: ["값(비용 아님)", "Value (not a cost)"],
|
|
Unit: ["단위", "Unit"],
|
|
UnitHint: ["예: 원 · ㎥ · 시간 · -", "e.g. KRW · ㎥ · h · -"],
|
|
Got: ["받은 값", "Received value"],
|
|
From: ["어디서", "From"],
|
|
Value: ["값", "Value"],
|
|
NoLink: [
|
|
"아직 이어진 선이 없어요 — 입력이나 함수 상자에서 이 상자로 선을 이어 주세요",
|
|
"Nothing is linked yet — draw a line from an input or function box",
|
|
],
|
|
Table: ["표", "Table"],
|
|
Calc: ["계산 중이 아니에요", "Not calculated"],
|
|
Cant: ["지금은 바꿀 수 없어요", "Cannot change this now"],
|
|
PlainHint: [
|
|
"원가 합계에 안 넣고 값으로만 보여요",
|
|
"Shown as a value, not added to the cost total",
|
|
],
|
|
} as const;
|
|
|
|
type TextKey = keyof typeof TEXT;
|
|
const to = (key: TextKey): string => TEXT[key][currentLanguageIndex === 1 ? 1 : 0];
|
|
|
|
const COSTS: [CfCost, TextKey][] = [
|
|
["노무비", "Labor"],
|
|
["재료비", "Material"],
|
|
["경비", "Expense"],
|
|
["값", "Plain"],
|
|
];
|
|
|
|
/** 값 한 칸의 글 — 수 · 글 · 참거짓 · 표 */
|
|
export function showValue(v: CfValue | undefined, money = false): string {
|
|
if (v === undefined || v === null) return "—";
|
|
if (Array.isArray(v)) return to("Table");
|
|
if (typeof v === "number")
|
|
return money ? formatMoney(v) : v.toLocaleString("ko-KR", { maximumFractionDigits: 6 });
|
|
return String(v);
|
|
}
|
|
|
|
const withBox = (lg: CfLogic, id: string, edit: (b: CfBox) => CfBox): CfLogic => ({
|
|
...lg,
|
|
boxes: lg.boxes.map((b) => (b.id === id ? edit(b) : b)),
|
|
});
|
|
|
|
/** 출력 상자 판 — `opts.result` 가 있으면 그 값으로 「받은 값」을 보임(없으면 지금 계산을 한 번 해 봄) */
|
|
export function renderOutputPanel(
|
|
host: HTMLElement,
|
|
lg: CfLogic,
|
|
boxId: string,
|
|
on: { change(lg: CfLogic): void },
|
|
opts: { result?: CfEvalResult } = {},
|
|
): void {
|
|
const box = lg.boxes.find((b) => b.id === boxId);
|
|
if (!box || box.kind !== "output") {
|
|
host.replaceChildren();
|
|
return;
|
|
}
|
|
const msg = el("p", { className: "m01-cfout__msg" });
|
|
const emit = (make: () => CfLogic): void => {
|
|
msg.textContent = "";
|
|
try {
|
|
on.change(make());
|
|
} catch {
|
|
msg.textContent = to("Cant");
|
|
}
|
|
};
|
|
const cost = box.output?.cost ?? "노무비";
|
|
|
|
/* 받은 값 — 이 상자로 들어오는 선 하나(받는 자리는 하나) */
|
|
const link = lg.links.find((l) => l.to.box === box.id);
|
|
const from = link && lg.boxes.find((b) => b.id === link.from.box);
|
|
let result = opts.result;
|
|
if (!result && link) {
|
|
try {
|
|
result = evalLogic(lg);
|
|
} catch {
|
|
result = undefined;
|
|
}
|
|
}
|
|
let unit = "";
|
|
if (from && link) {
|
|
try {
|
|
unit = portsOut(from).find((p) => p.name === link.from.name)?.unit ?? from.unit;
|
|
} catch {
|
|
unit = from.unit;
|
|
}
|
|
}
|
|
const got = el("div", { className: "m01-cfout__got" });
|
|
if (link && from) {
|
|
const value = result?.ports[from.id]?.[link.from.name] ?? result?.outputs[box.id];
|
|
got.replaceChildren(
|
|
el("strong", { text: to("Got") }),
|
|
el("dl", {
|
|
children: [
|
|
el("dt", { text: to("From") }),
|
|
el("dd", { text: `${from.name} · ${link.from.name}` }),
|
|
el("dt", { text: to("Value") }),
|
|
el("dd", {
|
|
className: "m01-cfout__num",
|
|
text: `${showValue(value, cost !== "값")}${unit && unit !== "-" ? ` ${unit}` : ""}`,
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
} else {
|
|
got.replaceChildren(
|
|
el("strong", { text: to("Got") }),
|
|
el("p", { className: "m01-cfout__warn", text: to("NoLink") }),
|
|
);
|
|
}
|
|
|
|
/* 이름 · 비용 종류 · 단위 */
|
|
const nameBox = el("input", {
|
|
className: "m01-cfout__input",
|
|
attrs: { type: "text", "aria-label": to("Name") },
|
|
});
|
|
nameBox.value = box.name;
|
|
nameBox.addEventListener("change", () => {
|
|
const next = nameBox.value.trim();
|
|
if (!next || next === box.name) {
|
|
nameBox.value = box.name;
|
|
return;
|
|
}
|
|
emit(() => withBox(lg, box.id, (b) => ({ ...b, name: next })));
|
|
});
|
|
const costField = createSelectField({
|
|
label: to("Cost"),
|
|
options: COSTS.map(([value, key]) => ({ value, text: to(key) })),
|
|
value: cost,
|
|
compact: true,
|
|
onChange: (v) =>
|
|
emit(() =>
|
|
withBox(lg, box.id, (b) => ({
|
|
...b,
|
|
output: { unit: b.output?.unit ?? b.unit, cost: v as CfCost },
|
|
})),
|
|
),
|
|
});
|
|
const unitBox = el("input", {
|
|
className: "m01-cfout__input",
|
|
attrs: { type: "text", placeholder: to("UnitHint"), "aria-label": to("Unit") },
|
|
});
|
|
unitBox.value = box.output?.unit ?? box.unit;
|
|
unitBox.addEventListener("change", () => {
|
|
const next = unitBox.value.trim() || "-";
|
|
emit(() =>
|
|
withBox(lg, box.id, (b) => ({
|
|
...b,
|
|
unit: next,
|
|
output: { cost: b.output?.cost ?? "노무비", unit: next },
|
|
})),
|
|
);
|
|
});
|
|
const field = (label: string, control: HTMLElement): HTMLElement =>
|
|
el("label", {
|
|
className: "m01-cfout__field",
|
|
children: [el("span", { text: label }), control],
|
|
});
|
|
|
|
host.replaceChildren(
|
|
el("strong", { className: "m01-cfout__title", text: `${to("Title")} · ${box.name}` }),
|
|
el("div", {
|
|
className: "m01-cfout__body",
|
|
children: [
|
|
got,
|
|
el("div", {
|
|
className: "m01-cfout__fields",
|
|
children: [field(to("Name"), nameBox), costField.root, field(to("Unit"), unitBox)],
|
|
}),
|
|
],
|
|
}),
|
|
...(cost === "값" ? [el("p", { className: "m01-cfout__muted", text: to("PlainHint") })] : []),
|
|
msg,
|
|
);
|
|
}
|