From 2c0ae48ac7c344a606565dd744551df29828d8e1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 06:32:14 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(b08):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=ED=91=9C=20=EA=B5=AC=EB=A9=8D=20=EB=84=B7=20=E2=80=94=20?= =?UTF-8?q?=EA=B3=A8=EB=A7=89=EC=9D=B4=C2=B7=EB=B0=94=EB=8B=A5=EB=A7=89?= =?UTF-8?q?=EC=9D=B4=20=EB=82=B4=EC=97=AD=20=EC=88=98=EB=9F=89=200=20?= =?UTF-8?q?=C2=B7=20=EB=96=BC=ED=9D=99=EB=A7=89=EC=9D=B4=20m=EB=8B=B9=20?= =?UTF-8?q?=EC=98=A4=ED=91=9C=EC=8B=9C=20=C2=B7=20=EA=B0=9C=EA=B1=B0=20L?= =?UTF-8?q?=ED=98=95=20=ED=82=A4=20=C2=B7=20=EA=B8=B0=EC=8A=AD=EB=A7=89?= =?UTF-8?q?=EC=9D=B4=20=EA=B1=B0=EC=A7=93=20=EC=82=AC=EC=9C=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 구조물도 남은 종류 그림을 시작하며 시험 제원으로 표를 세워 보다 드러남: - 골막이(개소당)·바닥막이(㎡당)가 「m · 연장 0」으로 읽혀 구조물도 전 줄 「단위당 못 냄」 · 인계 「물량이 0 이라 내역에 안 세움」 → 내역 단위 개소 1 / ㎡ 면적으로 - 떼흙막이 개소당 값이 연장으로 나뉘어 떼 0.139/m 로 보였음 → 개소당 1.39 - 개거 「L형수로 H=0.2」 — 엔진 키가 「콘크리트 L형수로 H=0.2」라 등록부에서 골라도 영영 안 섰음 → 등록부 글자로 - 기슭막이 사유 「돌 종류·뒷길이 칸이 없어」 — 칸이 선 뒤로 거짓 · 돌쌓기 한 줄과 같은 말 두 번 → 걷음 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- .../B08_Quantity_Engine_UnitQuantity.py | 12 +++ ..._Quantity_Engine_UnitQuantity_Revetment.py | 28 ++---- .../tester/test_b08_structure_table_holes.py | 98 +++++++++++++++++++ 3 files changed, 117 insertions(+), 21 deletions(-) create mode 100644 resources/tester/test_b08_structure_table_holes.py diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index 26072a38..b5769d65 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -162,6 +162,15 @@ def _revetment(): #: 전개식을 일부러 안 두는 종류 — 왜 안 두는지 사람이 읽게 적는다. EXPANDER_WITHHELD: dict[str, str] = {} +#: 전개식이 **개소당·㎡당**으로 값을 내는 종류의 내역 단위와 수량(2026-09-14 구조물도 그림에서 잡음). +#: ⚠ 안 적으면 「m · 연장」으로 읽혀 — 골막이·바닥막이(점 배치라 연장 0)는 **수량 0 이라 내역에 안 서고** +#: 구조물도가 전 줄 「단위당 못 냄」, 떼흙막이는 개소당 값이 연장으로 나뉘어 **떼 0.139/m** 로 보였음. +EXPANDER_BILLING: dict[str, Any] = { + "erosion_check": lambda options: ("개소", 1.0), # 정본 「골막이(찰)(치수조서연결)」 개소당 + "soil_guard": lambda options: ("개소", 1.0), # 정본 「떼흙막이」 개소당(평균 붙박이) + "bed_sill": lambda options: ("㎡", _num(options.get("area_m2"), 0.0)), # 정본 돌붙임 ㎡당 +} + #: 한 구조물이 **여러 내역 줄**을 낳는 자리. 배수관은 관 자체와 유입부 집수정이 따로 선다 #: (품셈도 관부설과 집수정을 다른 공종으로 둔다). 한 줄로 합치면 어느 쪽 물량인지 못 가른다. @@ -305,6 +314,9 @@ def expand( face, face_reason = structure_face_role(section_mode, options.get("side")) result.components, notes = expander(height, length, options, face, face_reason) result.notes.extend(notes) + billing = EXPANDER_BILLING.get(type_id) + if billing is not None and result.components: + result.billing_unit, result.billing_quantity = billing(options) return result diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py index 47181b73..de1d3782 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py @@ -12,9 +12,9 @@ ⚠ **형태가 돌쌓기가 아닌 갈래는 식이 없다** — 콘크리트·돌망태·통나무/목재틀·바자. 지어내지 않고 **왜 없는지**를 줄로 남긴다. -⚠ **뒷길이·돌 종류 칸이 기슭막이 등록부에 아직 없다**(2026-09-09 실측). 돌쌓기 식은 그 둘로 - 계수가 갈리는데, 없으면 `_back_length` 가 **조용히 45㎝ 로 돈다**. 값이 나오므로 아무도 - 못 알아채는 자리라 **그 사실을 줄로 드러낸다** — 「값이 있기는 하니 안 보이는」 그것. +ⓘ 뒷길이·돌 종류 칸은 기슭막이 등록부에 섰음(`test_revetment_stone_fields`). 안 고른 장은 + 돌쌓기 식이 내는 한 줄(「뒷길이를 안 골라 기본 45㎝」·「돌 종류를 안 정해」)이 그대로 드러냄. + 2026-09-14 까지 붙이던 「칸이 없어」 줄은 **거짓 + 같은 말 두 번**이라 걷음. """ from __future__ import annotations @@ -85,12 +85,6 @@ BED_SILL_FORMS: dict[str, dict[str, Any]] = { }, } -#: 돌쌓기 식이 계수를 가르는 데 쓰는 칸 — 기슭막이 등록부에 아직 없는 것을 알린다. -STONE_SPEC_KEYS: tuple[tuple[str, str], ...] = ( - ("back_len_cm", "뒷길이"), - ("stone_kind", "돌 종류"), -) - def revetment( height_m: float, @@ -110,17 +104,7 @@ def revetment( "— 형태를 골라야 물량이 섭니다" ] - components, notes = stone_masonry( - height_m, length_m, options, STONE_FORMS[form], face, face_reason - ) - # ⚠ 계수를 가르는 칸이 없으면 **기본값으로 조용히 돈다** — 그 사실을 드러낸다. - missing = [label for key, label in STONE_SPEC_KEYS if not options.get(key)] - if missing and components: - notes.append( - f"⚠ 기슭막이 제원에 {' · '.join(missing)} 칸이 없어 **돌쌓기 기본값으로 섰음** " - "— 그 값이 고임돌·야면석·채움콘크리트 계수를 가름" - ) - return components, notes + return stone_masonry(height_m, length_m, options, STONE_FORMS[form], face, face_reason) #: 돌골막이 원단위 — 소광리 정본 「골막이(찰)(치수조서연결)」, **개소당**. @@ -414,7 +398,9 @@ OPEN_DITCH_FORMS: dict[str, dict[str, Any]] = { ("면목", "m", 2.0 * 1.0, "2 × 1"), ), }, - "콘크리트 L형수로 H=0.2": { + # ⚠ 키는 **등록부 선택지 글자 그대로**(「L형수로 H=0.2」) — 2026-09-14 까지 앞에 + # 「콘크리트 」가 붙어 있어 골라도 **영영 「원단위가 없습니다」**로 섰음. + "L형수로 H=0.2": { "rows": ( ("터파기", "㎥", 0.31645, "[{(1.11+1.51)÷2×0.2} + {(0.33×0.33)÷2}] × 1.0"), ("되메우기", "㎥", 0.129975, "(0.2+0.30)÷2×0.25×1 + (0.2+0.19)÷2×0.33×1.0"), diff --git a/resources/tester/test_b08_structure_table_holes.py b/resources/tester/test_b08_structure_table_holes.py new file mode 100644 index 00000000..5b6363f2 --- /dev/null +++ b/resources/tester/test_b08_structure_table_holes.py @@ -0,0 +1,98 @@ +"""구조물도 그림을 그리려다 드러난 **표 구멍** (2026-09-14 · PLAN 12장 남은 종류 그림). + +⚠ 셋 — 값이 서기는 하거나(떼흙막이) 조용히 0 이라 아무 시험도 안 잡던 자리. + ① 골막이·바닥막이는 개소당·㎡당 값인데 내역 단위가 「m · 연장 0」 → **수량 0, 내역에 안 섬** + ② 떼흙막이는 개소당 값이 연장으로 나뉘어 **떼 0.139/m** 로 보였음 + ③ 개거 「L형수로 H=0.2」 — 등록부 선택지 글자와 엔진 키가 달라 골라도 영영 안 섬 + ④ 기슭막이 사유 「돌 종류·뒷길이 칸이 없어」 — 칸이 선 뒤로 거짓 · 같은 말 두 번 +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff # noqa: E402 +from B08_Quantity.B08_Quantity_Engine_StructureSheet import build_standard_sheets # noqa: E402 +from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table # noqa: E402 + + +def _table(type_id: str, start: float, end: float, **options) -> dict: + structure = { + "structure_id": type_id, + "type_id": type_id, + "start_m": start, + "end_m": end, + "options": options, + } + return build_table([structure], None) + + +def _sheet(table: dict) -> dict: + return build_standard_sheets(table)["sheets"][0] + + +def _unit(sheet: dict, name: str) -> float: + return next(row["unit_amount"] for row in sheet["rows"] if row["name"] == name) + + +def test_골막이는_개소당으로_서고_내역_수량이_1_개소() -> None: + table = _table( + "erosion_check", 50, 50, form="돌", height_m=1.5, top_length_m=4, bottom_length_m=2 + ) + item = table["structures"][0] + assert (item["billing_unit"], item["billing_quantity"]) == ("개소", 1.0) + sheet = _sheet(table) + assert sheet["unit_label"] == "개소당" and sheet["unpriced_rows"] == [] + row = next(r for r in build_handoff(unit_quantity_table=table)["work_items"] if r["unit"]) + assert (row["unit"], row["quantity"]) == ("개소", 1.0) # 전: m · 0.0 → 내역에 안 섬 + + +def test_떼흙막이는_개소당_값이_그대로_보인다() -> None: + sheet = _sheet(_table("soil_guard", 60, 70, form="떼", height_m=0.5, length_m=10)) + assert sheet["unit_label"] == "개소당" + assert abs(_unit(sheet, "떼") - 1.39) < 1e-9 # 전: 0.139 (m당으로 나뉨) + + +def test_바닥막이는_제곱미터당() -> None: + table = _table("bed_sill", 80, 80, form="돌붙임(찰)", area_m2=12, height_m=0.3) + sheet = _sheet(table) + assert sheet["unit_label"] == "㎡당" and sheet["billing_total"] == 12 + assert abs(_unit(sheet, "돌붙임") - 1.0) < 1e-9 and abs(_unit(sheet, "입적") - 0.45) < 1e-9 + + +def test_개거_L형수로는_등록부_글자로_선다() -> None: + registry = json.loads( + (ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8") + ) + choices = next( + option["choices"] + for item in registry["types"] + if item["type_id"] == "open_ditch" + for option in item["options"] + if option["key"] == "ditch_spec" + ) + for choice in choices: + item = _table("open_ditch", 0, 0, ditch_spec=choice, length_m=5)["structures"][0] + assert item["components"], (choice, item["notes"]) + concrete = next( + c + for c in _table("open_ditch", 0, 0, ditch_spec="L형수로 H=0.2", length_m=5)["structures"][ + 0 + ]["components"] + if c["name"] == "콘크리트" + ) + assert abs(concrete["amount"] - 0.1508 * 5) < 1e-9 + + +def test_기슭막이_사유에_없는_칸_이야기가_없다() -> None: + notes = _table("revetment", 0, 10, form="돌쌓기(찰)", height_m=2, length_m=10)["structures"][0][ + "notes" + ] + assert not any("칸이 없어" in note for note in notes) + assert any("뒷길이를 안 골라" in note for note in notes) # 같은 사실은 돌쌓기 한 줄이 말함 From 6454f4fefb0fba1000dbde5d5539ae06fe4e9bac Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 06:32:56 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs(b09):=20=EC=B6=95C=20=EB=AA=85?= =?UTF-8?q?=EC=84=B8=20=EB=B0=98=EC=98=AC=EB=A6=BC=20=ED=91=9C=EC=97=90=20?= =?UTF-8?q?=EC=A4=91=EA=B8=B0=EC=8B=9C=EA=B0=84=EA=B8=88=EC=95=A1=EC=A7=91?= =?UTF-8?q?=EA=B3=84=ED=91=9C=20=EC=A4=84=20=EB=B3=B4=ED=83=AC=20=E2=80=94?= =?UTF-8?q?=20=EC=84=B1=EB=B6=84=EB=A7=88=EB=8B=A4=20=EB=B0=98=EC=98=AC?= =?UTF-8?q?=EB=A6=BC=20=ED=9B=84=20=ED=95=A9(131/144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Prk9BCHG1EMAywk9k8wegA --- docs/raw/verification/2026-09-13_축C_명세.md | 85 ++++++++++++++++++-- 1 file changed, 79 insertions(+), 6 deletions(-) diff --git a/docs/raw/verification/2026-09-13_축C_명세.md b/docs/raw/verification/2026-09-13_축C_명세.md index dca9d4ae..2b3a1862 100644 --- a/docs/raw/verification/2026-09-13_축C_명세.md +++ b/docs/raw/verification/2026-09-13_축C_명세.md @@ -202,9 +202,37 @@ AR-<종류>-<8자리 16진> 카탈로그에 없는 자원 | **자원 종류** (`resource_kind`) | `labor` · `material` · `machine` | 품셈 줄이 무엇을 부르나 | | **단가 층** (`PriceKind`) | `M·L·S·X·B·D·W` | 단가가 어느 겹에 사나 | -단가 층 넷의 차례 (STC `COSTN` 186행 실측) — -`S 중기취득가(천원) → X 시간당 중기사용료 → B 일위대가 → D 단가산출`, -`L 노임`·`M 자재`는 카탈로그 층(상세 없이 값이 바로 있음). +### ⚠⚠ 층 차례 — 2026-09-13 정정. 화살표 방향이 모호했음 + +**조립(참조) 차례는 아래가 맞음.** 앞서 적은 `… → B → D` 는 **거꾸로 읽힐 수 있었음.** + +``` +L 노임 · M 자재 (카탈로그 — 상세 없이 값이 바로 있음) + ↓ +S 중기취득가(천원) → X 시간당 중기사용료 + ↓ +D 단가산출 (Q 식으로 시간당 단가 ÷ Q) + ↓ +B 일위대가 (D 와 카탈로그를 물어 조립) +``` + +⇒ **D 가 먼저 서고 B 가 D 를 참조함.** 우리 코드는 지금 **D 가 B 를 참조해 뒤집혀 있음**(랩탑 서브 실측). +⚠ 차례가 뒤집히면 **위상순서·재귀·이중계상 판정이 다 영향받음** — 값 정밀도 고치기와 **따로 뗄 것.** + +### D 는 **Q 를 쓰는 자리에만** 섬 (2026-09-13 실측) + +``` +D 가 서는 자리 Q 식으로 시간당 단가를 나누는 기계 공종 + (굴착기 · 도자 · 직접 작업량/다짐 · 암 잎) +D 가 없는 자리 사람 품만 있는 공종 · 자원축 표 시간(Q 아님) → B 가 X·카탈로그를 바로 물음 +``` + +- `D-<공종>[#갈래]` · **D→X 수량 `1/Q × 몫`** · **B→D 수량 1** +- STmate 한 `D` 에 **`Q` 가 여럿일 수 있음**(기계조합 호표) +- ⚠ **「단산 N」 번호는 실제 `D` 가 든 줄에만.** 단가산출이 없는 줄에 번호를 붙이면 **없는 근거를 가리킴** +- ⚠ **내역이 부르는 코드 축은 `B` 하나로 둠.** STmate 는 순수 기계 공종에서 내역이 `D` 를 바로 부르나 + (봉화 「토사절취 … 산근 19호표」), 금액은 같고(`B→D` 수량 1) **축이 하나여야 인계·골든셋·이중계상이 단순함.** + ⇒ **골든셋 대조에서 STmate 의 `D` 줄과 우리 `B` 줄을 짝으로 알아보게** 해 둘 것 --- @@ -300,16 +328,38 @@ B08 인계본 `ground_class_aliases` — **B09 가 안 읽음**)을 한 벌로 | 자리 | 규칙 | 근거 | | --- | --- | --- | | **호표 안 성분 소계** | **절사** | 중기사용료 398건 100% · 일위대가 345건 94.2% | -| **집계표 행 금액** | `ROUND` | 자원·일위대가·중기 집계표 | +| **집계표 행 금액** | `ROUND` | 자원 셋(재료비·노무비·경비) **279/279** | +| **중기시간금액집계표** | **성분마다 반올림한 뒤 합** | **131/144**. ⚠ 합계를 한 번에 반올림하면 107/144 로 떨어짐 | | **내역에 실릴 때** | 정수 절사 | 착공내역서 178건 100% | | **단가산출 성분단가** | 소수 1자리 절사 | 211건 중 137건 설명 | | 내부 계산 | 은행가(최근접-짝수) | STmate 내부. ⚠ 뽑은 엑셀은 사사오입이라 20개 경계 중 10개가 갈림 | +### ⭐ 규정이 없는 자리 — 소수점 2자리가 기본값 (2026-09-13 사용자 확정) + +**단수처리에 법적 근거가 없음이 확인됐음** — 기재부 **예정가격작성기준 전 조문에 +원단위 절사·관급 올림 조문이 없고**, 국고금관리법 §47 과 **발주청 지침 영역**임(안티그래비티 조사). + +``` +규정이 있는 자리 그 규정을 따름 (품셈·발주청 지침·STmate 실증) +규정이 없는 자리 소수점 2자리 · 반올림 ← 기본값. 사용자가 칸으로 바꿈 +``` + +⇒ STmate 관행(절사·은행가)을 따라도 되는 근거가 생겼음. **발주청 지침이 오면 그것으로 덮음.** +⚠ **붙박이로 박지 말 것** — 사용자가 바꿀 칸으로 둘 것. + **식을 접지 말 것 둘** - **노임 시간당 환산** — 식 문자열을 **좌→우 순차 평가**. 계수로 미리 곱하면 `267,360 × 0.20833… = 55,699.99…` → **55,699** 로 1원 틀림. - 좌→우는 모든 단계가 정수라 **55,700**. 18건 중 계수 선계산은 14/18 만 맞음 + 좌→우는 모든 단계가 정수라 **55,700**. + + ⚠⚠ **2026-09-13 정정 — 「18/18 원 단위」는 틀렸음**(랩탑 서브 실측). + ``` + 다섯 건 원 미만 절사 + 2025 울진소광 0.1 원 미만 절사 (57,077.2 · 49,479.1 · 35,608.3) + ``` + ⇒ **자릿수가 프로젝트마다 다름.** 실무 설정값임. + **프로젝트 설정 칸으로 둘 것 · 기본 0**(원 단위). 붙박이로 박지 말 것 - **단가산출 Q 식** — `Q = 3600 × q × K × f × E / Cm`. ⚠ **Q 를 소수 2자리로 먼저 확정한 뒤 나눌 것**(`55,700/15.708 = 3,545.96` ≠ `55,700/15.71 = 3,545.5`) @@ -590,7 +640,30 @@ IF 36(비교식 11) · VLOOKUP 13(**전부 근사**) · SUM 47. | 공종 축 ↔ 자원 축 | **서로 다른 표를 잇는 것** | ⛔ 안 됨. 코드로만 | | `#갈래` | **한 공종 안의 조건 딱지**(토질·암종·규격) | ⚠ 지금은 이름으로 맞추고 있어 약함 | -**⇒ 판정 — 마스터의 `variant_key` 를 정본으로 삼을 것.** +### ⚠⚠ 2026-09-13 정정 — 위 판정이 틀렸음 + +**`variant_key` 를 정본으로 못 씀.** 브레인이 「456표에 다 있다」만 보고 「쓸 수 있다」로 넘겼으나, +그 값은 **「표 첫 열 값 24개까지」**(`Build_WorkItemMaster.variant_axis`)임. + +``` +단가표 갈래 제목 294개 중 그 목록에 있는 것 86개 (약 1/3) +나머지 208 — 머리행에 갈래가 있는 표(벌목 「5m이상~8m미만」) + 두 단 머리(「굴착기(무한궤도)0.2·밀」) + 구간 이름 어긋남(표 「0.3~0.7m이하」 ↔ 목록 「0.3m미만」) + 첫 열이 자원 이름인 표 +``` + +⇒ 지금 정본으로 읽으면 **3분의 2가 폴백으로 떨어짐.** + +**고쳐 세운 판정 둘** + +1. **지금은 B09 단가표 제목을 정본으로 둘 것**(모양별 읽기가 이미 만든 것). + `find_variant_code` 의 **느슨한 맞춤**(수의 짝·구간 고르기)이 쓰일 때 **로그로 남길 것** — + **조용히 다른 갈래에 붙는 자리가 바로 거기임** +2. **마스터 생성기가 표 모양(첫 열 / 머리행 / 두 단)대로 `variant_key` 를 다시 뽑게** 할 것 — + **1장 마스터 일감**. 그것이 서면 그때 정본을 옮김 + +### (지난 판정) 마스터의 `variant_key` 를 정본으로 삼을 것 — ⛔ 위 정정으로 뒤집힘 - 마스터 **456표 전부에 `variant_key` 가 이미 있음**(브레인 확인). 그런데 **B09 가 안 읽고** 변형 문자열에서 매번 다시 만들어 씀