From 7322c5af12c90ec6913e7f8974fed62c54507ad1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 16:48:49 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(M02):=20=EB=8B=A8=EA=B0=80=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=EB=8B=A4=EC=9D=B4=EC=96=B4=EA=B7=B8=EB=9E=A8=20?= =?UTF-8?q?=E2=80=94=20=EC=95=8C=EC=95=BD=20=EA=B0=92=20=EC=89=BC=ED=91=9C?= =?UTF-8?q?=20=C2=B7=20=ED=91=9C=20=E3=80=8C=C2=B7=20=E2=97=8B=EC=A4=84?= =?UTF-8?q?=E3=80=8D=20=C2=B7=20=EC=A0=80=EC=A0=88=EB=A1=9C=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=A4=2090%=20=EC=95=84=EB=9E=98=EB=A1=9C=20=EC=95=88=20?= =?UTF-8?q?=EB=82=B4=EB=A6=BC(PLAN=2035-7=20=EB=B8=8C=EB=A0=88=EC=9D=B8=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=E2=91=A1=20=E2=91=A2=20=E2=91=A5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 수는 천 단위 쉼표 · 소수 셋째 자리까지 · 끝 0 지움 · 글은 그대로 - 저절로 맞춤이 넘치면 왼쪽 위부터 보임 · 축소 단추로는 더 줄임 - 알약 대신 길(Model 준비 전) 걷음 · 682줄 · CostFlow 전부 검사 초록 - ORCA 견본 메쌓기: 97,882.512 · 268,908 · 0.364 · 표 · 40줄 · 처음 확대 0.9 · 한눈에 0.9 · 축소 0.75 · 알약 글 10.8px Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z --- M02_MasterTemplete/CostFlow/M02_CF_Diagram.ts | 49 +++++++------------ 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/M02_MasterTemplete/CostFlow/M02_CF_Diagram.ts b/M02_MasterTemplete/CostFlow/M02_CF_Diagram.ts index e4febfee2..5ba3662b6 100644 --- a/M02_MasterTemplete/CostFlow/M02_CF_Diagram.ts +++ b/M02_MasterTemplete/CostFlow/M02_CF_Diagram.ts @@ -93,29 +93,9 @@ export interface DiagramOpts { /* ── 알약 · 자리(DOM 없음) ─────────────────────────────────────────── */ -/** 알약 — Model 이 아직이면 상자 칸에서 바로 뽑음 */ +/** 알약 — Model 의 받는 · 내보내는 알약 */ export function pillsOf(box: CfBox): { ins: CfPill[]; outs: CfPill[] } { - try { - return { ins: portsIn(box), outs: portsOut(box) }; - } catch { - if (box.kind === "input") { - const inp = box.input; - const outs = [ - ...(inp?.elements ?? []).map((e) => ({ name: e.name, unit: e.unit })), - ...(inp?.table ? [{ name: "표", unit: "-" }] : []), - ...(inp?.variables ?? []).map((v) => ({ name: v.name, unit: v.unit })), - ]; - return { ins: [], outs }; - } - if (box.kind === "function") { - const f = box.func; - return { - ins: (f?.inputs ?? []).map((p) => ({ name: p.port, unit: "-" })), - outs: f ? [{ name: f.output.name, unit: f.output.unit }] : [], - }; - } - return { ins: [{ name: "값", unit: box.unit }], outs: [] }; - } + return { ins: portsIn(box), outs: portsOut(box) }; } /** 떨군 자리 → 받을 끝 · 상자에 떨구면 같은 이름 알약(받는 알약이 하나면 그것) · 못 정하면 까닭 */ @@ -170,7 +150,10 @@ export function arrange( return out; } -/** 모두 보이게 맞춤 — 상자 둘레 → 확대 · 판 자리 */ +/** 저절로 맞출 때 가장 작은 확대 — 알약 글이 너무 작아지지 않게(더 줄이기는 축소 단추로) */ +export const FIT_MIN = 0.9; + +/** 모두 보이게 맞춤 — 상자 둘레 → 확대 · 판 자리 · FIT_MIN 아래로 안 내림(넘치면 왼쪽 위부터 보임) */ export function fitView( rects: { x: number; y: number; w: number; h: number }[], view: { w: number; h: number }, @@ -181,12 +164,11 @@ export function fitView( const y0 = Math.min(...rects.map((r) => r.y)) - pad; const x1 = Math.max(...rects.map((r) => r.x + r.w)) + pad; const y1 = Math.max(...rects.map((r) => r.y + r.h)) + pad; - const zoom = clampZoom(Math.min(view.w / (x1 - x0), view.h / (y1 - y0), 1.5)); - return { - zoom, - panX: (view.w - (x1 - x0) * zoom) / 2 - x0 * zoom, - panY: (view.h - (y1 - y0) * zoom) / 2 - y0 * zoom, - }; + const zoom = clampZoom(Math.max(FIT_MIN, Math.min(view.w / (x1 - x0), view.h / (y1 - y0), 1.5))); + /** 남으면 가운데 · 넘치면 왼쪽(위) 끝부터 */ + const place = (size: number, from: number, to: number): number => + Math.max(0, (size - (to - from) * zoom) / 2) - from * zoom; + return { zoom, panX: place(view.w, x0, x1), panY: place(view.h, y0, y1) }; } const clampZoom = (z: number): number => Math.min(2, Math.max(0.25, +z.toFixed(3))); @@ -208,10 +190,15 @@ interface HostState { const states = new WeakMap(); const SVG = "http://www.w3.org/2000/svg"; -const showValue = (v: unknown): string => { +/** 알약 값 글 — 수는 천 단위 쉼표 · 소수 셋째 자리까지(끝 0 지움) · 표는 「· ○줄」 · 글은 그대로 */ +export const showValue = (v: unknown): string => { if (v === null || v === undefined || v === "") return ""; if (typeof v === "number") - return Number.isInteger(v) ? v.toLocaleString() : String(+v.toFixed(4)); + return Number.isFinite(v) ? v.toLocaleString("ko-KR", { maximumFractionDigits: 3 }) : ""; + if (Array.isArray(v)) { + const head = Array.isArray(v[0]) && v[0].every((c) => typeof c === "string") ? 1 : 0; + return `· ${Math.max(0, v.length - head).toLocaleString("ko-KR")}줄`; + } return String(v).slice(0, 12); }; From aacd21dd7c7cb719fba4c129bd99a149a68eeba3 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 16:49:20 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(m02-cf):=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EC=83=81=EC=9E=90=20=EB=8B=A8=EC=9C=84=EB=A5=BC=20=EC=9A=94?= =?UTF-8?q?=EC=86=8C=20=C2=B7=20=EB=B3=80=EC=88=98=20=EB=8B=A8=EC=9C=84?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=A4=20=E2=80=94=20=EC=97=B4=20?= =?UTF-8?q?=EB=95=8C=20=EB=B0=94=EB=A1=9C=EC=9E=A1=EC=9D=8C(=EA=B2=AC?= =?UTF-8?q?=EB=B3=B8=20=EC=9D=B8=EB=A0=A5=20=EC=83=81=EC=9E=90=20=EC=9B=90?= =?UTF-8?q?/=EC=9D=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_019ACSGaHdLgnkEkoA4LDtMU --- .../CostFlow/M02_CF_InputPanel.ts | 4 +++ .../CostFlow/M02_CF_InputPanel_Build.ts | 10 ++++++ .../tester/test_m02_cf_inputpanel_build.py | 33 +++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/M02_MasterTemplete/CostFlow/M02_CF_InputPanel.ts b/M02_MasterTemplete/CostFlow/M02_CF_InputPanel.ts index 168a14005..bcaed88ba 100644 --- a/M02_MasterTemplete/CostFlow/M02_CF_InputPanel.ts +++ b/M02_MasterTemplete/CostFlow/M02_CF_InputPanel.ts @@ -31,6 +31,7 @@ import { elementOf, machineFields, queryOf, + reconcileUnit, removeElement, setKind, valueOf, @@ -143,6 +144,9 @@ export function renderInputPanel( host.replaceChildren(el("p", { className: "m02-cfip__muted", text: t("NotInput") })); return; } + // 상자 단위가 고른 요소 · 변수 단위와 다르면(견본 등) 맞춰 다시 그림 — 맞으면 바로 지나감 + const fixed = reconcileUnit(lg, boxId); + if (fixed !== lg) return on.change(fixed); const input = box.input ?? { mode: "variable" as const, elements: [], variables: [] }; const kind: CfGroup | "변수" = input.mode === "variable" ? "변수" : (input.group ?? "인력"); const body = el("div", { className: "m02-cfip__body" }); diff --git a/M02_MasterTemplete/CostFlow/M02_CF_InputPanel_Build.ts b/M02_MasterTemplete/CostFlow/M02_CF_InputPanel_Build.ts index ce2f0799b..1e392059b 100644 --- a/M02_MasterTemplete/CostFlow/M02_CF_InputPanel_Build.ts +++ b/M02_MasterTemplete/CostFlow/M02_CF_InputPanel_Build.ts @@ -282,3 +282,13 @@ export function removeVariable(lg: CfLogic, id: string, name: string, pills?: Pi pills, ); } + +/** 상자 단위를 요소 · 변수 단위에 맞춤 — 이미 맞으면 받은 것 그대로(견본처럼 단위가 안 맞게 저장된 상자를 열 때 바로잡음) */ +export function reconcileUnit(lg: CfLogic, id: string): CfLogic { + const box = lg.boxes.find((b) => b.id === id); + if (!box?.input) return lg; + const want = unitOfInput(box.input); + return box.unit === want + ? lg + : { ...lg, boxes: lg.boxes.map((b) => (b.id === id ? { ...b, unit: want } : b)) }; +} diff --git a/resources/tester/test_m02_cf_inputpanel_build.py b/resources/tester/test_m02_cf_inputpanel_build.py index 890b0639d..79aa5cb05 100644 --- a/resources/tester/test_m02_cf_inputpanel_build.py +++ b/resources/tester/test_m02_cf_inputpanel_build.py @@ -219,3 +219,36 @@ def test_환율_유가전력_알약_이름에_날짜가_안_들어감(): "값": 1900, } assert call("elementOf", "유가전력", oil)["name"] == "휘발유 서울" + + +def test_단위가_안_맞게_저장된_상자는_열_때_맞춤(): + el1, el2 = call("elementOf", "인력", LABOR), call("elementOf", "인력", LABOR2) + lg = logic() + lg["boxes"][0]["input"] = { + "mode": "group", + "group": "인력", + "elements": [el1, el2], + "variables": [], + } + assert lg["boxes"][0]["unit"] == "-" + fixed = run([{"op": "fn", "name": "reconcileUnit", "args": [lg, "b1"]}])[0] + assert fixed["boxes"][0]["unit"] == "원/일" + again = run([{"op": "fn", "name": "reconcileUnit", "args": [fixed, "b1"]}])[0] + assert again == fixed + oil = {"ref": "X", "name": "경유", "spec": "", "unit": "원/L"} + lg["boxes"][0]["input"]["elements"].append(oil) + lg["boxes"][0]["unit"] = "원/일" + assert ( + run([{"op": "fn", "name": "reconcileUnit", "args": [lg, "b1"]}])[0]["boxes"][0]["unit"] + == "-" + ) + one = { + "mode": "variable", + "elements": [], + "variables": [{"name": "면적", "type": "number", "unit": "㎡", "example": 1}], + } + lg["boxes"][0]["input"] = one + assert ( + run([{"op": "fn", "name": "reconcileUnit", "args": [lg, "b1"]}])[0]["boxes"][0]["unit"] + == "㎡" + )