From 3c7832235048f0f99ac8bc341be91d605ea0aec1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 23:30:18 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(M02):=20=EC=98=A4=EB=A5=B8=EC=AA=BD=20?= =?UTF-8?q?=ED=9D=90=EB=A6=84=EB=8F=84=20=ED=8C=A8=EB=84=90=20M02=5FDF=5FP?= =?UTF-8?q?anel.ts=20=E2=80=94=20mountFlowPanel(host,=20opts)=20=E2=86=92?= =?UTF-8?q?=20set=20=C2=B7=20destroy=20=C2=B7=20expand(PLAN=2036-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 세로: 머리(제목 · 크게 보기 · 접기) · 실행 취소 · 다이어그램 · 상세 내용 · 원가 · 검증(접기) · 저장 단추 없음 - 왼쪽 가장자리 끌기 · 화살표 키 → onResize(px) · 마지막 너비 localStorage · 접기 = 얇은 띠 「흐름도 ▸」 - 크게 보기 = 같은 판을 화면 전체로 옮겨 끼움 · 닫으면 패널로(고친 것 그대로) · Esc · Ctrl+Z - 좁은 패널은 저절로 맞춤 바닥 0.5(한눈에) · 크게 보기는 0.9 — 다이어그램 opts.fitMin - 시험 test_m02_df_panel.py 2 통과 · typecheck · DataFlow 전부 검사 초록 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z --- M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts | 2 +- .../DataFlow/M02_DF_Diagram_Parts.ts | 5 +- M02_MasterTemplete/DataFlow/M02_DF_Panel.css | 151 ++++++++++ M02_MasterTemplete/DataFlow/M02_DF_Panel.ts | 262 ++++++++++++++++++ M02_MasterTemplete/DataFlow/M02_DF_Paper.ts | 3 + resources/tester/test_m02_df_panel.py | 83 ++++++ 6 files changed, 504 insertions(+), 2 deletions(-) create mode 100644 M02_MasterTemplete/DataFlow/M02_DF_Panel.css create mode 100644 M02_MasterTemplete/DataFlow/M02_DF_Panel.ts create mode 100644 resources/tester/test_m02_df_panel.py diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts index e74e30fa7..ae84fe5e1 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts @@ -320,7 +320,7 @@ export function renderDiagram( 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 })); + Object.assign(st, fitView(rects, { w: r.width, h: r.height }, 40, opts.fitMin)); applyView(); drawLines(); }; diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts index 649aac97e..49052241f 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts @@ -122,6 +122,8 @@ export interface DiagramOpts { readOnly?: boolean; /** 바뀌면 다시 맞춤(자리를 옮겨 끼운 뒤 · 새 로직) */ fitKey?: number; + /** 저절로 맞출 때 가장 작은 확대 — 없으면 FIT_MIN(좁은 패널은 더 작게) */ + fitMin?: number; } /* ── 알약 · 자리 ────────────────────────────────────────────────────── */ @@ -193,13 +195,14 @@ export function fitView( rects: { x: number; y: number; w: number; h: number }[], view: { w: number; h: number }, pad = 40, + min = FIT_MIN, ): { zoom: number; panX: number; panY: number } { if (!rects.length) return { zoom: 1, panX: 0, panY: 0 }; const x0 = Math.min(...rects.map((r) => r.x)) - pad; 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.max(FIT_MIN, Math.min(view.w / (x1 - x0), view.h / (y1 - y0), 1.5))); + const zoom = clampZoom(Math.max(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; diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Panel.css b/M02_MasterTemplete/DataFlow/M02_DF_Panel.css new file mode 100644 index 000000000..f9ffd4fc8 --- /dev/null +++ b/M02_MasterTemplete/DataFlow/M02_DF_Panel.css @@ -0,0 +1,151 @@ +/* 오른쪽 흐름도 패널 — 부르는 쪽 칸 안에서 100% */ +.m02-dfp { + position: relative; + display: flex; + width: 100%; + min-width: 0; + height: 100%; + border-left: 1px solid var(--color-border); + background: var(--color-canvas); +} + +.m02-dfp__open { + display: flex; + flex: 1; + flex-direction: column; + gap: var(--spacing-8); + min-width: 0; + padding: var(--spacing-8) var(--spacing-12) var(--spacing-12); +} + +.m02-dfp__head { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-8); +} + +.m02-dfp__title { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.m02-dfp__tools { + display: flex; + justify-content: flex-end; + gap: var(--spacing-8); +} + +/* 좁은 패널 — 다이어그램 판 높이를 패널에 맞춤 */ +.m02-dfp .m02-dfdg__view { + height: max(320px, 45vh); +} + +.m02-dfp__lock { + min-width: 0; + margin: 0; + padding: 0; + border: 0; +} + +.m02-dfp__cost > summary { + cursor: pointer; + font-weight: 600; + padding: var(--spacing-4) 0; +} + +/* 왼쪽 가장자리 끌기 */ +.m02-dfp__grip { + position: absolute; + top: 0; + bottom: 0; + left: -4px; + z-index: 2; + width: 8px; + cursor: col-resize; + touch-action: none; +} + +.m02-dfp__grip:hover, +.m02-dfp__grip:focus-visible, +.m02-dfp.is-resizing .m02-dfp__grip { + background: color-mix(in srgb, var(--color-accent) 35%, transparent); + outline: none; +} + +.m02-dfp.is-resizing { + user-select: none; +} + +/* 접기 = 얇은 띠 */ +.m02-dfp__strip { + width: 100%; + padding: var(--spacing-12) 0; + border: 0; + background: var(--color-surface); + color: var(--color-text-secondary); + font: inherit; + writing-mode: vertical-rl; + cursor: pointer; +} + +.m02-dfp__strip:hover, +.m02-dfp__strip:focus-visible { + color: var(--color-accent); +} + +.m02-dfp__strip[hidden], +.m02-dfp__open[hidden], +.m02-dfp__grip[hidden] { + display: none; +} + +/* 크게 보기 — 화면 전체 */ +.m02-dfp-full { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + flex-direction: column; + gap: var(--spacing-8); + padding: var(--spacing-12) var(--spacing-16); + overflow: auto; + background: var(--color-canvas); + outline: none; +} + +.m02-dfp-full__head { + display: flex; + align-items: center; + gap: var(--spacing-8); +} + +.m02-dfp-full__head strong { + flex: 1; + font-size: var(--text-body); +} + +.m02-dfp-full__diagram .m02-dfdg__view { + height: max(420px, 60vh); +} + +.m02-dfp-full__lower { + display: grid; + grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); + gap: var(--spacing-16); + align-items: start; +} + +.m02-dfp-full__lower h3 { + margin: 0 0 var(--spacing-8); + font-size: var(--text-body); +} + +@media (max-width: 900px) { + .m02-dfp-full__lower { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Panel.ts b/M02_MasterTemplete/DataFlow/M02_DF_Panel.ts new file mode 100644 index 000000000..ade890724 --- /dev/null +++ b/M02_MasterTemplete/DataFlow/M02_DF_Panel.ts @@ -0,0 +1,262 @@ +/* ============================================================================= + * M02_DF_Panel.ts + * 오른쪽 흐름도 패널(흐름도 적용 정함 5) — mountFlowPanel(host, opts) → { set, destroy, expand } + * 패널 안 세로: 머리(제목 · 「크게 보기」 · 접기) → 다이어그램(+ 입력 · 함수 · 출력 · 실행 취소 · 맞추기) → + * 상세 내용(상자 · 알약 · 선 판) → 원가 · 검증 칸(접기). 저장 단추 없음(페이지 [저장]) · 바뀔 때마다 onChange. + * 너비 = 부르는 쪽 칸 안 100% · 왼쪽 가장자리 끌기 → onResize(px) · 마지막 너비 localStorage · + * 접기 = 얇은 띠(「흐름도 ▸」) · 크게 보기 = 화면 전체(같은 흐름 객체 · 닫으면 패널로 · 고친 것 그대로). + * 고치는 핵심은 `M02_DF_Paper.ts`(판 DOM 을 옮겨 끼움). + * ========================================================================== */ + +import { currentLanguageIndex } from "@ui/ui_template_locale"; +import { createButton, el } from "@ui/ui_template_elements"; +import type { CfLogic } from "./M02_DF_Model"; +import { createPaper } from "./M02_DF_Paper"; +import "./M02_DF_Panel.css"; + +const TEXT = { + Big: ["크게 보기", "Full screen"], + Fold: ["접기", "Collapse"], + Strip: ["흐름도 ▸", "Flow ▸"], + StripTitle: ["흐름도 펴기", "Open the flow panel"], + Close: ["닫기", "Close"], + Detail: ["상세 내용", "Details"], + Cost: ["원가 · 검증", "Cost · check"], + Grip: ["패널 너비 — 끌거나 화살표 키", "Panel width — drag or arrow keys"], +} as const; + +const tf = (key: keyof typeof TEXT): string => + TEXT[key][currentLanguageIndex as 0 | 1] ?? TEXT[key][0]; + +export interface FlowPanelOpts { + logic: CfLogic; + readOnly?: boolean; + title: string; + /** 고칠 때마다 — 객체 그대로(페이지 캐시에 쌓음 · [저장] 은 페이지) */ + onChange(lg: CfLogic): void; + /** 다른 로직 입력이 부르는 로직 */ + logics?(key: string): Promise; + /** 패널 너비가 바뀜(끌기 · 접기 · 펴기) — 부르는 쪽이 칸 너비를 맞춤 */ + onResize?(px: number): void; +} + +export interface FlowPanel { + /** 밖에서 바꿈(고친 것 버리기 · 다른 로직 열기) */ + set(lg: CfLogic): void; + destroy(): void; + /** 크게 보기 열기 */ + expand(): void; +} + +/* ── 너비(DOM 없음) ─────────────────────────────────────────────────── */ + +export const PANEL_MIN = 320; +export const STRIP_WIDTH = 40; +/** 좁은 패널에서 저절로 맞출 때 가장 작은 확대 */ +export const PANEL_FIT_MIN = 0.5; +const WIDTH_KEY = "m02.dfpanel.width"; +const FOLD_KEY = "m02.dfpanel.folded"; + +/** 끌어서 정한 너비 — 왼쪽 가장자리를 왼쪽으로 끌면 넓어짐 · 가장 작게 PANEL_MIN · 가장 크게 화면의 85% */ +export function dragWidth(start: number, startX: number, x: number, viewport: number): number { + const max = Math.max(PANEL_MIN, Math.round(viewport * 0.85)); + return Math.min(max, Math.max(PANEL_MIN, Math.round(start + (startX - x)))); +} + +/** 저장해 둔 너비 — 숫자가 아니거나 너무 작으면 null */ +export function storedWidth(raw: string | null): number | null { + const n = Number(raw); + return raw && Number.isFinite(n) && n >= PANEL_MIN ? Math.round(n) : null; +} + +const readStore = (key: string): string | null => { + try { + return localStorage.getItem(key); + } catch { + return null; + } +}; +const writeStore = (key: string, value: string): void => { + try { + localStorage.setItem(key, value); + } catch { + /* 저장 칸이 막힌 브라우저 — 이번 화면에서만 */ + } +}; + +/* ── 패널 ───────────────────────────────────────────────────────────── */ + +export function mountFlowPanel(host: HTMLElement, opts: FlowPanelOpts): FlowPanel { + const paper = createPaper({ + logic: opts.logic, + readOnly: opts.readOnly, + onChange: opts.onChange, + logics: opts.logics, + // 좁은 패널은 「한눈에」 가 되게 더 작게 · 크게 보기는 글이 읽히게 90% 아래로 안 내림 + fitMin: () => (full ? 0.9 : PANEL_FIT_MIN), + }); + const { diagram, detail, summary, note, undo } = paper.parts; + let width = storedWidth(readStore(WIDTH_KEY)); + let folded = readStore(FOLD_KEY) === "1"; + let full: HTMLElement | null = null; + const tell = (): void => opts.onResize?.(folded ? STRIP_WIDTH : (width ?? host.clientWidth)); + + const sm = (label: string, onClick: () => void, variant: "ghost" | "filled" = "ghost") => + createButton({ label, size: "sm", variant, onClick }); + + /* 펼친 패널 */ + const costBox = el("details", { + className: "m02-dfp__cost", + attrs: { open: "" }, + children: [el("summary", { text: tf("Cost") })], + }); + const tools = el("div", { className: "m02-dfp__tools", children: [undo] }); + const open = el("div", { + className: "m02-dfp__open", + children: [ + el("header", { + className: "m02-dfp__head", + children: [ + el("strong", { className: "m02-dfp__title", text: opts.title }), + sm(tf("Big"), () => expand()), + sm(tf("Fold"), () => setFolded(true)), + ], + }), + tools, + note, + diagram, + detail, + costBox, + ], + }); + costBox.append(summary); + + /* 접힌 띠 */ + const strip = el("button", { + className: "m02-dfp__strip", + text: tf("Strip"), + attrs: { type: "button", title: tf("StripTitle"), "aria-label": tf("StripTitle") }, + }); + strip.addEventListener("click", () => setFolded(false)); + + /* 왼쪽 가장자리 — 끌기 · 화살표 키 */ + const grip = el("div", { + className: "m02-dfp__grip", + attrs: { + role: "separator", + "aria-orientation": "vertical", + "aria-label": tf("Grip"), + tabindex: "0", + }, + }); + const setWidth = (px: number, save: boolean): void => { + width = px; + if (save) writeStore(WIDTH_KEY, String(px)); + tell(); + }; + grip.addEventListener("pointerdown", (ev) => { + if (ev.button !== 0) return; + ev.preventDefault(); + const start = root.getBoundingClientRect().width; + const startX = ev.clientX; + root.classList.add("is-resizing"); + const move = (m: PointerEvent): void => + setWidth(dragWidth(start, startX, m.clientX, window.innerWidth), false); + const up = (u: PointerEvent): void => { + window.removeEventListener("pointermove", move); + window.removeEventListener("pointerup", up); + root.classList.remove("is-resizing"); + setWidth(dragWidth(start, startX, u.clientX, window.innerWidth), true); + paper.refit(); + }; + window.addEventListener("pointermove", move); + window.addEventListener("pointerup", up); + }); + grip.addEventListener("keydown", (ev) => { + const step = ev.shiftKey ? 80 : 20; + const d = ev.key === "ArrowLeft" ? step : ev.key === "ArrowRight" ? -step : 0; + if (!d) return; + ev.preventDefault(); + const now = root.getBoundingClientRect().width; + setWidth(dragWidth(now, 0, -d, window.innerWidth), true); + }); + + const root = el("aside", { className: "m02-dfp", children: [grip, open, strip] }); + root.addEventListener("keydown", (ev) => { + if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === "z" && !ev.shiftKey) { + if ((ev.target as HTMLElement).closest("input, textarea, [contenteditable]")) return; + ev.preventDefault(); + paper.undo(); + } + }); + + const setFolded = (on: boolean): void => { + folded = on; + writeStore(FOLD_KEY, on ? "1" : "0"); + root.classList.toggle("is-folded", on); + open.hidden = on; + strip.hidden = !on; + grip.hidden = on; + tell(); + if (!on) paper.refit(); + }; + + /* 크게 보기 — 같은 판을 화면 전체로 옮겨 끼우고 닫으면 되돌림 */ + const expand = (): void => { + if (full) return; + const close = sm(tf("Close"), () => shrink()); + full = el("div", { + className: "m02-dfp-full", + attrs: { role: "dialog", "aria-modal": "true", "aria-label": opts.title, tabindex: "-1" }, + children: [ + el("header", { + className: "m02-dfp-full__head", + children: [el("strong", { text: opts.title }), undo, close], + }), + note, + el("div", { className: "m02-dfp-full__diagram", children: [diagram] }), + el("div", { + className: "m02-dfp-full__lower", + children: [ + el("section", { children: [el("h3", { text: tf("Detail") }), detail] }), + el("section", { children: [el("h3", { text: tf("Cost") }), summary] }), + ], + }), + ], + }); + full.addEventListener("keydown", (ev) => { + if (ev.key === "Escape" && !ev.defaultPrevented) shrink(); + if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === "z" && !ev.shiftKey) { + if ((ev.target as HTMLElement).closest("input, textarea, [contenteditable]")) return; + ev.preventDefault(); + paper.undo(); + } + }); + document.body.append(full); + full.focus(); + paper.refit(); + }; + const shrink = (): void => { + if (!full) return; + full.remove(); + full = null; + tools.append(undo); + open.insertBefore(note, diagram); + open.insertBefore(diagram, detail); + open.insertBefore(detail, costBox); + costBox.append(summary); + paper.refit(); + }; + + host.replaceChildren(root); + setFolded(folded); + + return { + set: (lg) => paper.set(lg), + destroy: () => { + shrink(); + root.remove(); + }, + expand, + }; +} diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Paper.ts b/M02_MasterTemplete/DataFlow/M02_DF_Paper.ts index 6a90118bb..85146e7f4 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Paper.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Paper.ts @@ -53,6 +53,8 @@ export interface PaperOpts { onChange(lg: CfLogic): void; /** 다른 로직 입력이 부르는 로직 — 없으면 흐름도 저장본(DF-) 만 */ logics?(key: string): Promise; + /** 저절로 맞출 때 가장 작은 확대 — 부를 때마다 물음(패널 · 크게 보기가 다름) */ + fitMin?(): number; } export interface Paper { @@ -304,6 +306,7 @@ export function createPaper(opts: PaperOpts): Paper { breaks: broken, readOnly, fitKey, + fitMin: opts.fitMin?.(), }); drawPanel(); renderSummary(summaryHost, lg, result, { change: editExample, select: choose }, { values }); diff --git a/resources/tester/test_m02_df_panel.py b/resources/tester/test_m02_df_panel.py new file mode 100644 index 000000000..0356e18d8 --- /dev/null +++ b/resources/tester/test_m02_df_panel.py @@ -0,0 +1,83 @@ +"""오른쪽 흐름도 패널(`M02_DF_Panel.ts`) 순수 부분 — 끌어서 정한 너비 · 저장해 둔 너비. + +화면 TS 를 그 자리에서 트랜스파일해 node 로 돌림(부품 · 판은 빈 흉내 · 순수 함수만). +""" + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +SOURCE = ROOT / "M02_MasterTemplete" / "DataFlow" / "M02_DF_Panel.ts" + +RUNNER = r""" +const fs = require("fs"); +const path = require("path"); +const [root, file] = process.argv.slice(1); +const ts = require(path.join(root, "config", "node_modules", "typescript")); +const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, +}).outputText; +const stub = new Proxy({}, { get: () => () => undefined }); +const box = { exports: {} }; +new Function("exports", "module", "require", js)(box.exports, box, () => stub); +const m = box.exports; +const jobs = JSON.parse(fs.readFileSync(0, "utf8")); +const out = jobs.map((j) => + j.op === "drag" ? m.dragWidth(j.start, j.startX, j.x, j.viewport) + : j.op === "stored" ? m.storedWidth(j.raw) + : j.op === "consts" ? [m.PANEL_MIN, m.STRIP_WIDTH] + : null, +); +process.stdout.write(JSON.stringify(out)); +""" + + +def run(jobs: list[dict]) -> list: + if shutil.which("node") is None: + pytest.skip("node 가 없음") + done = subprocess.run( + ["node", "-e", RUNNER, str(ROOT), str(SOURCE)], + input=json.dumps(jobs, ensure_ascii=False), + capture_output=True, + text=True, + encoding="utf-8", + check=True, + ) + return json.loads(done.stdout) + + +def test_왼쪽_가장자리_끌기_너비(): + [consts, wider, narrower, floor, ceil] = run( + [ + {"op": "consts"}, + # 왼쪽으로 100 끌면 넓어짐 + {"op": "drag", "start": 480, "startX": 1000, "x": 900, "viewport": 1600}, + # 오른쪽으로 끌면 좁아짐 + {"op": "drag", "start": 480, "startX": 1000, "x": 1060, "viewport": 1600}, + # 너무 좁게 — 가장 작은 너비에서 멈춤 + {"op": "drag", "start": 480, "startX": 1000, "x": 1400, "viewport": 1600}, + # 너무 넓게 — 화면의 85% 에서 멈춤 + {"op": "drag", "start": 480, "startX": 1000, "x": -2000, "viewport": 1600}, + ] + ) + min_width, strip = consts + assert (min_width, strip) == (320, 40) + assert wider == 580 and narrower == 420 + assert floor == min_width and ceil == 1360 + + +def test_저장해_둔_너비(): + got = run( + [ + {"op": "stored", "raw": "640"}, + {"op": "stored", "raw": "640.6"}, + {"op": "stored", "raw": None}, + {"op": "stored", "raw": "abc"}, + {"op": "stored", "raw": "100"}, + ] + ) + assert got == [640, 641, None, None, None] From 14547d4f2002467edf3787c22995252ce277e9a9 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 23:35:09 +0900 Subject: [PATCH 2/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=E2=80=94=20=ED=81=AC=EA=B2=8C=20=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=EB=A5=BC=20=EB=8B=AB=EC=9C=BC=EB=A9=B4=20=ED=8C=90?= =?UTF-8?q?=EC=9D=84=20=EC=B2=98=EC=9D=8C=20=EC=B0=A8=EB=A1=80=EB=A1=9C=20?= =?UTF-8?q?=EB=90=98=EB=8F=8C=EB=A0=A4=20=EB=81=BC=EC=9B=80=20=C2=B7=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=20=EB=95=8C=20=EB=B3=80=EC=88=98=20=EC=B0=A8?= =?UTF-8?q?=EB=A1=80=20=C2=B7=20=EC=A2=81=EC=9D=80=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=A4=20=EB=B0=94=EB=8B=A5=200.25(PLAN=2036-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ORCA(5183): 540px 칸 상자 11 모두 판 안(0.288) · 머리 끌기 → onChange 새 객체 1 번 · 크게 보기 0.9 · 닫으면 패널 0.288 · 고친 자리 그대로 · 실행 취소 → onChange 되돌린 객체 · 접기 띠 「흐름도 ▸」 onResize 40 · 펴기 · 가장자리 끌기 +120 → onResize 764 · localStorage · 보기만: 더하기 0 · 끌기 onChange 0 · 고르기 됨 · 판 칸 막힘 · 여러 조건 이름표 · 밝음 · 어두움 - resources/master_data 안 바뀜(git status) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z --- M02_MasterTemplete/DataFlow/M02_DF_Panel.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Panel.ts b/M02_MasterTemplete/DataFlow/M02_DF_Panel.ts index ade890724..de5d49f87 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Panel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Panel.ts @@ -53,7 +53,7 @@ export interface FlowPanel { export const PANEL_MIN = 320; export const STRIP_WIDTH = 40; /** 좁은 패널에서 저절로 맞출 때 가장 작은 확대 */ -export const PANEL_FIT_MIN = 0.5; +export const PANEL_FIT_MIN = 0.25; const WIDTH_KEY = "m02.dfpanel.width"; const FOLD_KEY = "m02.dfpanel.folded"; @@ -87,6 +87,8 @@ const writeStore = (key: string, value: string): void => { /* ── 패널 ───────────────────────────────────────────────────────────── */ export function mountFlowPanel(host: HTMLElement, opts: FlowPanelOpts): FlowPanel { + /** 크게 보기 화면 — 열려 있을 때만 */ + let full: HTMLElement | null = null; const paper = createPaper({ logic: opts.logic, readOnly: opts.readOnly, @@ -98,7 +100,6 @@ export function mountFlowPanel(host: HTMLElement, opts: FlowPanelOpts): FlowPane const { diagram, detail, summary, note, undo } = paper.parts; let width = storedWidth(readStore(WIDTH_KEY)); let folded = readStore(FOLD_KEY) === "1"; - let full: HTMLElement | null = null; const tell = (): void => opts.onResize?.(folded ? STRIP_WIDTH : (width ?? host.clientWidth)); const sm = (label: string, onClick: () => void, variant: "ghost" | "filled" = "ghost") => @@ -241,10 +242,16 @@ export function mountFlowPanel(host: HTMLElement, opts: FlowPanelOpts): FlowPane full.remove(); full = null; tools.append(undo); - open.insertBefore(note, diagram); - open.insertBefore(diagram, detail); - open.insertBefore(detail, costBox); costBox.append(summary); + // 머리 · 실행 취소 줄 뒤에 판을 처음 차례대로 되돌려 끼움 + open.replaceChildren( + open.firstElementChild as HTMLElement, + tools, + note, + diagram, + detail, + costBox, + ); paper.refit(); };