diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Detail.ts b/M02_MasterTemplete/DataFlow/M02_DF_Detail.ts index 30b1312f8..2f4cf9fc2 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Detail.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Detail.ts @@ -69,11 +69,15 @@ export function evalSafe(lg: CfLogic, o: DetailOptions = {}): CfEvalResult | nul export const valueOf = (ev: CfEvalResult | null, p: CfPort): CfValue | undefined => ev?.ports[p.box]?.[p.name]; +/** 숫자 글 — 천 단위 쉼표 · 군더더기 소수는 뗌 */ +export const commas = (n: number): string => + Number(n.toPrecision(12)).toLocaleString("ko-KR", { maximumFractionDigits: 6 }); + /** 화면에 보일 값 글 — 표는 줄 수 */ export function showValue(v: CfValue | undefined): string { if (v === undefined || v === null) return td("Empty"); if (Array.isArray(v)) return td("Table", { n: Math.max(0, v.length - 1) }); - return typeof v === "number" ? String(Number(v.toPrecision(12))) : String(v); + return typeof v === "number" ? commas(v) : String(v); } export function unitOf(lg: CfLogic, p: CfPort): string { diff --git a/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts b/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts index 1d7c4043c..5b4be1dc6 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts @@ -19,6 +19,7 @@ import { } from "./M02_DF_Model"; import { GATE_OPS, + commas, SCALE_OPS, evalSafe, labelOf, @@ -127,6 +128,7 @@ function select( return s; } +/** 숫자 칸 — 보일 때만 천 단위 쉼표 · 고치는 동안은 쉼표 없이 */ function numberBox( ctx: Ctx, value: number | string, @@ -135,10 +137,13 @@ function numberBox( ): HTMLElement { const i = el("input", { className: "ui-input ui-input--sm m02-dflink-num", - attrs: { type: "text", "aria-label": aria }, + attrs: { type: "text", inputmode: "decimal", "aria-label": aria }, }); - i.value = String(value); + const shown = (): string => (typeof value === "number" ? commas(value) : String(value)); + i.value = shown(); i.readOnly = ctx.readOnly; + i.addEventListener("focus", () => (i.value = String(value))); + i.addEventListener("blur", () => (i.value = shown())); i.addEventListener("change", () => { const t = i.value.trim().replace(/,/g, ""); onCommit(t !== "" && Number.isFinite(Number(t)) ? Number(t) : t); diff --git a/M02_MasterTemplete/DataFlow/M02_DF_OutputPanel.ts b/M02_MasterTemplete/DataFlow/M02_DF_OutputPanel.ts index bef75f6cb..e9c050c5b 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_OutputPanel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_OutputPanel.ts @@ -27,12 +27,12 @@ const TEXT = { 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"], + Calc: ["계산 중이 아님", "Not calculated"], + Cant: ["지금은 바꿀 수 없음", "Cannot change this now"], Round: ["끝수", "Rounding"], RoundPlaces: ["자리", "Places"], RoundMethod: ["방법", "Method"], @@ -43,10 +43,7 @@ const TEXT = { Apply: ["적용", "Apply"], RoundReset: ["기본으로", "Back to default"], RoundUser: ["고른 끝수", "Chosen rounding"], - PlainHint: [ - "원가 합계에 안 넣고 값으로만 보여요", - "Shown as a value, not added to the cost total", - ], + PlainHint: ["원가 합계에 안 넣고 값으로만 보임", "Shown as a value, not added to the cost total"], } as const; type TextKey = keyof typeof TEXT; @@ -128,7 +125,7 @@ export function renderOutputPanel( el("dt", { text: to("Value") }), el("dd", { className: "m02-dfout__num", - text: `${showValue(value, cost !== "값")}${unit && unit !== "-" ? ` ${unit}` : ""}`, + text: `${showValue(value)}${unit && unit !== "-" ? ` ${unit}` : ""}`, }), ], }), diff --git a/M02_MasterTemplete/DataFlow/M02_DF_PillPanel.ts b/M02_MasterTemplete/DataFlow/M02_DF_PillPanel.ts index dcd3a4aa3..deff36790 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_PillPanel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_PillPanel.ts @@ -47,6 +47,7 @@ const line = (label: string, ...value: (HTMLElement | string)[]): HTMLElement => children: [el("span", { className: "m02-dflink-label", text: label }), ...value], }); +/** 한 줄 = 선 하나면 「보낸 알약 → (규칙 글) → 받는 알약」 · 함수 안 걸음이면 알약 하나 */ function steps( lg: CfLogic, list: CfStep[], @@ -55,21 +56,30 @@ function steps( onBox?: (id: string) => void, ): HTMLElement { if (!list.length) return el("p", { className: "m02-dflink-note", text: empty }); + const pill = (box: string, name: string): HTMLElement | string => + onBox + ? createButton({ + label: portText(lg, { box, name }), + variant: "ghost", + size: "sm", + onClick: () => onBox(box), + }) + : portText(lg, { box, name }); return el("ol", { className: "m02-dfpill-list", children: list.map((s) => { - const parts: (HTMLElement | string)[] = [ - arrow, - onBox - ? createButton({ - label: portText(lg, { box: s.box, name: s.port }), - variant: "ghost", - size: "sm", - onClick: () => onBox(s.box), - }) - : portText(lg, { box: s.box, name: s.port }), - ]; - if (s.rule) parts.push(el("span", { className: "m02-dfpill-rule", text: labelOf(s.rule) })); + const link = s.link ? lg.links.find((l) => l.id === s.link) : undefined; + const parts: (HTMLElement | string)[] = link + ? [ + arrow, + pill(link.from.box, link.from.name), + "→", + ...(s.rule + ? [el("span", { className: "m02-dfpill-rule", text: `(${labelOf(s.rule)})` }), "→"] + : []), + pill(link.to.box, link.to.name), + ] + : [arrow, pill(s.box, s.port)]; return el("li", { className: "m02-dfpill-step", children: parts }); }), }); diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Round.ts b/M02_MasterTemplete/DataFlow/M02_DF_Round.ts index 3d1a54d90..5341b5a83 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Round.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Round.ts @@ -88,3 +88,17 @@ export function roundText(r: CfRound): string { const step = r.places > 0 ? Number((10 ** -r.places).toFixed(r.places)) : 1; return `${step}원 미만 ${r.method}`; } + +/** 출력 기본 끝수(표를 읽어 오기 전에는 금액란 0.1원 미만 버림) — 화면이 바로 쓰는 값 */ +let itemNow: CfRound = { places: 1, method: "버림" }; +void defaultItemRound().then((r) => (itemNow = r)); +export const itemRoundNow = (): CfRound => itemNow; + +/** 끝수대로 자른 값을 그 자리 수만큼 보임(3,216.6) — 끝수가 없으면 소수 여섯째 자리까지 */ +export function showRounded(v: number, r?: CfRound): string { + if (!r) return v.toLocaleString("ko-KR", { maximumFractionDigits: 6 }); + return roundBy(r, v).toLocaleString("ko-KR", { + minimumFractionDigits: 0, + maximumFractionDigits: Math.max(0, r.places), + }); +} diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Standards.css b/M02_MasterTemplete/DataFlow/M02_DF_Standards.css index 066d8dc0f..ca3351b9c 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Standards.css +++ b/M02_MasterTemplete/DataFlow/M02_DF_Standards.css @@ -11,7 +11,8 @@ gap: var(--spacing-8, 8px); } -.m02-df-std__head h3 { +/* 첫 칸(제목 · 제목이 없으면 상태 글)이 단추를 오른쪽으로 밂 */ +.m02-df-std__head > :first-child { margin: 0 auto 0 0; } diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Standards.ts b/M02_MasterTemplete/DataFlow/M02_DF_Standards.ts index 8d4bd646b..969a92a01 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Standards.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Standards.ts @@ -4,7 +4,7 @@ * 처음 값 = `_standard/단위표준.json` · `금액단위표준.json`(산림품셈 1-2-2 · 원문 흠 그대로 — 사용자가 고침) * 서버 = `M02_DF_Api` cfStandards · cfStandardsSave · cfStandardsReset(`/api/m02/df/standards`) → {unit, money, saved} · * 고친 것은 `_data`(마스터에 안 씀) - * `renderStandards(host, on)` = 시험판 안 「표준 표」 판(두 표 보기 · 칸 고치기 · 저장 · 처음 값으로) + * `renderStandards(host, on, { title })` = 시험판 안 「표준 표」 판(두 표 보기 · 칸 고치기 · 저장 · 처음 값으로) * `standardsList()` = 단위 고르기 · 끝수 제안이 부르는 목록(동기 · 처음엔 번들 값 · 판이 서버 값을 받으면 그것) * 이 폴더째 지워도 됨. * ========================================================================== */ @@ -185,8 +185,17 @@ function parseCell(key: string, text: string): string | number | null | undefine const show = (v: unknown): string => v === null || v === undefined ? "" : typeof v === "number" ? v.toLocaleString() : String(v); +export interface StandardsOpts { + /** 판 제목 「표준 표」 — 모달처럼 겉에 제목이 있으면 false(기본 true) */ + title?: boolean; +} + /** 시험판 안 「표준 표」 판 */ -export function renderStandards(host: HTMLElement, on: StandardsOn = {}): void { +export function renderStandards( + host: HTMLElement, + on: StandardsOn = {}, + opts: StandardsOpts = {}, +): void { let work = clone(current); let dirty = false; const state = el("span", { className: "m02-df-std__state" }); @@ -334,7 +343,12 @@ export function renderStandards(host: HTMLElement, on: StandardsOn = {}): void { children: [ el("div", { className: "m02-df-std__head", - children: [el("h3", { text: tx("Title") }), state, reset, save], + children: [ + ...(opts.title === false ? [] : [el("h3", { text: tx("Title") })]), + state, + reset, + save, + ], }), el("p", { className: "m02-df-std__hint", text: `${tx("Source")} · ${tx("Hint")}` }), body, diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Summary.ts b/M02_MasterTemplete/DataFlow/M02_DF_Summary.ts index 19a2754ac..a4686af39 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Summary.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Summary.ts @@ -12,7 +12,7 @@ import { formatMoney } from "../../M01_MasterData/M01_MasterData_UI_Logic_Money" 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"; +import { defaultTotalRound, itemRoundNow, roundBy, roundText, showRounded } from "./M02_DF_Round"; import { renderVerify, type CfSel } from "./M02_DF_Verify"; import "./M02_DF_Summary.css"; @@ -49,6 +49,12 @@ const costText = (c: string): string => (c === "값" ? ts("Plain3") : c); let totalRound: CfRound = { places: 0, method: "버림" }; void defaultTotalRound().then((r) => (totalRound = r)); +/** 출력 상자 값 — 그 출력의 끝수 자리대로(끝수를 안 정했으면 비용은 표준 기본 · 값은 그대로) */ +const shown = (b: CfBox, v: unknown, isCost: boolean): string => + typeof v === "number" + ? showRounded(v, b.output?.round ?? (isCost ? itemRoundNow() : undefined)) + : showValue(v as never); + /** 원가 합계 칸 — result = 화면 계산 결과 · on 이 있으면 예시값을 바꿀 수 있음 · opts.values = 받은 입력 값(검사가 씀) */ export function renderSummary( host: HTMLElement, @@ -89,7 +95,7 @@ export function renderSummary( ...(isCost ? [el("td", { text: costText(b.output?.cost ?? "") })] : []), el("td", { className: "m02-dfsum__num", - text: showValue(result.outputs[b.id], isCost), + text: shown(b, result.outputs[b.id], isCost), }), el("td", { text: b.output?.unit ?? b.unit }), ], diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Verify.ts b/M02_MasterTemplete/DataFlow/M02_DF_Verify.ts index c02658d53..9c1e422bd 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Verify.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Verify.ts @@ -23,9 +23,9 @@ import "./M02_DF_Verify.css"; const TEXT = { Breaks: ["끊긴 곳", "Broken spots"], - BreaksHint: ["누르면 그 상자 · 선을 골라요", "Click to select the box or line"], + BreaksHint: ["누르면 그 상자 · 선을 고름", "Click to select the box or line"], Design: ["설계 때 정할 것", "To decide at design time"], - DesignHint: ["여기서 바꾸면 해당 상자도 바뀌어요", "Changing here changes the box too"], + DesignHint: ["여기서 바꾸면 그 상자도 바뀜", "Changing here changes the box too"], Vars: ["변수 예시값", "Variable samples"], Slots: ["재료 선택 자리", "Material choice"], Rules: ["재료 조건 자리", "Material by condition"], @@ -37,23 +37,23 @@ const TEXT = { Next: ["다음", "Next"], Search: ["재료 이름 · 규격", "Material name or size"], Page: ["쪽 {p} / {n} · 모두 {t}", "Page {p} / {n} · {t} in all"], - NoHit: ["찾은 재료가 없어요", "No material found"], - NoDefault: ["아직 고르지 않았어요", "Nothing chosen yet"], + NoHit: ["찾은 재료 없음", "No material found"], + NoDefault: ["아직 고르지 않음", "Nothing chosen yet"], Price: ["단가", "Unit price"], Won: ["원", "KRW"], Loading: ["찾는 중", "Searching"], - NeedValue: ["조건 값이 아직 없어요", "The condition value is not there yet"], + NeedValue: ["조건 값이 아직 없음", "The condition value is not there yet"], Caught: ["잡힌 품목", "Matched item"], Zero: ["맞는 재료 없음(0)", "No matching material (0)"], Many: ["여럿 맞음({n}) — 첫째를 씀", "{n} match — the first is used"], Cond: ["{field}에 「{v}」 {op}", "{field} {op} “{v}”"], - Pick: ["고르세요", "Choose"], - Bad: ["숫자를 넣어 주세요", "Enter a number"], + Pick: ["선택", "Choose"], + Bad: ["숫자를 넣어야 함", "Enter a number"], Range: ["{a} ∼ {b} 사이", "between {a} and {b}"], - Cant: ["지금은 바꿀 수 없어요", "Cannot change this now"], + Cant: ["지금은 바꿀 수 없음", "Cannot change this now"], OwnSample: ["비우면 그 로직 예시값", "Empty = the logic's own sample"], OtherLogic: [ - "이어 주지 않아서 그 로직의 예시값을 써요", + "이어 주지 않아 그 로직의 예시값을 씀", "Not linked — the logic's own sample is used", ], } as const;