From cf73a51fdb32311625b78b01adbbb382bed52576 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 16:04:46 +0900 Subject: [PATCH] =?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=20700?= =?UTF-8?q?=EC=A4=84=20=EC=95=88=EC=9C=BC=EB=A1=9C=20=C2=B7=20=EC=95=8C?= =?UTF-8?q?=EC=95=BD=20=EC=9D=B4=EB=A6=84=20=EA=B7=9C=EC=B9=99(=ED=91=9C?= =?UTF-8?q?=20=C2=B7=20=EA=B0=92)=20=EB=A7=9E=EC=B6=A4(PLAN=2035-7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z --- M02_MasterTemplete/CostFlow/M02_CF_Diagram.ts | 141 +++++------------- 1 file changed, 41 insertions(+), 100 deletions(-) diff --git a/M02_MasterTemplete/CostFlow/M02_CF_Diagram.ts b/M02_MasterTemplete/CostFlow/M02_CF_Diagram.ts index 4a31666f3..e66809dba 100644 --- a/M02_MasterTemplete/CostFlow/M02_CF_Diagram.ts +++ b/M02_MasterTemplete/CostFlow/M02_CF_Diagram.ts @@ -46,20 +46,17 @@ const TEXT = { SameBox: ["같은 상자 안에서는 이을 수 없음", "Can't connect inside the same box"], NotIn: ["받는 알약에 놓아야 이어짐", "Drop it on a receiving pill"], NoMatch: [ - "이름이 같은 받는 알약이 없음 — 받을 알약에 바로 놓으세요", - "No receiving pill with that name — drop on the pill itself", - ], - Taken: [ - "이미 이어진 알약 — 먼저 그 선을 끊으세요", - "That pill already has a line — disconnect it first", + "같은 이름 받는 알약 없음 — 알약에 바로 놓으세요", + "No pill with that name — drop on a pill", ], + Taken: ["이미 이어진 알약 — 먼저 그 선을 끊으세요", "Pill already linked — disconnect first"], From: ["어디서", "From"], To: ["어디로", "To"], Value: ["지금 값", "Now"], LineOf: ["선 · {a} → {b}", "Line · {a} → {b}"], Hint: [ - "상자 머리를 끌어 옮김 · 빈 곳을 끌어 판 옮김 · 휠로 확대", - "Drag a box head to move · drag empty space to pan · wheel to zoom", + "상자 머리를 끌어 옮김 · 빈 곳 끌어 판 옮김 · 휠로 확대", + "Drag head to move · drag space to pan", ], } as const; @@ -101,7 +98,7 @@ export function pillsOf(box: CfBox): { ins: CfPill[]; outs: CfPill[] } { const inp = box.input; const outs = [ ...(inp?.elements ?? []).map((e) => ({ name: e.name, unit: e.unit })), - ...(inp?.table ? [{ name: inp.table.key, unit: "-" }] : []), + ...(inp?.table ? [{ name: "표", unit: "-" }] : []), ...(inp?.variables ?? []).map((v) => ({ name: v.name, unit: v.unit })), ]; return { ins: [], outs }; @@ -113,7 +110,7 @@ export function pillsOf(box: CfBox): { ins: CfPill[]; outs: CfPill[] } { outs: f ? [{ name: f.output.name, unit: f.output.unit }] : [], }; } - return { ins: [{ name: box.name, unit: box.unit }], outs: [] }; + return { ins: [{ name: "값", unit: box.unit }], outs: [] }; } } @@ -397,16 +394,18 @@ export function renderDiagram( while (lg.boxes.some((b) => Math.abs(b.x - c.x) < 160 && Math.abs(b.y - y) < 90)) y += 100; return { x: Math.round(c.x), y: Math.round(y) }; }; + /** 작은 단추 */ + const sm = ( + label: keyof typeof TEXT, + onClick: () => void, + variant: "ghost" | "filled" | "danger" = "ghost", + ) => createButton({ label: tt(label), size: "sm", variant, onClick }); const addBtn = (kind: CfKind, label: keyof typeof TEXT): HTMLButtonElement => - createButton({ - label: tt(label), - size: "sm", - variant: "ghost", - onClick: () => { - const at = freeSpot(); - on.add(kind, at.x, at.y); - }, + sm(label, () => { + const at = freeSpot(); + on.add(kind, at.x, at.y); }); + const endName = (e: CfPort): string => `${boxName(e.box)} · ${e.name}`; const selLink = lg.links.find((l) => l.id === st.selected) ?? null; const selBox = lg.boxes.find((b) => b.id === st.selected) ?? null; @@ -417,74 +416,34 @@ export function renderDiagram( if (st.linking) { tools.push( el("span", { text: tt("PickTarget", { v: st.linking.name }) }), - createButton({ - label: tt("Cancel"), - size: "sm", - variant: "ghost", - onClick: () => ((st.linking = null), redraw()), - }), + sm("Cancel", () => ((st.linking = null), redraw())), ); } else if (st.pill) { const p = st.pill; - const linked = lg.links.filter((l) => - p.dir === "in" - ? l.to.box === p.box && l.to.name === p.name - : l.from.box === p.box && l.from.name === p.name, - ); - const where = linked - .map((l) => - p.dir === "in" - ? `${boxName(l.from.box)} · ${l.from.name}` - : `${boxName(l.to.box)} · ${l.to.name}`, - ) + const mine = (e: CfPort): boolean => e.box === p.box && e.name === p.name; + const where = lg.links + .filter((l) => mine(p.dir === "in" ? l.to : l.from)) + .map((l) => endName(p.dir === "in" ? l.from : l.to)) .join(", "); - tools.push( - el("span", { - text: [ - p.name, - `${tt("Value")} ${showValue(values[p.box]?.[p.name]) || "—"}`, - where ? `${tt(p.dir === "in" ? "From" : "To")} ${where}` : "", - ] - .filter(Boolean) - .join(" · "), - }), - ); + const now = `${tt("Value")} ${showValue(values[p.box]?.[p.name]) || "—"}`; + const dir = where ? `${tt(p.dir === "in" ? "From" : "To")} ${where}` : ""; + tools.push(el("span", { text: [p.name, now, dir].filter(Boolean).join(" · ") })); if (p.dir === "out") tools.push( - createButton({ - label: tt("Connect"), - size: "sm", - onClick: () => { - st.linking = { box: p.box, name: p.name }; - st.pill = null; - redraw(); - }, - }), + sm( + "Connect", + () => ((st.linking = { box: p.box, name: p.name }), (st.pill = null), redraw()), + "filled", + ), ); } else if (selLink) { + const line = tt("LineOf", { a: endName(selLink.from), b: endName(selLink.to) }); tools.push( - el("span", { - text: tt("LineOf", { - a: `${boxName(selLink.from.box)} · ${selLink.from.name}`, - b: `${boxName(selLink.to.box)} · ${selLink.to.name}`, - }), - }), - createButton({ - label: tt("Disconnect"), - size: "sm", - variant: "danger", - onClick: () => on.unlink(selLink.id), - }), + el("span", { text: line }), + sm("Disconnect", () => on.unlink(selLink.id), "danger"), ); } else if (selBox) { - tools.push( - createButton({ - label: tt("Remove"), - size: "sm", - variant: "danger", - onClick: () => void askRemove(selBox), - }), - ); + tools.push(sm("Remove", () => void askRemove(selBox), "danger")); } const bar = el("div", { @@ -519,10 +478,6 @@ export function renderDiagram( }), ], }); - bar.querySelectorAll("button").forEach((b) => { - if (b.textContent === "−") b.setAttribute("aria-label", tt("ZoomOut")); - if (b.textContent === "+") b.setAttribute("aria-label", tt("ZoomIn")); - }); const root = el("div", { className: `m02-cfdg${st.linking ? " is-linking" : ""}`, children: [ @@ -606,23 +561,15 @@ export function renderDiagram( svg.append(g); } } + const firstFit = (): void => { + if (st.fitted || !lg.boxes.length || !view.clientWidth) return drawLines(); + st.fitted = true; + fit(); + }; st.watch?.disconnect(); - st.watch = - typeof ResizeObserver === "undefined" - ? undefined - : new ResizeObserver(() => { - if (!st.fitted && lg.boxes.length && view.clientWidth) { - st.fitted = true; - fit(); - } else drawLines(); - }); + st.watch = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(firstFit); st.watch?.observe(view); - if (view.isConnected) { - if (!st.fitted && lg.boxes.length && view.clientWidth) { - st.fitted = true; - fit(); - } else drawLines(); - } + if (view.isConnected) firstFit(); /* 끌기 셋 — 상자 · 판 · 선 */ function follow( @@ -726,9 +673,3 @@ export function renderDiagram( ); } } - -/** 시험 · 잇는 쪽이 판 상태를 읽을 때 — 확대 · 판 자리 */ -export function viewOf(host: HTMLElement): { zoom: number; panX: number; panY: number } | null { - const st = states.get(host); - return st ? { zoom: st.zoom, panX: st.panX, panY: st.panY } : null; -}