From deabd978c00e4bc1c2362cbf7a7875a16ee38130 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 23:39:21 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(M02):=20=ED=9D=90=EB=A6=84=EB=8F=84=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=20=EC=B2=98=EC=9D=8C=20=EB=B0=B0=EC=9C=A8=20?= =?UTF-8?q?=3D=20=EC=9D=BD=ED=9E=88=EB=8A=94=20=EC=B5=9C=EC=86=8C=200.6(?= =?UTF-8?q?=EB=84=98=EC=B9=98=EB=A9=B4=20=EC=99=BC=EC=AA=BD=20=EC=9C=84=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EC=83=81=EC=9E=90=EB=B6=80=ED=84=B0)=20?= =?UTF-8?q?=C2=B7=20=E3=80=8C=ED=95=9C=EB=88=88=EC=97=90=E3=80=8D=20?= =?UTF-8?q?=EB=8B=A8=EC=B6=94=EB=8A=94=20=EC=A0=84=EC=B2=B4(0.25)=20=C2=B7?= =?UTF-8?q?=20=ED=81=AC=EA=B2=8C=20=EB=B3=B4=EA=B8=B0=EB=8A=94=20=EA=B7=B8?= =?UTF-8?q?=EB=8C=80=EB=A1=9C=200.9(PLAN=2036-3=20=EB=8D=A7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 다이어그램 opts.fitAllMin(단추) · fitMin(저절로) 나눔 · Paper · Panel 이 넘김 - 시험 diagram_parts 8 + panel 2 통과 · DataFlow 전부 검사 초록 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z --- M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts | 15 ++++++++++++--- .../DataFlow/M02_DF_Diagram_Parts.ts | 2 ++ M02_MasterTemplete/DataFlow/M02_DF_Panel.ts | 9 ++++++--- M02_MasterTemplete/DataFlow/M02_DF_Paper.ts | 3 +++ resources/tester/test_m02_df_diagram_parts.py | 17 ++++++++++++++++- 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts index ae84fe5e1..26dff53d5 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts @@ -314,13 +314,22 @@ export function renderDiagram( applyView(); drawLines(); }; - const fit = (): void => { + /** 맞추기 — 저절로(처음 · 다시 맞춤)는 fitMin · 「한눈에」 단추는 fitAllMin(전체가 보이게) */ + const fit = (button = false): void => { const rects = lg.boxes.map((b) => { const n = world.querySelector(`.m02-dfdg__box[data-box="${CSS.escape(b.id)}"]`); return { x: b.x, y: b.y, w: n?.offsetWidth ?? 180, h: n?.offsetHeight ?? 80 }; }); const r = view.getBoundingClientRect(); - Object.assign(st, fitView(rects, { w: r.width, h: r.height }, 40, opts.fitMin)); + Object.assign( + st, + fitView( + rects, + { w: r.width, h: r.height }, + 40, + button ? (opts.fitAllMin ?? opts.fitMin) : opts.fitMin, + ), + ); applyView(); drawLines(); }; @@ -400,7 +409,7 @@ export function renderDiagram( sm("ZoomOut", () => setZoom(st.zoom / 1.2)), zoomLabel, sm("ZoomIn", () => setZoom(st.zoom * 1.2)), - sm("Fit", fit), + sm("Fit", () => fit(true)), ...(ro ? [] : [ diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts index 49052241f..a81da8fbc 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts @@ -124,6 +124,8 @@ export interface DiagramOpts { fitKey?: number; /** 저절로 맞출 때 가장 작은 확대 — 없으면 FIT_MIN(좁은 패널은 더 작게) */ fitMin?: number; + /** 「한눈에」 단추로 맞출 때 가장 작은 확대 — 없으면 fitMin */ + fitAllMin?: number; } /* ── 알약 · 자리 ────────────────────────────────────────────────────── */ diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Panel.ts b/M02_MasterTemplete/DataFlow/M02_DF_Panel.ts index de5d49f87..173a32cd0 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Panel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Panel.ts @@ -52,8 +52,10 @@ export interface FlowPanel { export const PANEL_MIN = 320; export const STRIP_WIDTH = 40; -/** 좁은 패널에서 저절로 맞출 때 가장 작은 확대 */ -export const PANEL_FIT_MIN = 0.25; +/** 패널에서 저절로 맞출 때 가장 작은 확대 — 글이 읽히는 최소 */ +export const PANEL_FIT_MIN = 0.6; +/** 패널 「한눈에」 단추 — 전체가 보이게(가장 작은 확대) */ +export const PANEL_FIT_ALL_MIN = 0.25; const WIDTH_KEY = "m02.dfpanel.width"; const FOLD_KEY = "m02.dfpanel.folded"; @@ -94,8 +96,9 @@ export function mountFlowPanel(host: HTMLElement, opts: FlowPanelOpts): FlowPane readOnly: opts.readOnly, onChange: opts.onChange, logics: opts.logics, - // 좁은 패널은 「한눈에」 가 되게 더 작게 · 크게 보기는 글이 읽히게 90% 아래로 안 내림 + // 패널 처음 배율 = 글이 읽히는 최소(넘치면 왼쪽 위 입력 상자부터) · 「한눈에」 단추는 전체 · 크게 보기는 90% fitMin: () => (full ? 0.9 : PANEL_FIT_MIN), + fitAllMin: () => (full ? 0.9 : PANEL_FIT_ALL_MIN), }); const { diagram, detail, summary, note, undo } = paper.parts; let width = storedWidth(readStore(WIDTH_KEY)); diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Paper.ts b/M02_MasterTemplete/DataFlow/M02_DF_Paper.ts index 85146e7f4..228f3debf 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Paper.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Paper.ts @@ -55,6 +55,8 @@ export interface PaperOpts { logics?(key: string): Promise; /** 저절로 맞출 때 가장 작은 확대 — 부를 때마다 물음(패널 · 크게 보기가 다름) */ fitMin?(): number; + /** 「한눈에」 단추의 가장 작은 확대 — 없으면 fitMin */ + fitAllMin?(): number; } export interface Paper { @@ -307,6 +309,7 @@ export function createPaper(opts: PaperOpts): Paper { readOnly, fitKey, fitMin: opts.fitMin?.(), + fitAllMin: opts.fitAllMin?.(), }); drawPanel(); renderSummary(summaryHost, lg, result, { change: editExample, select: choose }, { values }); diff --git a/resources/tester/test_m02_df_diagram_parts.py b/resources/tester/test_m02_df_diagram_parts.py index ae8248233..00e7301e8 100644 --- a/resources/tester/test_m02_df_diagram_parts.py +++ b/resources/tester/test_m02_df_diagram_parts.py @@ -43,7 +43,7 @@ const out = jobs.map((job) => { if (job.op === "deps") return m.depLines(job.lg).map((d) => ({ id: d.id, from: d.from, to: d.to })); if (job.op === "inTrace") return m.depLines(job.lg).map((d) => m.depInTrace(d, job.steps)); if (job.op === "rest") return job.breaks.map((b) => m.isRest(b)); - if (job.op === "fit") return m.fitView(job.rects, job.view); + if (job.op === "fit") return m.fitView(job.rects, job.view, 40, job.min); if (job.op === "value") return job.values.map((v) => m.showValue(v)); if (job.op === "end") return m.depEnd(job.a, job.tag); if (job.op === "restParts") { @@ -404,3 +404,18 @@ def test_지반_토사_쉼_흐림은_브레이커로_깨기_하나(): assert got["boxes"] == ["brk"] assert got["links"] == ["l2", "l6"] assert got["pills"] == ["brk|굴착량", "sum|브레이커 경비"] + + +def test_패널_처음_배율은_읽히는_최소_넘치면_왼쪽_위부터(): + rects = [{"x": 40, "y": 40, "w": 200, "h": 120}, {"x": 1500, "y": 600, "w": 200, "h": 120}] + auto, whole = run( + [ + {"op": "fit", "rects": rects, "view": {"w": 500, "h": 400}, "min": 0.6}, + {"op": "fit", "rects": rects, "view": {"w": 500, "h": 400}, "min": 0.25}, + ] + ) + # 처음 — 0.6 아래로 안 내림 · 넘치면 왼쪽 위(첫 상자 - 여백)가 판 왼쪽 위 + assert auto["zoom"] == 0.6 + assert auto["panX"] == 0 and auto["panY"] == 0 + # 한눈에 — 전체가 들어가게 더 작게 + assert whole["zoom"] < 0.6 From 3ea0c0fdf24a5d96185c76e8a696559ba4059525 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 23:40:53 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor(m01):=20=ED=9D=90=EB=A6=84=20?= =?UTF-8?q?=EC=99=BC=EC=AA=BD=20=EB=B3=B4=EA=B8=B0=20=E2=80=94=20=EC=8B=9D?= =?UTF-8?q?=20=EB=B3=B4=EA=B8=B0=EB=A5=BC=20sub2=20formulaLines=20?= =?UTF-8?q?=EC=A7=81=EC=A0=91=20import=20=EB=A1=9C?= 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_01EauA2ATmqNRbeV48s9CcBr --- .../M01_MasterData_UI_LogicLab_FlowView.ts | 53 +++++++------------ 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.ts index 87b9d9de2..cdf1e7456 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.ts @@ -1,7 +1,7 @@ /* ============================================================================= * M01_MasterData_UI_LogicLab_FlowView.ts * 흐름 로직의 왼쪽 「지금 내용」 — ② 단가산출 상세(출력 상자를 비용 종류별 줄 · 소계 · 계) · ③ 텍스트 수식(함수 상자마다 - * 식 두 줄 — sub2 `formulaLines` 가 있으면 그것 · 없으면 함수 이름 · 결과 값만). 기본정보 · 시험 계산은 Detail 이 둠. + * 식 두 줄 — sub2 `formulaLines`). 기본정보 · 시험 계산은 Detail 이 둠. * 값은 DataFlow 계산(Eval · 같은 코드)으로 바로 — 오른쪽 패널이 고치면 LogicLab 이 이 함수를 다시 부름. * 화면은 `buildFlowView(host, lg, values)` 하나. 이 파일은 DataFlow 를 정적으로 가져오므로 LogicLab 은 import() 로 늦게 부름. * 모양은 docs/raw/M01_쉬운단가/흐름도_적용_정함.md 7 · 8. @@ -15,7 +15,12 @@ import { type CfEvalResult, type CfValues, } from "../M02_MasterTemplete/DataFlow/M02_DF_Eval"; -import type { CfBox, CfLogic } from "../M02_MasterTemplete/DataFlow/M02_DF_Model"; +import { + cellValuesOf, + formulaLines, + type FormulaLine, +} from "../M02_MasterTemplete/DataFlow/M02_DF_FormulaView"; +import type { CfLogic } from "../M02_MasterTemplete/DataFlow/M02_DF_Model"; import { itemRoundNow, totalRoundNow } from "../M02_MasterTemplete/DataFlow/M02_DF_Round"; import { flowRows, @@ -51,22 +56,6 @@ const GROUP_TITLE: Record = { plain: "Plain", }; -/** sub2 의 식 보기(`M02_DF_FormulaView.ts` · 정함 8) — 아직 없으면 비어 있음 */ -interface FormulaLine { - name: string; - named: string; - valued: string; - branches?: { when: string; then: string; now: boolean }[]; -} -type FormulaFn = (box: CfBox, values: CfValues) => FormulaLine[]; -const FORMULA = import.meta.glob<{ formulaLines?: FormulaFn }>( - "../M02_MasterTemplete/DataFlow/M02_DF_FormulaView.ts", -); -const loadFormula = async (): Promise => { - const load = Object.values(FORMULA)[0]; - return load ? ((await load()).formulaLines ?? null) : null; -}; - const section = (title: string, body: HTMLElement[]): HTMLElement => el("section", { className: "m01-logic__section m01lab__box", @@ -182,26 +171,20 @@ export function buildFlowView( el("p", { className: "m01-logic__muted", text: tf("TotalNote") }), ...rows.errors.slice(0, 3).map((t) => el("p", { className: "m01flow__warn", text: t })), ]); - const bodies = rows.funcs.map((f) => funcBlock(f, null)); + /** 함수 상자마다 식 두 줄(sub2 식 보기 · 받는 알약 값을 칸에 넣고 셈) — 못 그리면 이름 = 값만 */ + const linesOf = (id: string): FormulaLine[] | null => { + const box = lg.boxes.find((b) => b.id === id); + if (!box?.func) return null; + try { + return formulaLines(box, cellValuesOf(box, ev.arrived?.[id])); + } catch { + return null; + } + }; + const bodies = rows.funcs.map((f) => funcBlock(f, linesOf(f.box))); const text = section( tf("Formula"), bodies.length ? bodies : [el("p", { className: "m01-logic__muted", text: tf("NoFunc") })], ); host.replaceChildren(detail, text); - // sub2 식 보기가 올라와 있으면 함수 상자마다 식 두 줄을 더함 - void loadFormula().then((fn) => { - if (!fn || !bodies.length) return; - const boxes = lg.boxes.filter((b) => b.kind === "function" && b.func); - const merged = { ...ev.ports, ...ev.arrived } as CfValues; - text.replaceChildren( - text.firstElementChild!, - ...rows.funcs.map((f, i) => { - try { - return funcBlock(f, fn(boxes[i], merged)); - } catch { - return bodies[i]; - } - }), - ); - }); }