Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_1

This commit is contained in:
2026-10-01 20:35:40 +09:00
9 changed files with 89 additions and 38 deletions
+5 -1
View File
@@ -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 {
@@ -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);
@@ -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}` : ""}`,
}),
],
}),
+22 -12
View File
@@ -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 });
}),
});
@@ -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),
});
}
@@ -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;
}
@@ -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,
@@ -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 }),
],
+9 -9
View File
@@ -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;