fix(axis): 표 읽기 ② — 원본이 [주] 문장을 절 제목으로 읽은 표 14장은 앞 절을 이어받음(12-2 표면 마무리 가짜 표 7장·갈래 #설치·#철거 → 12-17-1) · 9-21 제근 안내는 못 붙은 줄이 아니라 비고로(빌린 밑수 교차참조)

마스터 전후 대조: 표가 옮겨 붙은 공종 8(12-02·12-17-01·01-01-02·05-34·08-02-03·09-07-01·09-11-01·14-02) · 일위대가 453 제목 금액 변화 0 · 12-02 가짜 갈래 둘이 12-17-01 로만 옮김

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-14 13:08:01 +09:00
co-authored by Claude Opus 5
parent e85ea2fa2c
commit 8a31f87412
8 changed files with 2402 additions and 2209 deletions
@@ -178,6 +178,18 @@ def parse_toc(rows: list[list[str]]) -> list[dict[str, Any]]:
return nodes
def _is_section_title(section: str, by_number: dict[str, dict[str, Any]]) -> bool:
"""진짜 절 제목인가 — 「번호. 이름」, 또는 마침표 없이 번호 뒤 글이 목차 절 이름과 같음."""
if re.match(r"^\s*\d+(?:-\d+){0,3}\.\s", section):
return True
number = section_number(section)
node = by_number.get(number) if number else None
if node is None:
return False
tail = section.strip()[len(number) :]
return "".join(tail.split()) == "".join(str(node.get("name") or "").split())
def section_number(section: str) -> str | None:
"""`"9-3-1. 인력"` → `"9-3-1"`. 번호가 없으면 `None`."""
m = re.match(r"^\s*(\d+(?:-\d+){0,3})[.\s]", section + " ")
@@ -598,10 +610,19 @@ def build() -> dict[str, Any]:
orphans: list[dict[str, Any]] = []
undetermined: list[dict[str, Any]] = []
last_section = ""
for table in tables:
if table["table_id"] == "F0001":
continue # 목차 자신은 공종이 아니다.
section = norm(table.get("section"))
# ⚠ 원본 자료가 [주] 문장 조각을 절 제목으로 읽은 표가 있음 — 「12-2 표면 마무리를 따른다.」
# (12-17-1 펌프카 타설 [주]) 가 표 7장을 12-2 에 붙여 가짜 갈래가 섰음(2026-09-14).
# 절 제목은 「번호. 이름」 이거나(마침표 없는 원문 제목 「7-15-2 숲가꾸기, …」 도 있음)
# 번호 뒤 글이 **목차의 그 절 이름과 같음** — 둘 다 아니면 문장 조각이라 앞 절을 이어받음.
if _is_section_title(section, by_number):
last_section = section
elif last_section:
section = last_section
number = section_number(section)
chapter = number.split("-")[0] if number else None
form, why = detect_form(table, chapter)
@@ -67,7 +67,10 @@ FORM_JUDGMENTS: dict[str, tuple[str, str, str]] = {
"F0216": ("8-2-4", "reference", "위와 같은 선정약제 목록"),
"F0218": ("8-2-5", "reference", "위와 같은 선정약제 목록"),
"F0453": (
"8-2-3",
# 2026-09-14 절 8-2-3 → 14-2 — 원본이 [주] 「…8-2-3 굴착기(2025)」…」 를 절로 읽던 것을
# 앞 절 이어받기로 고치자 부록 앞 마지막 절(14-2)로 옮겨 감. 부록엔 절 번호가 없어 여전히
# 제자리가 아님 — 참고표라 값에 안 닿음.
"14-2",
"reference",
"〔부록 2〕 산림사업별 적용 가능 공종 목록 — 절 경계가 밀려 붙음",
),
@@ -103,8 +106,10 @@ FORM_JUDGMENTS: dict[str, tuple[str, str, str]] = {
),
# ── 제11·12장 ───────────────────────────────────────────────────
"F0328": ("11-4", "coefficient", "행이 q·k·f·E — [주]① Q=3600×q×k×f×E/Cm"),
"F0358": ("12-2", "reference", "시설유형 Type-Ⅰ~Ⅳ 적용 기준 설명"),
"F0360": ("12-2", "reference", "현장조건 Type-Ⅰ~Ⅲ 적용 기준 설명"),
# 2026-09-14 절 12-2 → 12-17-1 — 원본이 펌프카 타설 [주] 「…12-2 표면 마무리를 따른다.」 를
# 절 제목으로 읽어 12-2 에 붙어 있던 표(빌더가 앞 절 이어받기로 고침). 형태 판정은 그대로.
"F0358": ("12-17-1", "reference", "시설유형 Type-Ⅰ~Ⅳ 적용 기준 설명"),
"F0360": ("12-17-1", "reference", "현장조건 Type-Ⅰ~Ⅲ 적용 기준 설명"),
"F0388": ("12-34-4", "reference", "「재료비 JOINT FILLER」 항목만 — 값이 없는 구성 안내"),
"F0390": ("12-36", "reference", "제작비·운송비·설치비 「견적처리」"),
"F0392": ("12-38-1", "coefficient", "사용횟수별 잔존율(12회 25%·25회 10%)"),
@@ -88,6 +88,13 @@ def known_gap_note(code: str | None) -> str:
for prefix, note in CONDITIONAL_INCLUDED.items():
if plain.startswith(prefix):
parts.append(note)
# 다른 품셈에서 빌린 밑수(사용자 확정) — 교차 참조와 실무 어긋남을 값 옆에(2026-09-14 · 종전엔
# 고르기형 표 안내가 「못 붙은 줄」로 섰고 이 문구는 아무 데서도 안 불렸음).
from B09_Estimation.B09_Estimation_WorkItemUnit import borrowed_unit_note
borrowed = borrowed_unit_note(plain)
if borrowed:
parts.append(f"{borrowed}")
return " / ".join(parts)
@@ -191,6 +191,10 @@ def match_choose_one_machine_table(
if made == 0:
return False
if per != 1:
# 밑수를 사용자가 정해 빌려 온 표(확정 5차 6번 제근) — 안내 글은 「못 붙은 줄」이 아니라
# 내역·본표 비고(`KnownGaps.known_gap_note` ← `borrowed_unit_note`)로 뜸(2026-09-14).
return True
result.unmatched.append(
UnmatchedRow(
work_item_code=work_item_code,
@@ -1,7 +1,7 @@
{
"schema_version": "1.0",
"dataset_id": "data_work_item_master_manifest",
"generated_at": "2026-09-13T23:03:27+09:00",
"generated_at": "2026-09-14T13:07:44+09:00",
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
"source": {
"dataset_id": "pum_forest",
@@ -12,13 +12,13 @@
"files": [
{
"file": "work_item_master_2026-01-01.json",
"sha256": "1463ea14ec2d3d45ec18b3a7e4810333de8e5fc40aaeb0dd329c68f85c863e33",
"size_bytes": 835659
"sha256": "83efe531a6956f15696a4042dde1e13f37e25b26597e128b946d3f051b600fb6",
"size_bytes": 837285
},
{
"file": "form_undetermined_2026-01-01.json",
"sha256": "c835699596bf57ffbda9f72a773a72116ebe48c2a2f79ae232ba5784ec9842c7",
"size_bytes": 6447
"sha256": "6b3476810ddee395856b997fa524a0410a5001fe47511b989c1db2feb82dccd2",
"size_bytes": 6789
},
{
"file": "basis_missing_2026-01-01.json",
@@ -139,7 +139,7 @@
},
{
"pum_table_id": "F0248",
"section": "8-2-13 대형브레이커(2025)」, 「건설공사 표준품셈 공통부문, 1-2-3 토질. 3. 체적환산계수적용(2025)」 참조",
"section": "9-7-1. T=30㎝ 미만",
"headers": [
"구 분",
"적 용"
@@ -156,6 +156,31 @@
],
"reason": "헤더·첫 행에 단위·밑수·직종 표지 없음"
},
{
"pum_table_id": "F0257",
"section": "9-11-1. 콘크리트(기계)",
"headers": [
"구 분",
"단 위",
"적 용",
"비 고"
],
"first_rows": [
[
"거리(L)",
"km",
"0.1",
"N=(V×T)/(120×L+V.t)= 회/일 Q = N×q = /일"
],
[
"운반속도(V)",
"km/hr",
"2.5",
""
]
],
"reason": "헤더·첫 행에 단위·밑수·직종 표지 없음"
},
{
"pum_table_id": "F0304",
"section": "10-6-3. 기타 임업자재",
@@ -348,7 +373,7 @@
},
{
"pum_table_id": "F0356",
"section": "12-2 표면 마무리를 따른다.",
"section": "12-17-1. 펌프카 타설",
"headers": [
"슬 럼 프",
"기준 시공량"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,29 @@
"""표 읽기 ②(2026-09-14 · PLAN 1장) — 「못 붙은 줄」 로 잘못 서던 자리를 **상태가 움직이는지**로 잼.
9-21 제근 고르기형 안내 글이 `unmatched` 들어가 붙은 줄로 밑수는 사용자 확정
(건설품셈 1,000 빌림)이라 안내는 비고(`known_gap_note`) 옮김(사유는 사라짐)
12-2 표면 마무리 원본이 12-17-1 [] 문장을 제목으로 읽어 7·가짜 갈래 #설치·#철거 가 붙음
"""
from __future__ import annotations
from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note
from B09_Estimation.B09_Estimation_ResourceAxis import load_work_item_master
from B09_Estimation.B09_Estimation_UnitPrice import cached_build
def test_제근은_못_붙은_줄이_아니고_교차참조_사유는_비고에_남음() -> None:
build = cached_build()
assert not build.unattached.get("FP-09-21"), build.unattached.get("FP-09-21")
assert sum(1 for code in build.book.titles if code.startswith("B-FP-09-21#")) == 6
note = known_gap_note("FP-09-21")
assert "교차 참조" in note and "1,000㎡당" in note
def test_표면_마무리엔_미장공_표만_있고_가짜_갈래가_없음() -> None:
nodes = {n["work_item_code"]: n for n in load_work_item_master()["work_items"]}
assert [t["pum_table_id"] for t in nodes["FP-12-02"]["tables"]] == ["F0334"]
assert "F0356" in [t["pum_table_id"] for t in nodes["FP-12-17-01"]["tables"]]
build = cached_build()
assert [c for c in build.book.titles if c.startswith("B-FP-12-02")] == ["B-FP-12-02"]
assert not build.unattached.get("FP-12-02")