fix(b09): 원문이 줄마다 풀린 표를 못 읽어 금액이 떨어지던 것 — 직접공사비 5,983,724 복귀

랩탑 서브·안티그래비티가 원문 md 의 뭉친 줄을 줄마다 푸는 중인데(2026-09-18), 그 모양을
읽는 길이 없어 **호표가 첫 갈래 값만 남거나 표째 사라졌다.** 값이 바뀐 것이 아니라
**적는 모양만 바뀐 것**이라, 풀린 줄을 도로 뭉쳐 같은 길로 읽게 했다.

- `_ThreeAxis._glue_rows` — 세로축 값이 줄마다 갈린 표(11-1 콘테이너 · 13-5-1 돌붙임)를
  한 줄로 되뭉침. ⚠ 칸 수가 다른 줄이 섞이거나 자원 이름 줄이 안 남으면 안 뭉침
- `_ThreeAxis` 는 **두 단 머리 표(13-4-1·13-4-4)에서 손을 뗌** — 그 표는 제 길이 따로 있고,
  여기서 가로채면 대·소분류가 열 수로 안 나뉘어 표째 막힘(갈래 80 이 사라졌었음)
- `_Transposed.match_packed_rows` — 뭉침이 없어도 **값 칸이 갈래 수만큼 따로 선 줄**이면
  같은 표로 읽음(돌쌓기 13-4-2·13-4-5 · 13-5-2)
- 산림 할증률 표 31 — 자료에 박아 둔 원문 줄 번호를 지금 자리로 다시 잡음(`coef_2026.json`).
  ⚠ 그 번호는 B09 가 표 아래 [주] 원문을 읽는 데도 쓰므로 밀린 채 두면 엉뚱한 글이 붙음
- 합판 전용률 대조는 **열 이름으로** 칸을 찾게 함 — 원문 앞에 빈 칸이 늘어도 안 깨짐
- ⭐ 금액 — 4,932,972 로 떨어졌던 직접공사비가 **5,983,724** 로 복귀 · 합계 10,124,000
- 덤 — 6-7-1 교목 시비 갈래 다섯 · 13-10-2 나무 말뚝박기 두 표가 새로 섬(사라진 갈래 0)
- 못박은 수 갱신 — 산림 갈래 332 → 337 · 공종 축 620 → 624 · 건설 1,388 → 1,484 ·
  밑수 미확보 80 → 81 · 이름표 갈래 66 → 67 · 건설 갈래 195 → 207 · 열쇠 지문

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
2026-09-18 10:44:58 +09:00
co-authored by Claude Opus 5
parent 2312cbbdc4
commit 803c17163e
21 changed files with 7975 additions and 3233 deletions
@@ -164,6 +164,39 @@ def _axis_label(axis_name: str, value: str) -> str:
return f"{axis_name} {value}"
def _value_row(row: list[Any]) -> bool:
"""세로축 값 하나 + 값 칸들로 된 **풀린 줄**인가(`| 25 | 0.15 | 0.12 | …`).
⚠ 꼬리의 설명 칸(「H=2.6M」)은 값이 아니어도 된다 — 그 자리는 뒤에서 떼어 낸다.
"""
cells = [_clean(cell) for cell in row if _clean(cell)]
if len(cells) < 3 or len(_tokens(cells[0])) != 1 or not re.match(r"^\d", cells[0]):
return False
numeric = [c for c in cells[1:] if _NUMBER.match(c) or c in _ABSENT]
return len(numeric) >= 2
def _glue_rows(raw_rows: list[list[Any]]) -> list[list[Any]]:
"""**줄마다 풀린 표를 한 줄로 도로 뭉친다** — 읽는 길은 하나로 둔다.
원문 md 가 손질되며 「25 30 35 …」로 뭉쳐 있던 값줄이 줄마다 갈렸다(2026-09-18).
값이 달라진 것이 아니라 **적는 모양만 바뀐 것**이라, 뭉친 꼴로 되돌려 같은 길로 읽는다.
⚠ 칸 수가 다른 줄이 섞이면 뭉치지 않는다 — 자리를 짐작해 맞추지 않는다.
"""
block: list[list[Any]] = []
for row in reversed(raw_rows):
if not _value_row(row) or (block and len(row) != len(block[0])):
break
block.append(row)
block.reverse()
# ⚠ 값줄 위에 **자원 이름 줄이 남아야** 한다 — 표가 통째로 값줄이면 뭉치지 않는다.
if len(block) < 2 or len(block) >= len(raw_rows):
return raw_rows
width = len(block[0])
glued = [" ".join(_clean(row[i]) for row in block if _clean(row[i])) for i in range(width)]
return raw_rows[: len(raw_rows) - len(block)] + [glued]
def match_three_axis_table(
node: dict[str, Any],
table: dict[str, Any],
@@ -178,6 +211,13 @@ def match_three_axis_table(
raw_rows = [row for row in (table.get("raw_row") or []) if isinstance(row, list)]
if len(raw_rows) < 2:
return False
# ⚠ **두 단 머리 표(13-4-1·13-4-4)는 제 길이 따로 있다** — 여기서 뭉쳐 읽으면
# 대·소분류가 열 수로 안 나뉘어 표째 막히고, 그 길이 못 돌아 갈래 80 이 사라진다.
from B09_Estimation.B09_Estimation_ResourceAxis_TwoLevelHeader import TWO_LEVEL_HEADER
if str(node.get("work_item_code") or "") in TWO_LEVEL_HEADER:
return False
raw_rows = _glue_rows(raw_rows)
header = list(table.get("condition_note") or [])
data_row = raw_rows[-1]