fix(M02): 단가산출 다이어그램 700줄 안으로 · 알약 이름 규칙(표 · 값) 맞춤(PLAN 35-7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z
This commit is contained in:
2026-10-01 16:04:46 +09:00
co-authored by Claude Opus 5.5
parent 93ac4ab22e
commit cf73a51fdb
+41 -100
View File
@@ -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<HTMLButtonElement>("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;
}