From 9deaf479d0a49a0c692b46d9948f8e29dbc053e6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 15 Sep 2026 00:04:41 +0900 Subject: [PATCH] =?UTF-8?q?feat(b09):=20=ED=8C=90=EC=A0=95=ED=91=9C?= =?UTF-8?q?=EA=B0=80=20=EC=95=88=20=EC=9D=BD=EC=9D=80=20=EC=A4=84=EC=9D=84?= =?UTF-8?q?=20=EC=9E=90=EB=8F=99=EC=9C=BC=EB=A1=9C=20=E3=80=8C=EB=AA=BB=20?= =?UTF-8?q?=EB=B6=99=EC=9D=80=20=EC=A4=84=E3=80=8D=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=EC=97=90=20=E2=80=94=20=ED=91=9C=EB=A5=BC=20=EB=84=A3=EC=9D=84?= =?UTF-8?q?=20=EB=95=8C=EB=A7=88=EB=8B=A4=20=EC=86=90=20=EC=82=AC=EC=9C=A0?= =?UTF-8?q?=EB=A5=BC=20=EC=95=88=20=EB=8B=AC=EC=95=84=EB=8F=84=20=EC=95=88?= =?UTF-8?q?=20=EC=83=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit · 판정표 공통 한 곳(_unread_rows) — 읽힌 줄 밖의 줄을 올림 · 넘치지 않게: 읽힌 줄 · 이미 못 맞춤 이름 · 다른 갈래가 쓴 기계 줄 · 자원 머리(첫 줄 머리 모양 · 횟수별) · 빈 줄은 뺌 · 분류 딱지 줄(자재·인력·기계)은 이름 칸으로 · 비고는 글 앞머리와 함께 · 손 사유와 안 겹치게: 공종 사유 한 줄에 이미 적힌 이름은 안 올림 · 오늘 손으로 단 날개벽·면벽·맨홀 줄 목록 사유는 걷고 [주] 30% 할증만 남김 · 새던 셋이 함께 닫힘 — 12-15 거푸집·철근·뚜껑·설치비 · 12-04 비고(수직고 7m 할증) · 12-34-1 레미콘 별도계상 판정표 9표 늘어난 줄: 9-19-1 0 · 12-04 +1 · 12-15 +4 · 12-12 +8 · 12-13 +6 · 12-16 +7 · 12-34-1 +1 · 제목 금액·막힘·일부만 그대로 · 잴 시험 넷 빨강→초록 · 끄기 넷 빨강 · 전체 시험 1702 + 362 통과 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- B09_Estimation/B09_Estimation_KnownGaps.py | 23 +-------- ...B09_Estimation_ResourceAxis_JudgedTable.py | 44 +++++++++++++++- .../test_b09_flexible_vibrator_tables.py | 8 ++- .../tester/test_b09_judged_unread_rows.py | 50 +++++++++++++++++++ 4 files changed, 98 insertions(+), 27 deletions(-) create mode 100644 resources/tester/test_b09_judged_unread_rows.py diff --git a/B09_Estimation/B09_Estimation_KnownGaps.py b/B09_Estimation/B09_Estimation_KnownGaps.py index 3dad3531..ebc77595 100644 --- a/B09_Estimation/B09_Estimation_KnownGaps.py +++ b/B09_Estimation/B09_Estimation_KnownGaps.py @@ -55,27 +55,8 @@ KNOWN_GAPS: dict[str, tuple[str, str]] = { "(제안 무한궤도 — 영월 실무 「06M3 B/H」 · 2026-09-14 브레인 ②).", ), # 2026-09-14 ㉮ — 사용횟수 갈래로 푼 뒤 남는 원문 몫. 값을 짓지 않고 말만. - # 봉상후렉시블 셋(2026-09-14) — 판정표가 콘크리트 ㎥ 줄만 읽음 · 표의 나머지 줄이 조용히 안 사라지게 원문 그대로. - "FP-12-12": ( - "표의 다른 줄", - "ⓘ 이 일위대가는 콘크리트 ㎥(콘크리트공·보통인부 + 진동기) 몫만 — 표의 거푸집(합판4회) · 철근(SD300 D13" - " · 철근가공조립 간단) · 기초잡석 운반·부설·다짐(소할 30% 브레이커 · 적사 굴착기 0.7㎥ · 운반 덤프트럭" - " 15톤 · 부설다짐 보통인부 0.6인/㎥) · 바닥정리는 안 넣음(따로 공종) · [주] 성토부 날개벽 인건비 30%" - " 할증은 안 걺.", - ), - "FP-12-13": ( - "표의 다른 줄", - "ⓘ 이 일위대가는 콘크리트 ㎥(콘크리트공·보통인부 + 진동기) 몫만 — 표의 거푸집(합판4회) · 기초잡석" - " 운반·부설·다짐(소할 30% 브레이커 · 적사 굴착기 0.7㎥ · 운반 덤프트럭 15톤 · 부설다짐 보통인부" - " 0.6인/㎥)은 안 넣음(따로 공종).", - ), - "FP-12-16": ( - "표의 다른 줄", - "ⓘ 이 일위대가는 구체·버림콘크리트 ㎥ 몫만 — 표의 원형거푸집(PE10회 · 각 1조/10) · 설치비(기초 및" - " 슬라브 특별인부 0.16·보통인부 0.45인/㎡ · 벽체 0.20·0.60인/㎡) · 거푸집(합판4회) · 철근(가공조립" - " 보통) · 설치비 재료비의 5% · 사다리설치(철근가공조립 간단 준용) · 인력 보통인부 0.004 는 안 넣음" - "(따로 공종·조립 몫).", - ), + # 봉상후렉시블 셋의 표 나머지 줄은 판정표 자동 목록(`_unread_rows`)이 맡음 — 줄이 아닌 [주] 만 여기. + "FP-12-12": ("원문 [주]", "ⓘ [주] 「성토부 날개벽 설치시 인건비 30% 할증」 은 안 걺(선택)."), "FP-12-34-01": ( "원문 머리", "ⓘ 원문 머리 「(단위: 개소당)」 ↔ 인력 콘크리트공 0.17·보통인부 0.29 가 12-16 맨홀 ㎥당(0.17인/㎥" diff --git a/B09_Estimation/B09_Estimation_ResourceAxis_JudgedTable.py b/B09_Estimation/B09_Estimation_ResourceAxis_JudgedTable.py index 954b7373..b1c5e78f 100644 --- a/B09_Estimation/B09_Estimation_ResourceAxis_JudgedTable.py +++ b/B09_Estimation/B09_Estimation_ResourceAxis_JudgedTable.py @@ -97,6 +97,48 @@ JUDGED_TABLES: dict[str, dict[str, Any]] = { "why": "원문 L6839 12-34-1 「인력 콘크리트공·보통인부 인 · 기계 대 (Q=5.4㎥/hr)」", }, } +#: 줄 첫 칸이 분류 딱지인 표(12-34-1 「자재 | 콘크리트(레미콘)」) — 이름은 다음 칸. +_ROW_CATEGORIES = ("자재", "인력", "기계") +#: 자원이 아닌 머리 줄(12-04 「횟수별 | 재료별(%) | 노무비(%)」). +_HEADER_ROWS = ("횟수별", "구분") +UNREAD_REASON = "판정표가 안 읽은 줄 — 이 일위대가에 안 넣음(자동 · 2026-09-14)" + + +def _loose(text: str) -> str: + """겹침 비교용 — 빈칸·괄호·가운뎃점·쉼표를 뺌(「적사(굴착기 0.7㎥)」 ↔ 「적사 굴착기 0.7㎥」).""" + return re.sub(r"[\s()·,:]", "", str(text)) + + +def _unread_rows(code, table_id, judged, rows, staged) -> list: + """읽힌 줄 밖의 줄을 「못 붙은 줄」 로 — 표를 넣을 때마다 손으로 사유를 안 달아도 안 샘. + + ㉠ 빼는 것: 읽힌 줄(`raw_row_index`) · 이미 못 맞춤으로 선 이름 · 다른 갈래가 쓴 기계 줄 + (`needs_machine`·`same_machine`) · 자원 머리(`header_row` 첫 줄 · 「횟수별」) · 빈 줄 + ㉡ 손 사유(`known_gap_note`)가 이미 적은 이름은 안 올림 — 같은 말이 두 번 안 뜨게 + """ + from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note + + read = {item.raw_row_index for item in staged if isinstance(item, ResourceRow)} + taken = {_loose(item.cell) for item in staged if isinstance(item, UnmatchedRow)} + taken |= {_loose(name) for name in judged.get("needs_machine", {}).values()} + taken |= {_loose(name) for name in judged.get("same_machine", ())} + hand = _loose(known_gap_note(code)) + unread: list = [] + for index, cells in enumerate(rows): + cells = [c for c in cells] + if index in read or not any(cells) or (judged["shape"] == "header_row" and index == 0): + continue + name = cells[1] if cells[0] in _ROW_CATEGORIES and len(cells) > 1 else cells[0] + key = _loose(name) + if not key or key in _HEADER_ROWS or key in taken or key in hand: + continue + if key == "비고": + name = f"비고 — {' '.join(' '.join(cells[1:]).split())[:40]}…" + unread.append(UnmatchedRow(code, table_id, " ".join(name.split()), UNREAD_REASON)) + taken.add(key) + return unread + + #: 비고 칸 인력 — 「콘크리트공0.24인/㎥, 보통인부 0.42인/㎥」. _RE_REMARK_LABOR = re.compile(r"([가-힣]+)\s*(\d+(?:\.\d+)?)\s*인/㎥") _RE_Q = re.compile(r"Q\s*=\s*(\d+(?:\.\d+)?)") @@ -170,6 +212,7 @@ def match_judged_table( staged = _merged_first(code, table, judged, rows, catalog, basis_quantity, unit) if isinstance(staged, str): return block(f"{staged} — 판정({judged['why']})과 칸이 달라 안 읽음") + staged = [*staged, *_unread_rows(code, table_id, judged, rows, staged)] for item in staged: if isinstance(item, UnmatchedRow): result.unmatched.append(item) @@ -308,7 +351,6 @@ def _remark_labor(code, table, judged, rows, catalog) -> list | str: def _per_m3_rows(code, table, judged, rows, catalog) -> list | str: """「인」 칸 앞 이름 · 뒤 수(인/㎥) · 「대」 칸 앞 이름 · 뒤 대수 ÷ Q — 칸이 밀린 줄도 단위 칸으로 찾음.""" - table_id = str(table.get("pum_table_id", "")) staged: list = [] for index, cells in enumerate(rows): unit = next((i for i, c in enumerate(cells) if c in ("인", "대") and i > 0), None) diff --git a/resources/tester/test_b09_flexible_vibrator_tables.py b/resources/tester/test_b09_flexible_vibrator_tables.py index 6e895029..fd35e018 100644 --- a/resources/tester/test_b09_flexible_vibrator_tables.py +++ b/resources/tester/test_b09_flexible_vibrator_tables.py @@ -78,8 +78,6 @@ def test_12_34_1_은_개소당_머리를_세제곱미터당으로_읽은_까닭 def test_표의_나머지_줄은_조용히_안_사라지고_사유에_원문_그대로() -> None: - from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note - - assert "원형거푸집" in known_gap_note("FP-12-16") and "0.16" in known_gap_note("FP-12-16") - assert "기초잡석" in known_gap_note("FP-12-12") and "30%" in known_gap_note("FP-12-12") - assert "기초잡석" in known_gap_note("FP-12-13") + """손 사유로 달던 줄 목록은 판정표 자동 목록(`_unread_rows`)으로 옮김 — `test_b09_judged_unread_rows`.""" + left = " ".join(cached_build().unattached.get("FP-12-16", [])) + assert "원형거푸집" in left and "설치비" in left, left diff --git a/resources/tester/test_b09_judged_unread_rows.py b/resources/tester/test_b09_judged_unread_rows.py new file mode 100644 index 00000000..6db210aa --- /dev/null +++ b/resources/tester/test_b09_judged_unread_rows.py @@ -0,0 +1,50 @@ +"""판정표가 안 읽은 줄 — 자동으로 「못 붙은 줄」 목록에 (2026-09-14 브레인 · ㉰ 앞 뿌리 고침). + +판정표는 적어 둔 줄만 읽음 → 나머지 줄(거푸집·철근·뚜껑·설치비·비고 …)이 사유 없이 사라지고 있었음 +(12-15 · 12-04 비고 · 12-34-1 레미콘). 표를 넣을 때마다 손으로 사유를 달면 다음 표가 또 샘. +⇒ 판정표 공통 한 곳에서 「읽힌 줄 밖의 줄」 을 올림. + ㉠ 넘치지 않게 — 자원 머리(첫 줄 머리 모양) · 빈 줄 · 다짐기처럼 다른 줄이 쓴 줄은 뺌 + ㉡ 손 사유와 안 겹치게 — 공종 사유 한 줄(`known_gap_note`)이 이미 적은 이름은 안 올림 +""" + +from __future__ import annotations + +from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note +from B09_Estimation.B09_Estimation_UnitPrice import cached_build + + +def _left(build, code: str) -> str: + return " / ".join(build.unattached.get(code, [])) + + +def test_집수정_표의_거푸집_철근_뚜껑_설치비가_목록에_뜸() -> None: + left = _left(cached_build(), "FP-12-15") + for name in ("거 푸 집", "철근", "집수정 뚜껑", "설치비"): + assert name in left, (name, left) + assert "봉상후렉시블" not in left, left # 구체 갈래가 쓴 다짐기 줄은 안 올림(㉠) + + +def test_합판거푸집_비고는_글과_함께_뜨고_손_사유에_있는_사용고재는_안_겹침() -> None: + left = _left(cached_build(), "FP-12-04") + assert "비고" in left and "7m" in left, left + assert "사용고재" in known_gap_note("FP-12-04") and "사용고재" not in left, left # ㉡ + assert "횟수별" not in left, left # 머리 줄(㉠) + + +def test_토사면_고르기_자원_머리_줄은_안_뜸() -> None: + left = _left(cached_build(), "FP-09-19-01") + assert "보통인부 (인)" not in left and "공기압축기 (시간)" not in left, left + + +def test_12_34_1_레미콘_별도계상_줄은_이름으로_뜸() -> None: + left = _left(cached_build(), "FP-12-34-01") + assert "콘크리트(레미콘)" in left and "자재" not in left.split(" / "), left + + +def test_날개벽_면벽_맨홀은_자동_목록이_맡고_손_사유는_줄_목록을_안_되풀이() -> None: + build = cached_build() + assert "원형거푸집" in _left(build, "FP-12-16") and "기초잡석" in _left(build, "FP-12-12") + assert "기초잡석" in _left(build, "FP-12-13") + for code in ("FP-12-12", "FP-12-13", "FP-12-16"): + assert "거푸집" not in known_gap_note(code), code + assert "30%" in known_gap_note("FP-12-12") # 줄이 아닌 [주] 는 손 사유로 남음