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:
@@ -11,6 +11,7 @@ import { formatMoney } from "../../M01_MasterData/M01_MasterData_UI_Logic_Money"
|
||||
import { evalLogic, type CfEvalResult, type CfValue } from "./M02_DF_Eval";
|
||||
import { portsOut, type CfBox, type CfCost, type CfLogic, type CfRound } from "./M02_DF_Model";
|
||||
import { defaultItemRound, roundText } from "./M02_DF_Round";
|
||||
import { createUnitPick, suggestPlaces } from "./M02_DF_UnitPick";
|
||||
import "./M02_DF_OutputPanel.css";
|
||||
|
||||
const TEXT = {
|
||||
@@ -22,7 +23,6 @@ const TEXT = {
|
||||
Expense: ["경비", "Expense"],
|
||||
Plain: ["값(비용 아님)", "Value (not a cost)"],
|
||||
Unit: ["단위", "Unit"],
|
||||
UnitHint: ["예: 원 · ㎥ · 시간 · -", "e.g. KRW · ㎥ · h · -"],
|
||||
Got: ["받은 값", "Received value"],
|
||||
From: ["어디서", "From"],
|
||||
Value: ["값", "Value"],
|
||||
@@ -39,6 +39,8 @@ const TEXT = {
|
||||
RoundDefault: ["끝수 기본", "Default rounding"],
|
||||
RoundStd: ["일위대가표 금액란", "unit-price sheet amount column"],
|
||||
RoundNone: ["끝수 없음(값 그대로)", "No rounding (value as is)"],
|
||||
Suggest: ["단위 표준 제안", "Unit standard suggests"],
|
||||
Apply: ["적용", "Apply"],
|
||||
RoundReset: ["기본으로", "Back to default"],
|
||||
RoundUser: ["고른 끝수", "Chosen rounding"],
|
||||
PlainHint: [
|
||||
@@ -165,13 +167,7 @@ export function renderOutputPanel(
|
||||
})),
|
||||
),
|
||||
});
|
||||
const unitBox = el("input", {
|
||||
className: "m02-dfout__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() || "-";
|
||||
const setUnit = (next: string): void =>
|
||||
emit(() =>
|
||||
withBox(lg, box.id, (b) => ({
|
||||
...b,
|
||||
@@ -179,7 +175,27 @@ export function renderOutputPanel(
|
||||
output: { ...b.output, cost: b.output?.cost ?? "노무비", unit: next },
|
||||
})),
|
||||
);
|
||||
const unitBox = createUnitPick({
|
||||
value: box.output?.unit ?? box.unit,
|
||||
ariaLabel: to("Unit"),
|
||||
extra: lg.resultUnit.includes("/") ? [lg.resultUnit.split("/")[0]] : [],
|
||||
onPick: setUnit,
|
||||
});
|
||||
/** 단위 표준의 소수자리 제안(주 ① 반올림) — 값(비용 아님)이고 끝수를 안 정했을 때 */
|
||||
const suggestNote = el("span", { className: "m02-dfout__muted" });
|
||||
if (cost === "값" && !box.output?.round)
|
||||
void suggestPlaces(box.output?.unit ?? box.unit).then((p) => {
|
||||
if (p === null) return;
|
||||
suggestNote.textContent = `${to("Suggest")}: ${to("RoundPlaces")} ${p} · 반올림`;
|
||||
suggestNote.append(
|
||||
createButton({
|
||||
label: to("Apply"),
|
||||
size: "sm",
|
||||
variant: "ghost",
|
||||
onClick: () => setRound({ places: p, method: "반올림" }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
const field = (label: string, control: HTMLElement): HTMLElement =>
|
||||
el("label", {
|
||||
className: "m02-dfout__field",
|
||||
@@ -253,6 +269,7 @@ export function renderOutputPanel(
|
||||
],
|
||||
}),
|
||||
roundNote,
|
||||
suggestNote,
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { formatMoney } from "../../M01_MasterData/M01_MasterData_UI_Logic_Money";
|
||||
import type { CfEvalResult, CfValues } from "./M02_DF_Eval";
|
||||
import type { CfEvalOptions, CfEvalResult, CfValues } from "./M02_DF_Eval";
|
||||
import type { CfBox, CfLogic, CfRound } from "./M02_DF_Model";
|
||||
import { showValue } from "./M02_DF_OutputPanel";
|
||||
import { defaultTotalRound, roundBy, roundText } from "./M02_DF_Round";
|
||||
@@ -55,7 +55,7 @@ export function renderSummary(
|
||||
lg: CfLogic,
|
||||
result: CfEvalResult,
|
||||
on?: { change(lg: CfLogic): void; select?(sel: CfSel): void },
|
||||
opts?: { values?: CfValues },
|
||||
opts?: { values?: CfValues; logics?: CfEvalOptions["logics"] },
|
||||
): void {
|
||||
const money = !lg.resultUnit || lg.resultUnit.startsWith("원");
|
||||
const perUnit = lg.resultUnit.split("/").slice(1).join("/");
|
||||
@@ -131,7 +131,7 @@ export function renderSummary(
|
||||
lg,
|
||||
result,
|
||||
{ change: (next) => on?.change(next), select: on?.select },
|
||||
{ values: opts?.values },
|
||||
{ values: opts?.values, logics: opts?.logics },
|
||||
);
|
||||
const plainValues = Object.entries(cost.값들);
|
||||
host.replaceChildren(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user