feat(m02-cf): 함수 상자에 놓으면 받는 알약을 새로 만들어 이음
- link: 받는 쪽이 함수 상자이고 그 이름 받는 알약이 없으면 새로 만듦(빈 이름이면 보낸 알약 이름 · 같으면 「이름 (2)」) · func.inputs 에 칸 없이 더함 - unlink · removeBox: 받는 알약이 칸에 안 놓였고 다른 선도 없으면 같이 지움 - 검사: 칸에 놓지 않은 받는 알약 알림 - 시험 13개 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk
This commit is contained in:
@@ -53,6 +53,10 @@ const TEXT = {
|
||||
],
|
||||
NoCell: ["「{box}」 의 출력 칸이 정해지지 않음", "Output cell of 「{box}」 is not set"],
|
||||
NoOutput: ["출력 상자가 없음", "There is no output box"],
|
||||
NoInCell: [
|
||||
"「{box}」 의 받는 알약 「{pill}」 을 칸에 놓지 않음",
|
||||
"Pill 「{pill}」 of 「{box}」 is not placed in a cell",
|
||||
],
|
||||
NoValue: ["「{box}」 의 「{pill}」 값이 아직 없음", "「{pill}」 of 「{box}」 has no value yet"],
|
||||
} as const;
|
||||
|
||||
@@ -193,6 +197,8 @@ export function checkLogic(lg: CfLogic, values: CfValues = {}): string[] {
|
||||
out.push(tl("EmptyIn", { box: b.name, pill: p.name }));
|
||||
if (b.kind === "function" && !parseA1(b.func?.output.cell ?? ""))
|
||||
out.push(tl("NoCell", { box: b.name }));
|
||||
for (const i of b.func?.inputs ?? [])
|
||||
if (!parseA1(i.cell)) out.push(tl("NoInCell", { box: b.name, pill: i.port }));
|
||||
}
|
||||
if (!lg.boxes.some((b) => b.kind === "output")) out.push(tl("NoOutput"));
|
||||
const ev = evalLogic(lg, values);
|
||||
|
||||
@@ -201,11 +201,10 @@ export function addBox(
|
||||
|
||||
/** 상자 지우기 — 이어진 선도 지움 */
|
||||
export function removeBox(lg: CfLogic, id: string): CfLogic {
|
||||
return {
|
||||
...lg,
|
||||
boxes: lg.boxes.filter((b) => b.id !== id),
|
||||
links: lg.links.filter((l) => l.from.box !== id && l.to.box !== id),
|
||||
};
|
||||
// 선을 하나씩 끊어 받는 쪽 함수 상자의 칸 없는 알약도 정리
|
||||
let next = lg;
|
||||
for (const l of lg.links) if (l.from.box === id || l.to.box === id) next = unlink(next, l.id);
|
||||
return { ...next, boxes: next.boxes.filter((b) => b.id !== id) };
|
||||
}
|
||||
|
||||
export function moveBox(lg: CfLogic, id: string, x: number, y: number): CfLogic {
|
||||
@@ -267,7 +266,9 @@ export function reaches(lg: CfLogic, from: string, to: string): boolean {
|
||||
return walk(from);
|
||||
}
|
||||
|
||||
/** 선 잇기 — 입력 → 함수 · 출력 / 함수 → 함수 · 출력 · 출력에서 나감 없음 · 순환 · 같은 받는 자리 두 번 막음 */
|
||||
/** 선 잇기 — 입력 → 함수 · 출력 / 함수 → 함수 · 출력 · 출력에서 나감 없음 · 순환 · 같은 받는 자리 두 번 막음
|
||||
* 받는 쪽이 함수 상자이고 to.name 의 받는 알약이 없으면 새로 만듦(to.name 이 빈 글이면 보낸 알약 이름 ·
|
||||
* 같은 이름이 있으면 「이름 (2)」) · func.inputs 에 칸 없이(`cell: ""`) 더함 — 칸은 함수 판에서 놓음 */
|
||||
export function link(lg: CfLogic, from: CfPort, to: CfPort): { lg: CfLogic; why: string | null } {
|
||||
const a = boxOf(lg, from.box);
|
||||
const b = boxOf(lg, to.box);
|
||||
@@ -275,21 +276,48 @@ export function link(lg: CfLogic, from: CfPort, to: CfPort): { lg: CfLogic; why:
|
||||
if (a.kind === "output") return { lg, why: tl("FromOutput") };
|
||||
if (b.kind === "input") return { lg, why: tl("ToInput") };
|
||||
if (a.id === b.id) return { lg, why: tl("Self") };
|
||||
if (!portsOut(a).some((p) => p.name === from.name) || !portsIn(b).some((p) => p.name === to.name))
|
||||
return { lg, why: tl("NoPill") };
|
||||
if (!portsOut(a).some((p) => p.name === from.name)) return { lg, why: tl("NoPill") };
|
||||
const has = portsIn(b).some((p) => p.name === to.name);
|
||||
if (!has && b.kind !== "function") return { lg, why: tl("NoPill") };
|
||||
if (reaches(lg, b.id, a.id)) return { lg, why: tl("Loop") };
|
||||
if (lg.links.some((l) => l.to.box === to.box && l.to.name === to.name))
|
||||
if (has && lg.links.some((l) => l.to.box === to.box && l.to.name === to.name))
|
||||
return { lg, why: tl("Taken") };
|
||||
let next = lg;
|
||||
let port = to.name;
|
||||
if (!has) {
|
||||
const used = portsIn(b).map((p) => p.name);
|
||||
const base = to.name.trim() || from.name;
|
||||
port = used.includes(base) ? uniqueSuffix(used, base) : base;
|
||||
const func = b.func!;
|
||||
next = updateBox(lg, b.id, { func: { ...func, inputs: [...func.inputs, { port, cell: "" }] } });
|
||||
}
|
||||
const id = nextId(
|
||||
"l",
|
||||
lg.links.map((l) => l.id),
|
||||
next.links.map((l) => l.id),
|
||||
);
|
||||
return {
|
||||
lg: { ...lg, links: [...lg.links, { id, from: { ...from }, to: { ...to } }] },
|
||||
lg: {
|
||||
...next,
|
||||
links: [...next.links, { id, from: { ...from }, to: { box: to.box, name: port } }],
|
||||
},
|
||||
why: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 같은 이름이면 「이름 (2)」 「이름 (3)」 */
|
||||
function uniqueSuffix(used: string[], base: string): string {
|
||||
for (let n = 2; ; n++) if (!used.includes(`${base} (${n})`)) return `${base} (${n})`;
|
||||
}
|
||||
|
||||
/** 선 끊기 — 받는 쪽이 함수 상자이고 그 받는 알약이 칸에 안 놓였으면(cell 빈 글) 알약도 같이 지움 */
|
||||
export function unlink(lg: CfLogic, id: string): CfLogic {
|
||||
return { ...lg, links: lg.links.filter((l) => l.id !== id) };
|
||||
const gone = lg.links.find((l) => l.id === id);
|
||||
const next = { ...lg, links: lg.links.filter((l) => l.id !== id) };
|
||||
const box = gone && boxOf(next, gone.to.box);
|
||||
if (!gone || !box?.func) return next;
|
||||
const port = box.func.inputs.find((i) => i.port === gone.to.name);
|
||||
const still = next.links.some((l) => l.to.box === box.id && l.to.name === gone.to.name);
|
||||
if (!port || port.cell.trim() || still) return next;
|
||||
const inputs = box.func.inputs.filter((i) => i !== port);
|
||||
return updateBox(next, box.id, { func: { ...box.func, inputs } });
|
||||
}
|
||||
|
||||
@@ -272,3 +272,53 @@ def test_알약_이름_규칙() -> None:
|
||||
"out(M.portsOut(lg.boxes[0]).map(p => p.name));"
|
||||
)
|
||||
assert got == ["시멘트", "표", "길이"]
|
||||
|
||||
|
||||
def test_새_함수_상자에_놓으면_받는_알약이_생김() -> None:
|
||||
got = _run(
|
||||
ONE + "r = add(lg, 'function'); lg = r.lg; const g = r.box;\n"
|
||||
"let a = M.link(lg, { box: man.id, name: '석공' }, { box: g.id, name: '' });\n"
|
||||
"let b = M.link(a.lg, { box: area.id, name: '면적' }, { box: g.id, name: '' });\n"
|
||||
"let c = M.link(b.lg, { box: f.id, name: '노무비' }, { box: g.id, name: '석공' });\n"
|
||||
"let d = M.link(c.lg, { box: f.id, name: '노무비' }, { box: g.id, name: '' });\n"
|
||||
"const gb = (x) => x.boxes.find(y => y.id === g.id);\n"
|
||||
"const inputs = gb(d.lg).func.inputs;\n"
|
||||
"const check = E.checkLogic(d.lg, V).filter(x => x.includes('칸에 놓지 않음'));\n"
|
||||
"const placed = M.updateBox(d.lg, g.id, { func: { ...gb(d.lg).func, inputs: inputs.map(i => i.port === '면적' ? { ...i, cell: 'B2' } : i) } });\n"
|
||||
"const cutAll = placed.links.filter(l => l.to.box === g.id).reduce((x, l) => M.unlink(x, l.id), placed);\n"
|
||||
"const removed = M.removeBox(d.lg, man.id);\n"
|
||||
"out([a.why, b.why, c.why, d.why, inputs, M.portsIn(gb(d.lg)).map(p => p.name), d.lg.links.slice(-2).map(l => l.to),"
|
||||
" check, gb(cutAll).func.inputs, gb(removed).func.inputs.map(i => i.port),"
|
||||
" M.link(lg, { box: man.id, name: '석공' }, { box: o.id, name: '없음' }).why]);"
|
||||
)
|
||||
a, b, c, d, inputs, ports, last_to, check, cut_all, removed, to_output = got
|
||||
assert (a, b, c, d) == (None, None, "이미 선이 들어온 자리", None)
|
||||
assert inputs == [
|
||||
{"port": "석공", "cell": ""},
|
||||
{"port": "면적", "cell": ""},
|
||||
{"port": "노무비", "cell": ""},
|
||||
]
|
||||
assert ports == ["석공", "면적", "노무비"]
|
||||
assert last_to == [{"box": "b5", "name": "면적"}, {"box": "b5", "name": "노무비"}]
|
||||
assert check == [
|
||||
"「함수 2」 의 받는 알약 「석공」 을 칸에 놓지 않음",
|
||||
"「함수 2」 의 받는 알약 「면적」 을 칸에 놓지 않음",
|
||||
"「함수 2」 의 받는 알약 「노무비」 을 칸에 놓지 않음",
|
||||
]
|
||||
assert cut_all == [{"port": "면적", "cell": "B2"}] # 칸에 놓인 것만 남음
|
||||
assert removed == ["면적", "노무비"] # 석공 상자를 지우면 칸 없는 「석공」 받는 알약도 지움
|
||||
assert to_output == "알약이 없음" # 출력 상자는 받는 알약을 새로 만들지 않음
|
||||
|
||||
|
||||
def test_같은_이름_받는_알약은_번호() -> None:
|
||||
got = _run(
|
||||
ONE + "r = add(lg, 'function'); lg = r.lg; const g = r.box;\n"
|
||||
"let x = M.link(lg, { box: man.id, name: '석공' }, { box: g.id, name: '' }).lg;\n"
|
||||
"x = M.link(x, { box: f.id, name: '노무비' }, { box: g.id, name: '' }).lg;\n"
|
||||
"r = add(x, 'input'); x = r.lg; const m2 = r.box;\n"
|
||||
"x = M.updateBox(x, m2.id, { input: { mode: 'group', group: '인력', elements: [{ ref: 'LB1', name: '석공', spec: '', unit: '인' }], variables: [] } });\n"
|
||||
"x = M.link(x, { box: m2.id, name: '석공' }, { box: g.id, name: '' }).lg;\n"
|
||||
"x = M.link(x, { box: area.id, name: '면적' }, { box: g.id, name: '할증' }).lg;\n"
|
||||
"out(x.boxes.find(y => y.id === g.id).func.inputs.map(i => i.port));"
|
||||
)
|
||||
assert got == ["석공", "노무비", "석공 (2)", "할증"]
|
||||
|
||||
Reference in New Issue
Block a user