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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user