From 15d91be7b0c78ae687d83ccff1288d6fac93604d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 23:33:47 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(b09):=20=EB=B4=89=EC=83=81=ED=9B=84?= =?UTF-8?q?=EB=A0=89=EC=8B=9C=EB=B8=94=20=EC=85=8B(12-12=20=EB=82=A0?= =?UTF-8?q?=EA=B0=9C=EB=B2=BD=20=C2=B7=2012-13=20=EB=A9=B4=EB=B2=BD=20?= =?UTF-8?q?=C2=B7=2012-16=20=EB=A7=A8=ED=99=80)=20+=2012-34-1=20=E2=80=94?= =?UTF-8?q?=20=EA=B0=99=EC=9D=80=20=EC=A7=84=EB=8F=99=EA=B8=B0=20=EC=A4=84?= =?UTF-8?q?=20=ED=95=98=EB=82=98=EA=B0=80=20=EB=A7=89=EB=8D=98=20=EB=84=A4?= =?UTF-8?q?=20=EA=B3=B5=EC=A2=85=EC=9D=84=20=ED=8C=90=EC=A0=95=ED=91=9C?= =?UTF-8?q?=EB=A1=9C=20=EC=84=B8=EC=9B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit · 12-12·12-13·12-16 — 12-15 집수정과 같은 모양(㎥ 줄 비고 인력 + 다짐기 줄) · 판정표 한 벌에 더함 · 맨홀은 비고가 끝 칸이 아니라 단위 뒤 칸 전체에서 찾음 · 비고가 인/㎥ 인 판정표는 밑수 없음 목록을 안 봄(12-12 는 「개소당」 머리조차 없음) · 12-34-1 — 형태 판정 참조→소요량(「별도계상」 은 레미콘 줄 비고일 뿐) · 새 모양 per_m3_rows · 진동기(3.5HP) 대 2 → 4611-0350 × 2/5.4 · 봉상후렉시블 대 2 는 엔진식 진동기의 봉이라 안 셈 · 머리 개소당 ↔ 인력 0.17·0.29(12-16 ㎥당과 같음)·Q ㎥/hr ⇒ ㎥당으로 읽고 고른 쪽·버린 쪽을 사유에 나란히(브레인 판정) · 범위 별칭 넷 → 엔진식 4611-0350 · 표의 나머지 줄(거푸집·철근·기초잡석·원형거푸집·설치비 등)은 원문 그대로 사유 한 줄(조용히 안 사라지게) · 실무 영월 「콘크리트타설(임도품셈 5-5)」 호표가 같은 모양 — 인력 인/㎥ + 진동기 엔진식 1/Q · 손료 252천원×0.5101 · 노무 0 · 마스터 재생성 — 네 공종 갈래 키 · F0385 형태만 새 제목 다섯 · 바뀐 제목 0 · 프로젝트 내역 그대로 · 잴 시험 넷 빨강→초록 · 끄기 다섯 빨강 · 전체 시험 1699 + 337 통과 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- ...B08_Quantity_Build_WorkItemMaster_Forms.py | 5 ++ B09_Estimation/B09_Estimation_KnownGaps.py | 27 ++++++ ...B09_Estimation_ResourceAxis_JudgedTable.py | 64 +++++++++++++- B09_Estimation/B09_Estimation_UnitPrice.py | 6 +- .../data_aliases/aliases_2026-01-01.json | 32 +++++++ .../data_work_item_master/_manifest.json | 6 +- .../work_item_master_2026-01-01.json | 32 +++++-- .../test_b09_flexible_vibrator_tables.py | 85 +++++++++++++++++++ 8 files changed, 242 insertions(+), 15 deletions(-) create mode 100644 resources/tester/test_b09_flexible_vibrator_tables.py diff --git a/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Forms.py b/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Forms.py index dbac4226..51053dc7 100644 --- a/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Forms.py +++ b/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Forms.py @@ -110,6 +110,11 @@ FORM_JUDGMENTS: dict[str, tuple[str, str, str]] = { # 절 제목으로 읽어 12-2 에 붙어 있던 표(빌더가 앞 절 이어받기로 고침). 형태 판정은 그대로. "F0358": ("12-17-1", "reference", "시설유형 Type-Ⅰ~Ⅳ 적용 기준 설명"), "F0360": ("12-17-1", "reference", "현장조건 Type-Ⅰ~Ⅲ 적용 기준 설명"), + "F0385": ( + "12-34-1", + "requirement", + "인력(인)·기계(대, Q=5.4㎥/hr) 소요량 표 — 「별도계상」 은 레미콘 자재 줄 비고일 뿐", + ), "F0388": ("12-34-4", "reference", "「재료비 JOINT FILLER」 항목만 — 값이 없는 구성 안내"), "F0390": ("12-36", "reference", "제작비·운송비·설치비 「견적처리」"), "F0392": ("12-38-1", "coefficient", "사용횟수별 잔존율(12회 25%·25회 10%)"), diff --git a/B09_Estimation/B09_Estimation_KnownGaps.py b/B09_Estimation/B09_Estimation_KnownGaps.py index d94d6ca0..3dad3531 100644 --- a/B09_Estimation/B09_Estimation_KnownGaps.py +++ b/B09_Estimation/B09_Estimation_KnownGaps.py @@ -55,6 +55,33 @@ 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 는 안 넣음" + "(따로 공종·조립 몫).", + ), + "FP-12-34-01": ( + "원문 머리", + "ⓘ 원문 머리 「(단위: 개소당)」 ↔ 인력 콘크리트공 0.17·보통인부 0.29 가 12-16 맨홀 ㎥당(0.17인/㎥" + " · 0.29인/㎥)과 같고 기계도 Q=5.4㎥/hr ⇒ ㎥당으로 읽음(고른 쪽 ㎥당 · 버린 쪽 개소당) ·" + " 봉상후렉시블(45mm) 대 2 는 엔진식 진동기의 봉이라 두 번 안 셈(2026-09-14 브레인).", + ), "FP-12-04": ( "원문 [주]", "ⓘ 「사용고재 평가기준 23%(합판과 각재의 설계단가 기준)」 은 원문이 셈을 안 줘 값으로 안 씀" diff --git a/B09_Estimation/B09_Estimation_ResourceAxis_JudgedTable.py b/B09_Estimation/B09_Estimation_ResourceAxis_JudgedTable.py index f5ccccdf..954b7373 100644 --- a/B09_Estimation/B09_Estimation_ResourceAxis_JudgedTable.py +++ b/B09_Estimation/B09_Estimation_ResourceAxis_JudgedTable.py @@ -63,6 +63,39 @@ JUDGED_TABLES: dict[str, dict[str, Any]] = { "needs_machine": {"구체콘크리트": "다짐:봉상후렉시블(45mm)"}, "why": "원문 L6460 「구체·버림콘크리트 ㎥ — 비고 칸 콘크리트공·보통인부 인/㎥」", }, + # 12-15 와 같은 모양 셋 — 같은 봉상후렉시블 줄 하나가 셋을 막고 있었음(2026-09-14 브레인 · 672 다음). + "F0350": { + "code": "FP-12-12", + "shape": "remark_labor", + "prefix": "날개벽", + "needs_machine": {"콘크리트": "다짐:봉상후렉시블(45mm)"}, + "why": "원문 L6419 12-12 「콘크리트(레미콘) ㎥ — 비고 칸 콘크리트공·보통인부 인/㎥」", + }, + "F0351": { + "code": "FP-12-13", + "shape": "remark_labor", + "prefix": "면벽", + "needs_machine": {"콘크리트": "다짐:봉상후렉시블(45mm)"}, + "why": "원문 L6436 12-13 「콘크리트(레미콘) ㎥ — 비고 칸 콘크리트공·보통인부 인/㎥」", + }, + "F0354": { + "code": "FP-12-16", + "shape": "remark_labor", + "prefix": "맨홀", + # 칸이 하나 밀려 비고가 끝 칸이 아님(「구체콘크리트 | 철근 | ㎥ | | 비고 | 」). + "needs_machine": {"구체콘크리트": "봉상후렉시블(45mm)"}, + "why": "원문 L6474 12-16 「구체·버림콘크리트 ㎥ — 비고 칸 콘크리트공·보통인부 인/㎥」", + }, + # 12-34-1 — 머리 「(단위: 개소당)」 이나 인력 0.17·0.29 가 12-16 맨홀 ㎥당과 같고 기계가 Q ㎥/hr + # ⇒ ㎥당으로 읽음 · 「봉상후렉시블 대 2」 는 엔진식 진동기(엔진+플렉시블 한 대)의 봉(2026-09-14 브레인). + "F0385": { + "code": "FP-12-34-01", + "shape": "per_m3_rows", + "prefix": "콘크리트 타설", + # 엔진식 진동기(건설품셈 8-3 (4611) 엔진+플렉시블 한 대)의 봉 — 「진동기(3.5HP) 대 2」 로 셈. + "same_machine": ("봉상후렉시블(45mm)",), + "why": "원문 L6839 12-34-1 「인력 콘크리트공·보통인부 인 · 기계 대 (Q=5.4㎥/hr)」", + }, } #: 비고 칸 인력 — 「콘크리트공0.24인/㎥, 보통인부 0.42인/㎥」. _RE_REMARK_LABOR = re.compile(r"([가-힣]+)\s*(\d+(?:\.\d+)?)\s*인/㎥") @@ -122,7 +155,8 @@ def match_judged_table( result.partial_items[code] = why return True - if basis_quantity in (None, 0): + # 비고·Q 가 ㎥당을 적는 모양은 표 머리 밑수를 안 씀(12-12 날개벽은 「개소당」 머리조차 없음). + if basis_quantity in (None, 0) and judged["shape"] not in ("remark_labor", "per_m3_rows"): return block("판정 표에 밑수가 없습니다") if judged["shape"] == "header_row": staged = _header_row(code, table, judged, rows, catalog, basis_quantity, unit) @@ -130,6 +164,8 @@ def match_judged_table( staged = _use_count(code, table, rows, catalog, basis_quantity, unit) elif judged["shape"] == "remark_labor": staged = _remark_labor(code, table, judged, rows, catalog) + elif judged["shape"] == "per_m3_rows": + staged = _per_m3_rows(code, table, judged, rows, catalog) else: staged = _merged_first(code, table, judged, rows, catalog, basis_quantity, unit) if isinstance(staged, str): @@ -240,7 +276,7 @@ def _remark_labor(code, table, judged, rows, catalog) -> list | str: by_name = {"".join(cells[0].split()): cells for cells in rows if cells} staged: list = [] for index, cells in enumerate(rows): - labors = _RE_REMARK_LABOR.findall(cells[-1] if cells else "") + labors = _RE_REMARK_LABOR.findall(" ".join(cells[3:])) # 비고가 끝 칸이 아닌 표(12-16) if len(cells) < 3 or cells[2] != "㎥" or not labors: continue variant = cells[0].split("(")[0].strip() @@ -268,3 +304,27 @@ def _remark_labor(code, table, judged, rows, catalog) -> list | str: for entry, amount in pieces: staged.append(_row(code, table, entry, amount, "㎥", index, variant)) return staged + + +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) + if unit is None: + continue + name = cells[unit - 1] + if "".join(name.split()) in judged.get("same_machine", {}): + continue # 같은 기계 두 번 안 셈 — 까닭은 공종 사유 한 줄(`KnownGaps`)이 화면에 보임 + amount = next((parse_amount(c) for c in cells[unit + 1 :] if parse_amount(c)), None) + entry = _entry(catalog, name, code) + if amount is None or entry is None: + return f"{name} 줄" + if cells[unit] == "대": + found_q = _RE_Q.search(" ".join(cells)) + if found_q is None: + return f"{name} Q" + amount = amount / Decimal(found_q.group(1)) + staged.append(_row(code, table, entry, amount, "㎥", index, "")) + return staged diff --git a/B09_Estimation/B09_Estimation_UnitPrice.py b/B09_Estimation/B09_Estimation_UnitPrice.py index 100028f0..2c824a43 100644 --- a/B09_Estimation/B09_Estimation_UnitPrice.py +++ b/B09_Estimation/B09_Estimation_UnitPrice.py @@ -899,9 +899,13 @@ def build_unit_prices( # 예외 = 사용자가 다른 품셈 밑수를 확정한 자리(9-21 「1,000㎡당」 · ÷ 는 ChooseOne 이 검) — # 마스터 목록은 원문 사실이라 그대로 두고 여기서만 걷음(2026-09-14 브레인 · 사유는 비고). borrowed = work_item_code.startswith(tuple(BORROWED_BASIS_PER)) + from B09_Estimation.B09_Estimation_ResourceAxis_JudgedTable import JUDGED_TABLES + for row in rows: section = missing_basis.get(str(row.pum_table_id)) - if section and not borrowed: + # 비고가 「인/㎥」 로 밑수를 적은 판정표(12-12 날개벽 「개소당」 머리 없음)는 밑수가 ㎥ 로 섬. + per_remark = JUDGED_TABLES.get(str(row.pum_table_id), {}).get("shape") == "remark_labor" + if section and not borrowed and not per_remark: build.basis_missing[work_item_code] = section break diff --git a/resources/data_aliases/aliases_2026-01-01.json b/resources/data_aliases/aliases_2026-01-01.json index b41c5f8a..90611b55 100644 --- a/resources/data_aliases/aliases_2026-01-01.json +++ b/resources/data_aliases/aliases_2026-01-01.json @@ -51,6 +51,38 @@ "pum_edition": "2026-01-01", "basis": "12-15 집수정 표(원문 L6460)는 「봉상후렉시블(45mm) Q=5.4㎥/hr」 만 적고 전기·엔진을 안 적음 — 건설품셈 8-3 (4611) 은 전기식 플렉시블형 ø45(0.75㎾)·엔진식 플렉시블형 ø45(2.6㎾) 둘 · 12-34-1 「콘크리트 진동기(3.5HP)」 = 2.6㎾ 엔진식 · 운전경비표(8-4)에 엔진식 4611-0350(휘발유 1.0L)만 있음 → 엔진식. 전기식이 필요하면 그때 엶. 2026-09-14 브레인 승인(661 뒤 ①)" }, + { + "axis": "resource", + "from": "다짐:봉상후렉시블(45mm)", + "to": "4611-0350", + "scope": "FP-12-12", + "pum_edition": "2026-01-01", + "basis": "12-12 날개벽(원문 L6419) 표는 「봉상후렉시블(45mm) Q=5.4㎥/hr」 만 적고 전기·엔진을 안 적음 — 12-15 집수정과 같은 줄 · 같은 근거(건설품셈 8-3 (4611) 전기식·엔진식 둘 · 12-34-1 「콘크리트 진동기(3.5HP)」 = 2.6㎾ 엔진식 · 운전경비표에 엔진식 4611-0350 만) → 엔진식. 2026-09-14 브레인(672 다음 · 봉상후렉시블 셋)" + }, + { + "axis": "resource", + "from": "다짐:봉상후렉시블(45mm)", + "to": "4611-0350", + "scope": "FP-12-13", + "pum_edition": "2026-01-01", + "basis": "12-13 면벽(원문 L6436) 표는 「봉상후렉시블(45mm) Q=5.4㎥/hr」 만 적고 전기·엔진을 안 적음 — 12-15 집수정과 같은 줄 · 같은 근거(건설품셈 8-3 (4611) 전기식·엔진식 둘 · 12-34-1 「콘크리트 진동기(3.5HP)」 = 2.6㎾ 엔진식 · 운전경비표에 엔진식 4611-0350 만) → 엔진식. 2026-09-14 브레인(672 다음 · 봉상후렉시블 셋)" + }, + { + "axis": "resource", + "from": "봉상후렉시블(45mm)", + "to": "4611-0350", + "scope": "FP-12-16", + "pum_edition": "2026-01-01", + "basis": "12-16 맨홀(원문 L6474) 표는 「봉상후렉시블(45mm) Q=5.4㎥/hr」 만 적고 전기·엔진을 안 적음 — 12-15 집수정과 같은 줄 · 같은 근거(건설품셈 8-3 (4611) 전기식·엔진식 둘 · 12-34-1 「콘크리트 진동기(3.5HP)」 = 2.6㎾ 엔진식 · 운전경비표에 엔진식 4611-0350 만) → 엔진식. 2026-09-14 브레인(672 다음 · 봉상후렉시블 셋)" + }, + { + "axis": "resource", + "from": "콘크리트 진동기(3.5HP)", + "to": "4611-0350", + "scope": "FP-12-34-01", + "pum_edition": "2026-01-01", + "basis": "12-34-1 「콘크리트 진동기(3.5HP)」 — 3.5HP = 2.6㎾ · 건설품셈 8-3 (4611) 엔진식 플렉시블형 ø45(2.6㎾) · 운전경비표에 엔진식만 · 실무 영월 중기목록 「콘크리트 진동기 45φ(2.6㎾)엔진식플렉시블형」. 2026-09-14 브레인" + }, { "axis": "variant", "from": "0.2·소림", diff --git a/resources/data_work_item_master/_manifest.json b/resources/data_work_item_master/_manifest.json index 07545afd..faaf4482 100644 --- a/resources/data_work_item_master/_manifest.json +++ b/resources/data_work_item_master/_manifest.json @@ -1,7 +1,7 @@ { "schema_version": "1.0", "dataset_id": "data_work_item_master_manifest", - "generated_at": "2026-09-14T22:25:14+09:00", + "generated_at": "2026-09-14T23:14:39+09:00", "built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py", "source": { "dataset_id": "pum_forest", @@ -12,8 +12,8 @@ "files": [ { "file": "work_item_master_2026-01-01.json", - "sha256": "2f0d8bfb3836597ee6f2fd755e2f2e172cb33df1e1e9061af81982a43497f9b2", - "size_bytes": 838655 + "sha256": "708a6a80d016c42d3402bd2a6cb5266fa18e1e2b5301bac823e6ff79d17fb2b6", + "size_bytes": 838969 }, { "file": "form_undetermined_2026-01-01.json", diff --git a/resources/data_work_item_master/work_item_master_2026-01-01.json b/resources/data_work_item_master/work_item_master_2026-01-01.json index fef4885e..08b0f120 100644 --- a/resources/data_work_item_master/work_item_master_2026-01-01.json +++ b/resources/data_work_item_master/work_item_master_2026-01-01.json @@ -3,7 +3,7 @@ "dataset_id": "work_item_master_forest", "effective_date": "2026-01-01", "pum_edition": "2026-01-01", - "generated_at": "2026-09-14T22:25:14+09:00", + "generated_at": "2026-09-14T23:14:39+09:00", "dataset_version": { "dataset_id": "pum_forest", "effective_date": "2026-01-01", @@ -32888,7 +32888,9 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [], + "variant_key": [ + "콘크리트" + ], "condition_note": [ "구 분", "규 격", @@ -32970,7 +32972,9 @@ ] } ], - "variant_keys": [] + "variant_keys": [ + "콘크리트" + ] }, { "work_item_code": "FP-12-13", @@ -32997,7 +33001,9 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [], + "variant_key": [ + "콘크리트" + ], "condition_note": [ "구 분", "규 격", @@ -33065,7 +33071,9 @@ ] } ], - "variant_keys": [] + "variant_keys": [ + "콘크리트" + ] }, { "work_item_code": "FP-12-14", @@ -33242,7 +33250,10 @@ "formula_rows": [], "special_glyphs": [], "capacity_formula_here": false, - "variant_key": [], + "variant_key": [ + "구체콘크리트", + "버림콘크리트" + ], "condition_note": [ "구 분", "단위", @@ -33349,7 +33360,10 @@ ] } ], - "variant_keys": [] + "variant_keys": [ + "구체콘크리트", + "버림콘크리트" + ] }, { "work_item_code": "FP-12-17", @@ -35258,8 +35272,8 @@ "pum_table_id": "F0385", "section": "12-34-1. 콘크리트 타설(철근 진동기포함)", "source_line": 6843, - "pum_form": "reference", - "form_basis": "'별도계상' — 값이 아니라 참조 지시", + "pum_form": "requirement", + "form_basis": "사람 판정 — 인력(인)·기계(대, Q=5.4㎥/hr) 소요량 표 — 「별도계상」 은 레미콘 자재 줄 비고일 뿐", "basis_quantity": 1.0, "basis_unit": "개소", "basis_source": "본문", diff --git a/resources/tester/test_b09_flexible_vibrator_tables.py b/resources/tester/test_b09_flexible_vibrator_tables.py new file mode 100644 index 00000000..6e895029 --- /dev/null +++ b/resources/tester/test_b09_flexible_vibrator_tables.py @@ -0,0 +1,85 @@ +"""봉상후렉시블 셋 + 12-34-1 — 2026-09-14 브레인(672 다음). + +같은 「봉상후렉시블(45mm) Q=5.4㎥/hr」 줄 하나가 12-12 날개벽 · 12-13 면벽 · 12-16 맨홀을 막고 있었음. +세 표는 12-15 집수정과 같은 모양(㎥ 줄 비고 인력 + 다짐기 줄) — 판정표 한 벌에 더하고 엔진식 4611-0350 로 이음. +실무 영월 「콘크리트타설(임도품셈 5-5)」 호표가 같은 모양: 콘크리트공 0.15·보통인부 0.27 인/㎥ + 진동기 1/Q(5.4) +· 진동기 45φ(2.6㎾)엔진식플렉시블형 손료 252천원 × 0.5101 · 노무 0 — 엔진식·계수·조종원 없음이 실무 실증. + +12-34-1 콘크리트 타설(철근 진동기 포함) — 실무 호표 없음 → 브레인 판정 + ① 「콘크리트 진동기(3.5HP) 대 2」 만 4611-0350 × 2대 · 「봉상후렉시블(45mm) 대 2」 는 같은 기계의 봉이라 안 셈 + ② 머리 「(단위: 개소당)」 ↔ 인력 0.17·0.29 가 12-16 맨홀 ㎥당과 같고 기계도 Q ㎥/hr ⇒ ㎥당으로 읽고 사유에 나란히 +""" + +from __future__ import annotations + +from decimal import Decimal + +from B09_Estimation.B09_Estimation_UnitPrice import cached_build, detail_of + +VIBRATOR = "X-4611-0350" +PER_Q = Decimal(1) / Decimal("5.4") + + +def _rows(build, code: str) -> dict[str, Decimal]: + return { + r["ref_code"]: Decimal(r["quantity"]) + for r in detail_of(build, code)["rows"] + if r.get("ref_code") + } + + +def test_날개벽_면벽은_콘크리트_갈래가_진동기와_함께_섬() -> None: + build = cached_build() + for code in ("B-FP-12-12#콘크리트", "B-FP-12-13#콘크리트"): + rows = _rows(build, code) + assert rows[VIBRATOR] == PER_Q and rows["1013"] == Decimal("0.15"), (code, rows) + assert rows["1002"] == Decimal("0.27"), (code, rows) + assert build.book.titles[code].unit == "㎥" + assert ( + "FP-12-12" not in build.basis_missing + ) # 비고가 인/㎥ — 「개소당」 머리가 없어도 밑수는 ㎥ + + +def test_맨홀은_비고가_끝_칸이_아니어도_구체_버림_갈래() -> None: + build = cached_build() + body = _rows(build, "B-FP-12-16#구체콘크리트") + assert ( + body[VIBRATOR] == PER_Q + and body["1013"] == Decimal("0.17") + and body["1002"] == Decimal("0.29") + ) + lean = _rows(build, "B-FP-12-16#버림콘크리트") + assert lean == {"1013": Decimal("0.15"), "1002": Decimal("0.27")}, lean + for code in ("FP-12-12", "FP-12-13", "FP-12-16"): + assert "봉상후렉시블" not in " ".join(build.unattached.get(code, [])), code + + +def test_12_34_1_진동기_두_대만_세고_봉은_두_번_안_셈() -> None: + build = cached_build() + rows = _rows(build, "B-FP-12-34-01") + assert rows[VIBRATOR] == 2 * PER_Q, rows + assert rows["1013"] == Decimal("0.17") and rows["1002"] == Decimal("0.29"), rows + from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note + + assert "봉상후렉시블" in known_gap_note("FP-12-34-01") and "두 번 안 셈" in known_gap_note( + "FP-12-34-01" + ) + refs = [r.get("ref_code") for r in detail_of(build, "B-FP-12-34-01")["rows"]] + assert refs.count(VIBRATOR) == 1, refs # 봉 줄이 기계로 또 안 섬(한 줄 · 2대 몫) + + +def test_12_34_1_은_개소당_머리를_세제곱미터당으로_읽은_까닭을_보임() -> None: + from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note + + build = cached_build() + assert build.book.titles["B-FP-12-34-01"].unit == "㎥" + note = known_gap_note("FP-12-34-01") + assert "개소당" in note and "㎥당" in note and "12-16" in note, note + + +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") From 9deaf479d0a49a0c692b46d9948f8e29dbc053e6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 15 Sep 2026 00:04:41 +0900 Subject: [PATCH 2/2] =?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") # 줄이 아닌 [주] 는 손 사유로 남음