fix(m01-sl): 품셈 표 바깥 값 열 차례를 Table 과 같게 — 범위 조건만 두 칸 · 값 칸은 한 칸

- 조건 칸(범위 조건은 「·아래」 「·위」) → 값 칸 · 머리 줄 포함 블록을 A1 부터(M01_SL_Table_Build layoutOf · blockCells 와 같음)
- 범위 칸에 목록이 아니면 빈 칸 · 값 칸 범위 값은 「80,100」 글(JS String 과 같음)
- 시험 견본에 값 칸 범위 · 범위 아닌 칸 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
This commit is contained in:
2026-10-01 13:16:21 +09:00
co-authored by Claude Opus 5.5
parent 5f9240c9e3
commit f8d7776284
2 changed files with 34 additions and 26 deletions
+16 -12
View File
@@ -161,7 +161,10 @@ def _logic(key: str, inputs: dict, field: str | None) -> dict:
def _table(key: str) -> dict:
"""품셈 표 통째 → 머리 줄 + 줄(범위 · 범위 값은 「이름·아래」 「이름·위」 두 칸)."""
"""품셈 표 통째 → 머리 줄 + 줄 — `M01_SL_Table_Build.layoutOf · blockCells` 와 같은 열 차례.
조건 칸(범위 조건은 「이름·아래」 「이름·위」 두 칸) → 값 칸(늘 한 칸 · 범위 값은 「아래,위」).
"""
files = store.loaded()[0]
table = next(
(
@@ -175,24 +178,25 @@ def _table(key: str) -> dict:
)
if table is None:
return {"ok": False, "reason": f"없는 표 「{key}」"}
names = [*(table.get("조건") or {}), *(table.get("값칸") or {})]
split = {
n
for n in names
if (table.get("조건") or {}).get(n) == "범위"
or any(isinstance(r.get(n), list) for r in table.get("줄") or [])
}
conds = table.get("조건") or {}
names = [*conds, *(table.get("값칸") or {})]
split = {n for n in conds if conds[n] == "범위"}
head = [h for n in names for h in ((n + RANGE_LOW, n + RANGE_HIGH) if n in split else (n,))]
def plain(v: Any) -> Any: # JS String(배열) 과 같게 — 「80,100」 · 빈 끝은 빈 글
if isinstance(v, list):
return ",".join("" if x is None else str(x) for x in v)
return None if v == "" else v
rows = []
for r in table.get("줄") or []:
line = []
for n in names:
v = r.get(n)
if n in split:
low, high = v if isinstance(v, list) else (v, v)
line += [low, high]
if n in split: # 범위 칸 — 목록이 아니면 둘 다 빈 칸
line += list(v) if isinstance(v, list) else [None, None]
else:
line.append(v if not isinstance(v, (list, dict)) else str(v))
line.append(plain(v))
rows.append(line)
return {"ok": True, "rows": [head, *rows], "name": table.get("이름"), "unit": table.get("기준")}
+18 -14
View File
@@ -36,10 +36,10 @@ TABLE = {
"출처": "자체",
"조건": {"돌": "고르기", "높이": "범위"},
"범위규칙": "초과·이하",
"값칸": {"석공": "인"},
"값칸": {"석공": "인", "증가율": "%"},
"줄": [
{"돌": "깬돌", "높이": [None, 3], "석공": 0.5},
{"돌": "깬돌", "높이": [3, None], "석공": 0.7},
{"돌": "깬돌", "높이": [None, 3], "석공": 0.5, "증가율": [80, 100]},
{"돌": "견치돌", "높이": 4, "석공": 0.7},
],
"주": [],
}
@@ -158,15 +158,10 @@ def test_저장_열기_목록_지우기_서버가_다시_셈(client: TestClient,
assert Decimal(book["계산값"]["s1"]["B1"]["수"]) == 2 * price # 서버 다시 셈(999 는 버림)
assert "C1" not in cells # 출처 없는 바깥 알약은 안 넣음
table = book["시트"][1]["칸"]
assert [table[c]["값"] for c in ("A1", "B1", "C1", "D1")] == [
"돌",
"높이·아래",
"높이·위",
"석공",
]
assert (
"B2" not in table and table["C2"]["값"] == 3 and table["D3"]["값"] == 0.7
) # 빈 끝은 빈 칸
head = [table[c]["값"] for c in ("A1", "B1", "C1", "D1", "E1")]
assert head == ["돌", "높이·아래", "높이·위", "석공", "증가율"] # 머리 줄 · A1 부터
assert "B2" not in table and table["C2"]["값"] == 3 and table["E2"]["값"] == "80,100"
assert "B3" not in table and "C3" not in table and table["D3"]["값"] == 0.7 # 범위 아님 → 빈 칸
assert saved["logic"]["edges"] == _logic(labor)["edges"]
answers = {v["id"]: v for v in saved["values"]}
assert set(answers) == {"n1!A1", "n2!A1", "n1:노무비"} and all(
@@ -221,8 +216,17 @@ def test_바깥_값_표_노임_로직_틀린_종류(client: TestClient) -> None:
{"id": "z", "kind": "모름"},
]
got = {v["id"]: v for v in _post(client, "/api/m01/sl/outside", {"items": items})["values"]}
assert got["t"]["rows"][0] == ["돌", "높이·아래", "높이·위", "석공"]
assert got["t"]["rows"][1:] == [["깬돌", None, 3, 0.5], ["깬돌", 3, None, 0.7]]
assert got["t"]["rows"][0] == [
"돌",
"높이·아래",
"높이·위",
"석공",
"증가율",
] # sub3 layoutOf 차례
assert got["t"]["rows"][1:] == [
["깬돌", None, 3, 0.5, "80,100"],
["견치돌", None, None, 0.7, None],
]
assert Decimal(str(got["l"]["value"])) == price and got["l"]["unit"]
assert Decimal(str(got["g"]["value"])) == Decimal("0.5") * price
assert got["g2"]["value"] == 0