Files
Aislo/B08_Quantity/B08_Quantity_Provenance.py
T
eomsangdonandClaude Opus 5 f952ac7ffd fix(git): 병합이 떨군 파일 22개와 되돌아간 파일 35개를 되살림
무슨 일이 있었나
랩탑 줄의 병합 `20ba886c`(Merge origin/main_desktop_1·main_laptop_1·sub_desktop_1 into
sub_laptop_1)가 우리 파일 22개를 떨구고 35개 파일의 내용을 옛것으로 되돌림. 손으로 지운
커밋은 없고 **병합 자체가 떨군 것**임. 그것이 `origin/dev`·`main_laptop_1`·`sub_laptop_1`·
`CODEX` 까지 퍼졌고(데스크탑 둘만 무사), 이 창의 병합 `d92c1f2b` 로 들어옴.

잃었던 것
- 공용 — `common_util_provenance.py` · `ui_template_provenance.ts`
- B08 — 근거 사전 · 좌측 패널 상자 모듈 · 토량환산계수 칸
- B09 — 근거 사전 셋
- B05 — 계획노선 편집 모듈 아홉 · 지형 라우터 · B04 지도 모듈
- 시험 셋과, 35개 파일 안의 최근 작업(환산계수 고르기 · 근거 호버 배선 등)

어떻게 되살렸나
`611a2b40`(병합 직전, 전부 온전)에서 `git show <커밋>:<경로>` 로 내용만 꺼내 되돌림.
이력은 안 건드림. ⚠ HEAD 에만 있던 「추가 816줄」은 랩탑의 새 작업이 아니라 **되살아난
옛 코드**였음(B05 편집은 모듈로 쪼개기 전 덩어리 · B08 라우터는 환산계수 고르기 전 옛
상수판). 되돌릴 시점 이후의 **진짜 새 커밋은 둘뿐**이라 그 둘만 패치로 다시 얹음 —
`9f827bf6`(리로드 빌드 고리 끊기, 데스크탑 보조) · `b9bca6b3`(B06 조정창 1px, 랩탑).
위키 여덟은 코덱스 몫이라 손대지 않음.

자체검증 — 양쪽 작업이 다 살아 있음을 짚어 확인: `main.py` 의 「개발 서버는 살려 둔다」 ·
`B05_Profile_Engine_Grade.py` 의 `plan_curve_length_limit_m` · `B08_..._EarthworkGrid.ts` 의
`attachProvenance`. `tsc --noEmit` 통과 · `pytest -q` **1317 passed, 28 skipped**
(되살리기 전에는 시험 둘이 수집 단계에서 깨져 있었음).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RANEBHns1S4tkmsYwewtk
2026-09-12 18:18:57 +09:00

474 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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(),
}
)