diff --git a/M02_MasterTemplete/DataFlow/M02_DF_OutputPanel.ts b/M02_MasterTemplete/DataFlow/M02_DF_OutputPanel.ts index 70d4f4537..bef75f6cb 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_OutputPanel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_OutputPanel.ts @@ -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, ], }); diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Summary.ts b/M02_MasterTemplete/DataFlow/M02_DF_Summary.ts index ed996f5cd..19a2754ac 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Summary.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Summary.ts @@ -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( diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Verify.ts b/M02_MasterTemplete/DataFlow/M02_DF_Verify.ts index 9c0f850a7..8d7aa57f6 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Verify.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Verify.ts @@ -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