feat(m01-sl): 표 상자 이음매 — 블록은 표 알약 칸부터 · 줄면 옛 줄 지움 · 머리 줄 바뀜 알림

- 표 블록 왼위 = 그 표 상자의 「표」 바깥 값 알약 칸(A1 · E1 무엇이든) · applyOutside 가 그 칸부터 머리 줄 포함 채움
- 표가 줄었으면 블록 열 아래 남은 옛 값 칸을 지움(찾기 판 수식은 블록 열 밖이라 그대로)
- answersToValues(cells, 답, sl) — 시트 머리 줄과 받은 머리 줄이 다르면 쉬운 말
- addNode(table) = 빈 시트(Table 이 블록 · 찾기 판 · 알약을 채움 — 틀 칸이 블록과 겹치지 않게)
- 시험 15개(Table_Build 와 잇는 시험 더함)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk
This commit is contained in:
2026-10-01 13:17:13 +09:00
co-authored by Claude Opus 5.5
parent 8e2d8d6bb3
commit ebf51d8b35
3 changed files with 93 additions and 16 deletions
+51 -1
View File
@@ -222,7 +222,7 @@ def test_바깥값_칸목록과_표_덩어리() -> None:
]
assert (e1, f2, e3, f3) == ({"값": "뒷길이"}, {"값": 0.15}, {"값": 30}, None)
assert lg == {"결과": 12.5}
assert "「메쌓기 표」의 입력 알약 「조건」이 비어 있음" in check
assert check == []
def test_알약_정하기_이름바꾸면_선이_따라감() -> None:
@@ -361,3 +361,53 @@ def test_항목_틀은_Item_노무_틀과_같은_칸() -> None:
["단가", "B5"], ["금액", "B6"], ["노무비", "B7"], ["재료비", "B8"], ["경비", "B9"], ["사용 수량", "B10"],
] # fmt: skip
assert ref == '{"what":"노무"}'
# 표 상자 이음매 — Table_Build(sub3) 가 짠 품셈 표 상자에 서버 꼴 블록(머리 줄 + 줄 · A1 부터)을 넣고 찾기
TABLE = r"""
const T = loadTs(path.join(ROOT, "M01_MasterData", "SheetLogic", "M01_SL_Table_Build.ts"));
let sl = M.newLogic("표 시험");
let r = M.addNode(sl, "table", "메쌓기"); sl = r.sl; const tb = r.node;
const info = { key: "QF000421", name: "메쌓기", base: "1㎡", source: "", no: "13-4-1",
conds: { 뒷길이: "수", 돌: "고르기" }, vals: { 석공: "인" }, rule: null, notes: [],
rows: [{ 뒷길이: 25, 돌: "깬잡석", 석공: 0.15 }, { 뒷길이: 30, 돌: "깬돌", 석공: 0.26 },
{ 뒷길이: 35, 돌: "깬돌", 석공: 0.3 }] };
sl = T.applyTable(sl, tb.id, info, { cond: ["뒷길이", "돌"], out: ["석공"] },
{ ref: "QF000421", fillBlock: false, outside: true }).sl;
r = M.addNode(sl, "input", "뒷길이"); sl = r.sl; const len = r.node;
r = M.addNode(sl, "input", "돌"); sl = r.sl; const stone = r.node;
sl = E.applyOutside(sl, [{ node: len.id, cell: "B2", value: 35 }, { node: stone.id, cell: "B2", value: "깬돌" }]);
sl = link(sl, len, "뒷길이", sl.nodes.find(n => n.id === tb.id), "뒷길이");
sl = link(sl, stone, "돌", sl.nodes.find(n => n.id === tb.id), "돌");
const ROWS = [["뒷길이", "돌", "석공"], [25, "깬잡석", 0.15], [30, "깬돌", 0.26], [35, "깬돌", 0.3]];
const cells = E.outsideCells(sl);
"""
def test_표_상자_바깥값_블록_찾기_줄면_옛줄_지움_머리바뀜_알림() -> None:
got = _run(
TABLE
+ "const first = E.answersToValues(cells, [{ id: cells[0].id, ok: true, rows: ROWS }], sl);\n"
"const s1 = E.applyOutside(sl, first.values);\n"
"const shrink = E.answersToValues(cells, [{ id: cells[0].id, ok: true, rows: ROWS.slice(0, 3) }], s1);\n"
"const s2 = E.applyOutside(s1, shrink.values);\n"
"const moved = E.answersToValues(cells, [{ id: cells[0].id, ok: true,"
" rows: [['돌', '뒷길이', '석공'], ['깬돌', 30, 0.26]] }], s1);\n"
"out([cells, first.errors, E.evalLogic(s1).pills[tb.id], shrink.errors, sheet(s2, tb.id).칸.A4 ?? null,"
" sheet(s2, tb.id).칸.C4 ?? null, E.evalLogic(s2).pills[tb.id], moved.errors]);"
)
cells, first_err, found, shrink_err, a4, c4, gone, moved_err = got
assert cells == [
{
"id": "n1!A1",
"node": "n1",
"cell": "A1",
"kind": "table",
"ref": "QF000421",
"source": {"kind": "표", "table": "QF000421"},
}
]
assert first_err == [] and found["석공"] == 0.3 and found["표"] == "뒷길이"
assert shrink_err == [] and a4 is None and c4 is None
assert gone["석공"] == "#N/A"
assert moved_err == ["「표 · 메쌓기」 표 머리 줄이 바뀜 — 표를 다시 골라야 함"]