fix(axis): 표 읽기 — 콘크리트 포장 12-6 두께별 작업조 · 인력 돌쌓기 13-4-1/4 두 단 머리

12-6 은 두께 칸이 작업조 칸과 엇갈려 병합된 표라 두께 줄을 시공량 갈래로 읽음(20·30·40㎝, ㎥당).
후진 진입·경운기 칸은 「50%까지 감」 범위라 안 세움. 공종 단위가 빈 작업조 표는 시공량 열 머리 단위를 씀.
13-4-1·13-4-4 는 hwpx 병합대로 앞 세 돌 골·켜 두 잎 + 호박돌 및 야면석 한 잎으로 갈래를 세움(각 40 갈래).
13-4-4 는 같은 절의 표준경사 표 오판정으로 아직 막힘 — 형태 판정 일감으로 넘김.

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-13 21:42:23 +09:00
co-authored by Claude Opus 5
parent be1dfcd3a1
commit 1c79fc288c
4 changed files with 216 additions and 0 deletions
@@ -35,6 +35,8 @@ from B09_Estimation.B09_Estimation_ResourceAxis import (
)
_RE_NUMBER = re.compile(r"^\d+(?:,\d{3})*(?:\.\d+)?$")
#: 두께 갈래 칸 — 「20㎝」. 넓히면 규격 칸(「0.6㎥」)을 갈래로 오해한다.
_RE_THICKNESS = re.compile(r"^\d+(?:\.\d+)?\s*(?:㎝|cm)$")
#: 시공량 열을 알아보는 말. 「시 공 량 (㎡)」처럼 띄어쓰기가 섞여 온다.
_OUTPUT_WORDS = ("시공량", "기준시공량", "적용시공량")
@@ -101,6 +103,7 @@ def parse_crew_table(
members: list[CrewMember] = []
outputs: list[Decimal] = []
labels: list[str] = []
thickness: list[tuple[str, Decimal]] = []
for row in rows:
if not row or not row[0]:
@@ -117,6 +120,11 @@ def parse_crew_table(
names = [part for part in _normalize(head).split(" ") if part]
resolved = [_resolve(catalog, name) for name in names]
if not all(resolved):
# 두께 갈래 줄 — 「30㎝ | 150」. 콘크리트 포장 12-6 은 두께 칸이 작업조 칸과
# **줄을 엇갈려** 병합돼 있어(hwpx 확인) 마스터가 작업조 줄 사이에 두께 줄을 끼워 둔다.
if _RE_THICKNESS.match(head) and len(_numbers_in(" ".join(row[1:]))) == 1:
thickness.append((head, _numbers_in(" ".join(row[1:]))[0]))
continue
# 자원이 아니면 유형 라벨 줄로 본다 — 「복 잡 | 보 통 | 간 단」.
if not _numbers_in(" ".join(row)):
if not members:
@@ -143,9 +151,17 @@ def parse_crew_table(
# 시공량은 보통 **첫 작업조 줄**에 붙어 온다.
if not outputs:
outputs = _outputs_of(row, counts)
# 작업조 줄에 두께 갈래가 붙어 온 자리 — 「포장공 | 3 | 20㎝ | 100 | …」.
for label, value in zip(row, row[1:]):
if _RE_THICKNESS.match(label) and _RE_NUMBER.match(value):
thickness.append((label, Decimal(value.replace(",", ""))))
if not members:
return "작업조 줄을 못 찾았습니다"
if thickness:
# 두께마다 시공량이 하나 — 그 옆 시공량 칸(「후진 진입·경운기 운반은 좌측의 50%까지 감」)은
# **범위라 값이 아니다.** 유형 라벨은 그 두 칸의 머리라 두께 갈래와 짝이 아니다.
return CrewTable(members=tuple(members), outputs=tuple(thickness))
if not outputs:
return "시공량 값을 못 찾았습니다"
if labels and len(labels) != len(outputs):
@@ -206,6 +222,16 @@ def match_crew_table(
work_item_code = node.get("work_item_code", "")
table_id = str(table.get("pum_table_id", ""))
form = str(table.get("pum_form", ""))
from B09_Estimation.B09_Estimation_WorkItemUnit import unit_of
if not unit and not unit_of(work_item_code):
# 밑수가 「일당」이라 비어 있는 표(12-6) — 1단위당 품의 단위는 **시공량 열 머리**의 단위다.
# ⚠ 공종 단위가 따로 적힌 자리는 그대로 둔다(12-2 에 딸린 비계 표가 m 를 들고 옴).
headers = table.get("condition_note") or []
found = [
re.search(r"[((]\s*([^))]+?)\s*[))]", str(headers[i])) for i in output_columns(table)
]
unit = found[0].group(1) if found and found[0] else ""
parsed = parse_crew_table(table, catalog)
if isinstance(parsed, str):
result.unmatched.append(