"""B08 수량 화면의 **근거 사전** — 어느 숫자가 어디서 와서 어떻게 나왔나 (PLAN 8-36 ④). ⚠⚠ **개발 전용.** 사전은 `provenance_payload()` 를 거쳐 나가고, 개발환경이 아니면 `None` 이라 응답에 칸 자체가 안 생긴다. 화면에서 숨기는 것이 아니라 **안 보내는 것**이다. 왜 이 파일인가 「식」과 「원천」의 정답은 값을 낳는 엔진이 안다. 화면 TS 에 손으로 적어 두면 엔진을 고칠 때 설명만 옛것으로 남는다. 엔진 옆(같은 폴더)에 두어 같이 눈에 들어오게 한다. ⚠ **열 단위로 적는다.** 토적표 한 장이 30열 × 200줄 = 6천 칸이라 칸마다 지으면 응답이 붐는다. 줄마다 갈리는 것(측구 안분 폴백 사유 등)은 줄이 이미 `notes` 로 들고 있고, 화면이 그것을 카드에 덧붙인다. ⚠ **토적표에는 `final`(최종) 열이 없다 — 억지로 붙이지 않았다.** 이 표는 중간 장부다. 내역서로 나가는 값은 **토공집계표**에서 선다. 여섯 등급을 한 장에 다 채우려고 아무 열에나 `final` 을 붙이면 「분류가 있다」는 거짓만 남는다. 이 어긋남은 등급을 고칠 근거이므로 PLAN 8-36 ① 에 그대로 남긴다. """ from __future__ import annotations from typing import Any from common_util.common_util_provenance import ( TIER_CALC, TIER_FINAL, TIER_INPUT, TIER_STANDARD, TIER_SURVEY, ColumnProvenance, provenance_payload, sheet_provenance, ) #: 토량환산계수가 어디서 오는지 — 여러 열이 같은 문장을 쓰므로 한 벌로 둔다. _FACTOR_SOURCE = ( "토량환산계수(다짐) — 기본값 `config_system_design.EARTHWORK_CONVERSION_FACTORS`, " "프로젝트가 고른 값이 있으면 산출 조건 패널의 값" ) #: 단면적 넷의 공통 원천. B06 이 낸 설계 단면을 **그대로** 읽는다(여기서 다시 안 짓는다). _SECTION_SOURCE = "B06 횡단 설계가 낸 측점별 단면적" #: 평균단면적법 한 줄. 신규 문서 5장 「다. 공사수량의 산출」. _MEAN_AREA = "(앞 측점 단면적 + 이 측점 단면적) ÷ 2 × 두 측점 사이 거리" def _area(key: str, label: str, extra: str = "") -> ColumnProvenance: """단면적 열 — B06 설계값을 그대로 옮긴 자리라 식이 없다.""" return ColumnProvenance( key=key, label=label, tier=TIER_SURVEY, formula="설계가 낸 값을 그대로 읽음 (여기서 다시 계산하지 않음)", source=_SECTION_SOURCE + (f" · {extra}" if extra else ""), code="B08_Quantity_Engine_EarthworkTable.py:StationArea.from_design", ) def _volume(key: str, label: str, area_label: str) -> ColumnProvenance: return ColumnProvenance( key=key, label=label, tier=TIER_CALC, formula=_MEAN_AREA.replace("단면적", area_label), source="첫 측점은 앞이 없어 비어 있음 (실무 토적표도 첫 줄 체적이 빈칸)", code="B08_Quantity_Engine_EarthworkTable.py:200 mean_volume", ) def _adjusted(key: str, label: str, volume_label: str) -> ColumnProvenance: return ColumnProvenance( key=key, label=label, tier=TIER_CALC, formula=f"{volume_label} × 토량환산계수(다짐)", source=_FACTOR_SOURCE, code="B08_Quantity_Engine_EarthworkTable.py:210", ) def earthwork_sheet() -> dict[str, Any]: """토적표 한 장의 사전. 열 키는 화면 `EarthworkRow` 와 같은 낱말이라야 한다.""" return sheet_provenance( [ ColumnProvenance( key="chainage_m", label="측점", tier=TIER_SURVEY, formula="노선 시점에서 잰 이정(m). 화면은 NO.n+m 으로 적음", source="B05 종단이 놓은 측점 배치", code="B08_Quantity_Engine_EarthworkTable.py:StationArea.chainage_m", ), ColumnProvenance( key="distance_m", label="거리", tier=TIER_CALC, formula="이 측점 이정 − 앞 측점 이정", source="B05 종단 측점 배치. 첫 줄은 앞이 없어 0", code="B08_Quantity_Engine_EarthworkTable.py:195", ), _area("cut_soil_area_m2", "절토 토사 단면적"), _volume("cut_soil_volume_m3", "절토 토사 입적", "절토 토사 단면적"), _adjusted("cut_soil_adjusted_m3", "절토 토사 보정량", "절토 토사 입적"), _area("cut_rock_area_m2", "절토 암석 단면적", "암 갈래는 측점의 `cut_rock_kind`"), _volume("cut_rock_volume_m3", "절토 암석 입적", "절토 암석 단면적"), _adjusted("cut_rock_adjusted_m3", "절토 암석 보정량", "절토 암석 입적"), _area( "ditch_soil_area_m2", "측구터파기 토사 단면적", "지반 유형·암반 경계선으로 B06 이 가른 값. 가름이 없는 옛 저장분만 " "절토 토사:암 면적비로 안분하고 그 줄에 사유가 남음", ), _volume("ditch_soil_volume_m3", "측구터파기 토사 입적", "측구 토사 단면적"), _adjusted("ditch_soil_adjusted_m3", "측구터파기 토사 보정량", "측구 토사 입적"), _area( "ditch_rock_area_m2", "측구터파기 암석 단면적", "위와 같은 가름값. 0.0 은 설계가 낸 「없음」이고 값 없음과 다름", ), _volume("ditch_rock_volume_m3", "측구터파기 암석 입적", "측구 암석 단면적"), _adjusted("ditch_rock_adjusted_m3", "측구터파기 암석 보정량", "측구 암석 입적"), ColumnProvenance( key="adjusted_total_m3", label="보정량계", tier=TIER_CALC, formula="절토 토사 보정량 + 절토 암석 보정량 + 측구 토사 보정량 + 측구 암석 보정량", source="네 보정량의 합. 성토에 쓸 수 있는 양으로 환산한 뒤의 값", code="B08_Quantity_Engine_EarthworkTable.py:215", ), _area("fill_area_m2", "성토 단면적"), _volume("fill_volume_m3", "성토 입적", "성토 단면적"), ColumnProvenance( key="diverted_m3", label="유용토", tier=TIER_CALC, formula="min(보정량계, 성토 입적)", source="그 측점에서 절취분과 성토분이 서로 만나는 몫", code="B08_Quantity_Engine_EarthworkTable.py:222", ), ColumnProvenance( key="balance_m3", label="차인토량", tier=TIER_CALC, formula="보정량계 − 성토 입적", source="양수면 남는 흙(사토), 음수면 모자란 흙(객토)", code="B08_Quantity_Engine_EarthworkTable.py:223", ), ColumnProvenance( key="cumulative_m3", label="누가토량", tier=TIER_CALC, formula="첫 줄부터 이 줄까지 차인토량을 더해 온 값", source="유토곡선(mass haul)의 세로축이 되는 값", code="B08_Quantity_Engine_EarthworkTable.py:225", ), ] ) def summary_sheet() -> dict[str, Any]: """토공집계표 — 토적표·사면표를 공종별 총량으로 모은 장. ⚠ **B08 에서 `final`(최종)이 처음 서는 자리다.** 토적표는 중간 장부였고, 내역서로 나가는 값은 여기 「계」다. 다만 무대(소운반 20m)처럼 **집계에는 오르되 내역 줄이 되지 않는** 줄이 있어, 그 줄의 「계」는 칸 등급 `excluded` 로 덮어쓴다(화면 배선). """ return sheet_provenance( [ ColumnProvenance( key="group", label="구분", tier=TIER_STANDARD, formula="품셈 공종 갈래 이름을 그대로 씀 (흙깎기·성토·측구터파기…)", source="거창 실무 토공집계표 시트의 열 문구를 그대로 옮김", code="B08_Quantity_Engine_EarthworkSummary.py:SummaryRow", ), ColumnProvenance( key="item", label="공종", tier=TIER_STANDARD, formula="지반 갈래 이름 (토사·연암·발파암…)", source="갈래 수는 프로젝트 설정의 암 갈래 세트가 정함 — 코드에 안 박음", rule="암 총량을 설계자가 넣은 갈래 비율(%)로 나눠 줄을 만듦", code="B08_Quantity_Engine_EarthworkSummary.py:_rock_split", ), ColumnProvenance( key="spec", label="규격", tier=TIER_STANDARD, formula="시공 방법 표기 (기계(굴삭기)·백호우…)", source="품셈 공종이 요구하는 규격. 암은 시공법(긁어내기/터뜨리기)이 갈림", code="B08_Quantity_Engine_EarthworkSummary.py:SummaryRow", ), ColumnProvenance( key="unit", label="단위", tier=TIER_STANDARD, formula="품셈 공종이 정한 단위 (㎥·㎡·주…)", source="단위가 다르면 내역 단가와 안 맞음 — 여기서 정하지 않고 품셈을 따름", code="B08_Quantity_Engine_EarthworkSummary.py:SummaryRow", ), ColumnProvenance( key="amount", label="계", tier=TIER_FINAL, formula="토적표·사면표 총량 × 반영률(%)", source=( "토공은 토적표 합계, 사면은 사면표 합계. ⚠ 반영률은 법정값이 아니라 " "설계자가 넣는 값이고 기본 100 %" ), rule="무대(소운반 20m)는 집계에는 오르되 내역 줄이 아님 — 그 줄은 「제외」로 섬", code="B08_Quantity_Engine_EarthworkSummary.py:build_table", ), ] ) def haul_sheet() -> dict[str, Any]: """운반거리 — (운반수단 × 지반유형)별 가중평균 줄. ⚠⚠ **상태가 둘이다.** 거리는 다짐상태로 재고, 내역에 오르는 수량만 자연상태(÷C)로 낸다 (설계실무 요령 5-4-3). 이 표의 「토량」은 **다짐상태**이므로 내역서 수량과 숫자가 다르다 — 그 어긋남이 정상이라는 것을 카드가 말해 주어야 헛걸음을 안 한다. """ return sheet_provenance( [ ColumnProvenance( key="equipment", label="운반수단", tier=TIER_CALC, formula="유토곡선이 띠마다 고른 수단 (무대·도자운반·덤프운반)", source="B06 운반계획(HaulPlan)의 띠. 여기서 다시 고르지 않음", code="B08_Quantity_Engine_HaulSummary.py:_legs_of", ), ColumnProvenance( key="ground", label="지반유형", tier=TIER_CALC, formula="띠의 토량을 절토 구간 구성비로 안분한 세 갈래 (토사·리핑암·발파암)", source="B06 운반계획이 이미 안분해 둔 값", code="B08_Quantity_Engine_HaulSummary.py:_legs_of", ), ColumnProvenance( key="volume_m3", label="토량", tier=TIER_CALC, formula="그 갈래에 속한 근거 구간들의 토량 합", source=( "⚠ **다짐상태**임. 내역서에 오르는 수량은 자연상태(÷토량환산계수)라 " "숫자가 다름 — 어긋난 것이 아님" ), code="B08_Quantity_Engine_HaulSummary.py:summarize", ), ColumnProvenance( key="average_distance_m", label="평균운반거리", tier=TIER_CALC, formula="Σ(토량 × 거리) ÷ Σ(토량) — 단순평균이 아님", source="실무 산출서가 「토량 × 거리」를 쌓아 나누는 그 식", code="B08_Quantity_Engine_HaulSummary.py:119 average_distance_m", ), ColumnProvenance( key="legs", label="근거 구간", tier=TIER_CALC, formula="이 평균을 만든 구간의 개수", source="구간 줄은 버리지 않고 표 아래 근거로 함께 냄 — 되짚을 수 있어야 함", code="B08_Quantity_Engine_HaulSummary.py:190", ), ] ) def preparation_sheet() -> dict[str, Any]: """준비공·사방공 — **못 서는 줄도 서는 장.** 빈 표를 내면 「빠뜨린 것」과 「원래 없는 것」이 구별되지 않는다. 그래서 값이 없는 줄도 상태와 사유를 달아 그대로 세운다. 값이 비어 있는 줄의 「수량」은 칸 등급 `blocked` 로 덮어쓴다 — **근거가 오면 채워질 자리**이지 일부러 비운 자리가 아니다. """ return sheet_provenance( [ ColumnProvenance( key="group", label="구분", tier=TIER_STANDARD, formula="준비공·사방공의 갈래 이름", source="품셈 9장(준비공)과 배치된 구조물 종류가 줄을 만듦", code="B08_Quantity_Engine_Preparation.py:build_table", ), ColumnProvenance( key="item", label="공종", tier=TIER_STANDARD, formula="품셈 공종 이름 (표토제거·제근·임목파쇄…)", source="공종이 없으면 줄도 없음 — 화면에서 이름을 짓지 않음", code="B08_Quantity_Engine_Preparation.py:build_table", ), ColumnProvenance( key="unit", label="단위", tier=TIER_STANDARD, formula="품셈 공종이 정한 단위", source="단위가 다르면 내역 단가와 안 맞음", code="B08_Quantity_Engine_Preparation.py:build_table", ), ColumnProvenance( key="amount", label="수량", tier=TIER_CALC, formula="공종마다 다름 — 표토제거는 면적 × 표토 두께(T), 제근은 임목축적 등급", source=( "밑수는 사면표·구조물 목록이 내고, 두께·등급·개소는 산출 조건 패널에서 " "설계자가 넣음" ), rule="넣어야 할 값이 비면 줄은 서되 수량이 「-」로 남고 사유가 붙음", code="B08_Quantity_Engine_Preparation.py:build_table", ), ColumnProvenance( key="status", label="상태", tier=TIER_CALC, formula="값을 세웠나 못 세웠나", source="못 세운 줄은 옆 칸에 사유가 붙음 — 사유가 곧 무엇을 넣어야 하는지임", code="B08_Quantity_Engine_Preparation_Status.py", ), ] ) def material_sheet() -> dict[str, Any]: """자재총괄 — 구조물 원단위에서 자재별로 모은 장. 관급/사급을 줄마다 고른다.""" return sheet_provenance( [ ColumnProvenance( key="name", label="자재", tier=TIER_STANDARD, formula="품셈·카탈로그의 자재 이름", source="구조물 원단위의 성분 이름을 그대로 모음 — 여기서 이름을 짓지 않음", code="B08_Quantity_Engine_MaterialSummary.py", ), ColumnProvenance( key="unit", label="단위", tier=TIER_STANDARD, formula="자재가 팔리는 단위 (㎥·본·kg…)", source="단가가 붙는 단위와 같아야 함", code="B08_Quantity_Engine_MaterialSummary.py", ), ColumnProvenance( key="net_amount", label="순수량", tier=TIER_CALC, formula="구조물마다 낸 성분 수량의 합 (할증 전)", source="구조물 원단위 표의 「수량」을 자재 이름으로 모은 값", code="B08_Quantity_Engine_MaterialSummary.py", ), ColumnProvenance( key="surcharge_pct", label="할증률", tier=TIER_STANDARD, formula="자재마다 정해진 할증률(%)", source="할증 판(dataset)이 정함. 판에 없는 자재는 「-」로 두고 지어내지 않음", code="B08_Quantity_Engine_MaterialSummary.py", ), ColumnProvenance( key="total_amount", label="총수량", tier=TIER_FINAL, formula="순수량 × (1 + 할증률)", source="내역서·자재대로 나가는 값. 할증률이 없으면 순수량 그대로", code="B08_Quantity_Engine_MaterialSummary.py", ), ColumnProvenance( key="supply", label="관급/사급", tier=TIER_INPUT, formula="설계자가 줄마다 고름", source="자재마다 갈리는 발주 결정이라 표 안에서 고름 (2026-09-07 확정)", code="B08_Quantity_UI_MaterialGrid.ts", ), ColumnProvenance( key="install_by", label="설치 주체", tier=TIER_INPUT, formula="설계자가 줄마다 고름", source="⚠ **관급 줄에만 뜻이 있음** — 사급으로 되돌리면 값이 비워짐", code="B08_Quantity_UI_MaterialGrid.ts", ), ] ) def unit_quantity_sheet() -> dict[str, Any]: """구조물 원단위 — 치수에서 성분까지. 성분마다 갈 곳을 적는다. ⚠ 이 장은 **근거·출처 열을 이미 화면에 들고 있다**(2026-09-09 부터). 사전은 그 열이 무엇을 뜻하는지 설명하는 자리이지, 있는 값을 다시 만드는 자리가 아니다. """ return sheet_provenance( [ ColumnProvenance( key="structure", label="구조물", tier=TIER_SURVEY, formula="B05 노선에 놓인 구조물의 이름과 놓인 측점", source="측점 표기(NO.4 ~ NO.4+10)는 화면이 만듦 — 서버는 이정만 냄", code="B08_Quantity_Engine_Handoff_Rows_Prep.py:182", ), ColumnProvenance( key="spec", label="규격", tier=TIER_SURVEY, formula="구조물 제원 (길이 × 높이)", source="B05·B06 이 배치할 때 정한 치수. 여기서 다시 정하지 않음", code="B08_Quantity_Engine_UnitQuantity.py", ), ColumnProvenance( key="component", label="성분", tier=TIER_STANDARD, formula="그 구조물이 쓰는 재료·공종 이름", source="품셈 표 또는 실무 관측 원단위표가 정함", code="B08_Quantity_Engine_UnitQuantity.py", ), ColumnProvenance( key="unit", label="단위", tier=TIER_STANDARD, formula="성분이 세어지는 단위", source="단가가 붙는 단위와 같아야 함", code="B08_Quantity_Engine_UnitQuantity.py", ), ColumnProvenance( key="amount", label="수량", tier=TIER_CALC, formula="치수 전개(길이·높이로 편 식) 또는 실무 관측 원단위 × 개소", source="어느 쪽인지는 같은 줄의 「출처」 칸이 말해 줌 (치수 전개 / 실무 관측)", rule="치수 전개는 식이 있고, 실무 관측은 관측값이라 식이 없음 — 둘을 섞지 않음", code="B08_Quantity_Engine_UnitQuantity.py", ), ColumnProvenance( key="destination", label="갈 곳", tier=TIER_STANDARD, formula="이 성분이 어느 표로 가는가 (자재총괄·공종 내역·양쪽)", source=( "⚠ 갈 곳이 겹치면 이중계상임 — 그것을 막으려고 성분마다 갈 곳을 적음 (PLAN 8-7)" ), code="B08_Quantity_Engine_Handoff_Mapping.py", ), ] ) def quantity_provenance() -> dict[str, Any] | None: """B08 응답에 실을 사전 — **개발환경이 아니면 `None`.** 시트를 늘릴 때는 여기 한 줄만 더한다. 화면은 시트 이름으로 찾아 쓴다. """ return provenance_payload( { "earthwork": earthwork_sheet(), "summary": summary_sheet(), "haul": haul_sheet(), "preparation": preparation_sheet(), "material": material_sheet(), "unit_quantity": unit_quantity_sheet(), } )