feat(m02-df): 검증 칸을 Model designInputs · Eval breaks 로 이음 · 출력 단위 고르기(UnitPick) · 단위 표준 소수자리 제안

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr
This commit is contained in:
2026-10-01 19:52:25 +09:00
co-authored by Claude Sonnet 5.5
parent f759216242
commit 296255e357
3 changed files with 79 additions and 88 deletions
+51 -77
View File
@@ -9,10 +9,11 @@
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { createButton, createSelectField, el } from "@ui/ui_template_elements";
import { formatMoney } from "../../M01_MasterData/M01_MasterData_UI_Logic_Money";
import { breaks, type CfBreak, type CfEvalResult, type CfValues } from "./M02_DF_Eval";
import { breaks, type CfEvalOptions, type CfEvalResult, type CfValues } from "./M02_DF_Eval";
import { findMaterials, materialOf, priceOf, type CfMat } from "./M02_DF_Material";
import {
portsIn,
designInputs,
elementPort,
type CfBox,
type CfElementRef,
type CfLogic,
@@ -54,8 +55,6 @@ const TEXT = {
"이어 주지 않아서 그 로직의 예시값을 써요",
"Not linked — the logic's own sample is used",
],
NoLine: ["「{box}」 의 받는 자리 「{pill}」 에 선이 없어요", "“{pill}” of “{box}” has no line"],
NoOutput: ["출력 상자가 아직 없어요", "There is no output box yet"],
} as const;
type TextKey = keyof typeof TEXT;
@@ -78,28 +77,6 @@ export interface CfVerifyOn {
const PAGE = 10;
/** 끊긴 곳 — Model 쪽 `breaks` 가 차면 그것 · 아직이면 빈 받는 자리와 계산 오류만 */
function brokenSpots(lg: CfLogic, result: CfEvalResult, values: CfValues): CfBreak[] {
try {
return breaks(lg, values);
} catch {
const out: CfBreak[] = [];
if (!lg.boxes.some((b) => b.kind === "output"))
out.push({ kind: "오류", box: "", text: tv("NoOutput") });
for (const b of lg.boxes)
for (const p of portsIn(b))
if (!lg.links.some((l) => l.to.box === b.id && l.to.name === p.name))
out.push({
kind: "빈 받는 자리",
box: b.id,
port: p.name,
text: tv("NoLine", { box: b.name, pill: p.name }),
});
for (const e of result.errors) out.push({ kind: "오류", box: "", text: e });
return out;
}
}
/** 재료 요소 하나 고치기 — 나머지는 그대로 */
function withElement(
lg: CfLogic,
@@ -285,7 +262,7 @@ function slotRow(box: CfBox, e: CfElementRef, on: CfVerifyOn, lg: CfLogic): HTML
children: [
el("div", {
className: "m02-dfver__line",
children: [el("strong", { text: e.slot ?? e.name }), now, price, toggle],
children: [el("strong", { text: elementPort(e) }), now, price, toggle],
}),
panel,
],
@@ -325,7 +302,7 @@ function ruleRow(e: CfElementRef, lg: CfLogic, result: CfEvalResult): HTMLElemen
children: [
el("div", {
className: "m02-dfver__line",
children: [el("strong", { text: e.slot ?? e.name }), state, note],
children: [el("strong", { text: elementPort(e) }), state, note],
}),
el("span", {
className: "m02-dfver__muted",
@@ -341,7 +318,7 @@ export function renderVerify(
lg: CfLogic,
result: CfEvalResult,
on: CfVerifyOn,
opts: { values?: CfValues } = {},
opts: { values?: CfValues; logics?: CfEvalOptions["logics"] } = {},
): void {
const msg = el("p", { className: "m02-dfver__msg" });
const safe = (make: () => CfLogic): void => {
@@ -354,7 +331,7 @@ export function renderVerify(
};
/* 끊긴 곳 */
const spots = brokenSpots(lg, result, opts.values ?? {});
const spots = breaks(lg, opts.values ?? {}, { logics: opts.logics });
const spotBlock = spots.length
? [
el("div", {
@@ -383,59 +360,56 @@ export function renderVerify(
]
: [];
/* 설계 때 정할 것 */
/* 설계 때 정할 것 — Model `designInputs`(변수 · 재료 자리 · 다른 로직) + 재료 조건 자리 */
const designs = designInputs(lg);
const boxOf = (id: string): CfBox | undefined => lg.boxes.find((b) => b.id === id);
/** 변수 예시값을 그 상자(입력 · 함수)의 변수에 되적음 */
const putExample = (box: string, name: string, value: number | string): CfLogic => ({
...lg,
boxes: lg.boxes.map((x) => {
if (x.id !== box) return x;
const set = (list: CfVariable[]): CfVariable[] =>
list.map((y) => (y.name === name ? { ...y, example: value } : y));
return {
...x,
...(x.input ? { input: { ...x.input, variables: set(x.input.variables) } } : {}),
...(x.func ? { func: { ...x.func, variables: set(x.func.variables) } } : {}),
};
}),
});
const vars = designs.flatMap((d) =>
d.kind === "변수" && d.variable
? [sampleField(d.variable, (value) => safe(() => putExample(d.box, d.port, value)), msg)]
: [],
);
const slots = designs.flatMap((d) => {
const box = boxOf(d.box);
return d.kind === "재료 자리" && d.element && box
? [slotRow(box, d.element, { ...on, change: (n) => safe(() => n) }, lg)]
: [];
});
const inputs = lg.boxes.filter((b) => b.kind === "input" && b.input);
const vars = inputs.flatMap((b) =>
(b.input?.variables ?? []).map((v, i) =>
sampleField(
v,
(value) =>
safe(() => ({
...lg,
boxes: lg.boxes.map((x) =>
x.id === b.id && x.input
? {
...x,
input: {
...x.input,
variables: x.input.variables.map((y, j) =>
j === i ? { ...y, example: value } : y,
),
},
}
: x,
),
})),
msg,
),
),
);
const slots = inputs.flatMap((b) =>
(b.input?.elements ?? [])
.filter((e) => e.mode === "choice")
.map((e) => slotRow(b, e, { ...on, change: (n) => safe(() => n) }, lg)),
);
const rules = inputs.flatMap((b) =>
(b.input?.elements ?? [])
.filter((e) => e.mode === "rule" && e.rule)
.map((e) => ruleRow(e, lg, result)),
);
const logics = inputs.flatMap((b) =>
(b.input?.logic?.designs ?? [])
.filter((d) => !lg.links.some((l) => l.to.box === b.id && l.to.name === d.name))
.map((d) =>
el("li", {
children: [
createButton({
label: `${b.name} · ${d.name}${d.unit && d.unit !== "-" ? ` (${d.unit})` : ""} — ${tv("OtherLogic")}`,
variant: "ghost",
size: "sm",
disabled: !on.select,
onClick: () => on.select?.({ box: b.id, port: d.name }),
}),
],
}),
),
const logics = designs.flatMap((d) =>
d.kind === "다른 로직"
? [
el("li", {
children: [
createButton({
label: `${boxOf(d.box)?.name ?? ""} · ${d.port}${d.unit && d.unit !== "-" ? ` (${d.unit})` : ""} — ${tv("OtherLogic")}`,
variant: "ghost",
size: "sm",
disabled: !on.select,
onClick: () => on.select?.({ box: d.box, port: d.port }),
}),
],
}),
]
: [],
);
const group = (title: TextKey, body: HTMLElement[]): HTMLElement[] =>
body.length