diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff_Rows.py b/B08_Quantity/B08_Quantity_Engine_Handoff_Rows.py index 61303b3d..8aeafd70 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff_Rows.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff_Rows.py @@ -603,8 +603,10 @@ def _preparation_rows(preparation_table: dict[str, Any]) -> list[dict[str, Any]] # ⚠ 못 서는 까닭을 그대로 넘긴다 — 받는 쪽이 「만들어야 할 것」 목록에 얹는다. "blocked_kind": None if ready else _prep_blocked_kind(status), "blocked_reason": "" if ready else str(row.get("reason") or status), - "variant_axis": None, - "variant_value": None, + # ⚠ 준비공 줄도 갈래를 실어 보낸다(2026-09-09) — 종전에는 늘 `None` 이라 + # 표토 운반처럼 **부모 공종코드**로 가는 줄이 B09 에서 「후보 N건」에 머물렀다. + "variant_axis": row.get("variant_axis"), + "variant_value": row.get("variant_value"), "secondary_axes": None, "spec_class": None, "spec_class_basis": str(row.get("reason") or ""), diff --git a/B08_Quantity/B08_Quantity_Engine_Preparation.py b/B08_Quantity/B08_Quantity_Engine_Preparation.py index 52642610..50635c05 100644 --- a/B08_Quantity/B08_Quantity_Engine_Preparation.py +++ b/B08_Quantity/B08_Quantity_Engine_Preparation.py @@ -44,10 +44,20 @@ BATTER_MIN_SLOPE_LENGTH_M = 10.0 BATTER_INTERVAL_M = 20.0 LEVEL_MIN_FILL_HEIGHT_M = 5.0 -STATUS_COUNTED_ELSEWHERE = "다른 표에서 이미 섬" -STATUS_PENDING = "값을 낼 근거가 없음" -STATUS_NOT_APPLICABLE = "해당 없음" -STATUS_READY = "값 있음" +# ⚠ 상태 낱말은 `_Preparation_Status` 한 벌에서 온다 — 700줄 제한으로 부대시설을 갈라 낼 때 +# 두 파일이 같은 문자열을 각자 적으면 조용히 갈릴 자리였다(2026-09-09). +from B08_Quantity.B08_Quantity_Engine_Preparation_Ancillary import ( # noqa: E402 + ANCILLARY_ITEMS, + ancillary_rows, +) +from B08_Quantity.B08_Quantity_Engine_Preparation_Status import ( # noqa: E402 + STATUS_COUNTED_ELSEWHERE, + STATUS_NOT_APPLICABLE, + STATUS_PENDING, + STATUS_READY, +) + +__all__ = ["ANCILLARY_ITEMS", "ancillary_rows"] # 갈라 나간 뒤에도 여기서 읽을 수 있게 def batter_frame_count(slope_rows: Iterable[dict[str, Any]]) -> tuple[int, list[str]]: @@ -125,7 +135,8 @@ def preparation_rows( "원문이 가르지 않고 실무도 한 덩이로 셈). " "토공집계의 「지장목제거」로 이미 섬 — 여기서 또 세우면 이중계상. " "⚠ 다만 **공종 미확정** — 품셈 4장이 벌목을 목적별로 갈라(수확베기·단목베기·" - "위험목 베기) 임도 지장목이 어디에 붙는지 원본이 말하지 않음. 지금은 공종코드 없이 감." + "위험목 베기) 임도 지장목이 어디에 붙는지 원본이 말하지 않음." + " 지금은 공종코드 없이 감." ), "work_item_code": None, }, @@ -170,8 +181,8 @@ def _topsoil_row(slope: dict[str, float], thickness_m: float | None) -> dict[str "amount": None, "status": STATUS_PENDING, "reason": ( - "표토 두께가 아직 입력되지 않았습니다 — 품셈 9-15 [주]② 가 두께를 " - "「공식의 입력 변수(T)」로 두어 **품셈이 정하는 값이 아닙니다**. " + "표토 두께가 아직 입력되지 않았습니다 — 두께는 설계가 정하는 값입니다. " + f"{TOPSOIL_ORIGINAL_APPLIED} " f"산출 조건에서 두께를 넣으면 값이 섭니다 (대상 면적 {area:,.1f}㎡)" ), "reference_amount": area, @@ -189,6 +200,20 @@ def _topsoil_row(slope: dict[str, float], thickness_m: float | None) -> dict[str } +#: ⚠⚠ **원문에 적용값이 있다**(2026-09-09 마스터 원문 대조에서 잡음). +#: 품셈 **9-15-2 답(畓)외구간** 표는 조건을 이렇게 박아 둔다 — +#: `T(표토두께) 0.2m` · `L(운반거리) 20m` · `E 0.4` · 도자(삽날 3.2㎥) +#: ⇒ 앞서 여기 「품셈이 정하는 값이 아니다」라고 적었는데 **그 말이 반쪽이었다.** +#: 두께를 바꾸면 품이 달라지는 것은 맞지만, **원문 적용값은 0.2m 로 적혀 있다.** +#: 지어내지 않되 **원문에 있는 값을 없다고 말하지도 않는다.** +#: ⚠⚠ **그리고 그 품에 20m 압토가 이미 들어 있다** — 실무 내역서(영월 2024)가 정확히 +#: 그 모양이다: 「표토제거 답외구간 / M2 · 도자 19Ton · D=20」 **한 줄뿐이고 운반 줄이 +#: 따로 없다.** 우리 「표토 운반·적치」 줄은 그래서 **20m 를 넘는 몫**일 때만 새 줄이다. +TOPSOIL_ORIGINAL_APPLIED = ( + "⚠ 품셈 9-15-2(답외구간) 원문 적용값은 **T=0.2m · L(운반거리)=20m** 입니다" + " — 값을 자동으로 넣지 않되, 원문에 있는 값이므로 참고할 것" +) + #: ⚠⚠ **대상 면적이 법 문언과 어긋난다**(2026-09-09 준비공 축 감사에서 잡음). #: 별표2 Ⅰ.2.차.(6) 은 대상을 **「노면·절토대상지」**로 못박고, 성토대상지는 (7) 에서 #: 「표토 등은 **제거·정리**한다」로 따로 두어 **운반·적치 의무를 안 건다.** @@ -213,6 +238,9 @@ TOPSOIL_AREA_GAP = ( TOPSOIL_HAUL_LAW = ( "⚠ 법정 의무 — 시행규칙 별표2 「표토는 전량 제거한 후 … 최고 홍수위보다 높은 장소로" " 운반하고 쌓아두어야 한다」. 제거만 세면 운반이 빠짐" + " · ⚠⚠ **다만 제거 품(9-15-2)에 이미 L=20m 압토가 들어 있음** — 실무 내역서도" + " 「표토제거 답외구간 / M2」 한 줄뿐이고 운반 줄이 따로 없음. 이 줄은 **20m 를 넘는" + " 몫**일 때만 새 줄이며, 그 가름을 아직 안 함(확인 필요)" ) @@ -405,9 +433,18 @@ def _topsoil_haul_row(topsoil: dict[str, Any], distance_m: float | None) -> dict "unit": "㎥", "amount": float(amount), "status": STATUS_READY, - "reason": f"{TOPSOIL_HAUL_LAW} · 제거 물량 그대로 · 운반거리 {distance:g}m(설계 입력)", + "reason": ( + f"{TOPSOIL_HAUL_LAW} · 제거 물량 그대로 · 운반거리 {distance:g}m(설계 입력)" + " · 갈래 「토사」로 보냄(표토는 토사임 — 덤프 운반 10-12 는 토사/암절취/발파암으로" + " 갈리고 부모 코드에는 품이 없음)" + ), "work_item_code": "FP-10-12", "haul_distance_m": distance, + # ⚠ `FP-10-12` 는 **부모**다 — 품이 붙은 것은 잎(10-12-1 토사 · -2 암절취 · -3 발파암) + # 뿐이라 갈래를 안 보내면 B09 가 「후보 3건」만 보이고 금액이 안 선다. 표토가 토사인 + # 것은 다툼이 없으므로 여기서 갈래를 실어 보낸다(2026-09-09 마스터 대조에서 잡음). + "variant_axis": "ground_class", + "variant_value": "토사", } @@ -561,104 +598,6 @@ def erosion_rows( ] -#: 부대시설·가설공사 — **법이 요구하는데 우리가 안 내던 다섯 줄**(2026-09-09 사용자 확정 ⑬). -#: `key` 는 설정의 `ancillary_counts` 칸 이름, `code` 는 품셈 공종(없으면 `None`). -#: ⚠ 다섯 중 **품셈에 공종이 있는 것은 가설창고 하나뿐**이다(마스터 전수 확인). -#: 나머지 넷은 **금액이 못 선다** — 줄은 세우되 「공종 자체가 품셈에 없음」이라고 적는다. -#: 「아직 안 만든 것」과 갈라 적어야 다음에 할 일이 달라진다. -ANCILLARY_ITEMS: tuple[dict[str, Any], ...] = ( - { - "key": "national_point_sign", - "item": "국가지점번호판", - "unit": "개소", - "code": None, - "legal": True, - "why": ( - "⚠ 법정 의무 — 임도규정 제26조제5항 「국가지점번호판을 제작하여 500미터 마다 " - "설치·관리하되, 필요시 거리를 조정할 수 있으며」. ⚠ 기점 포함·종점 잔여·갈림길 " - "중복을 원문이 정하지 않아 **연장÷500 을 산식으로 쓰지 않는다** — 개소를 넣으면 섬" - ), - }, - { - "key": "guide_sign", - "item": "임도 안내판", - "unit": "개소", - "code": None, - "legal": True, - "why": ( - "⚠ 법정 의무 — 임도규정 제26조제6항 「임도의 시점 및 종점에 안내판…을 설치하여야 " - "한다」. 노선을 이어 가면 최초 시점·최종 종점에 둘 수 있어 **개소는 설계 판단**임" - ), - }, - { - "key": "gate", - "item": "차단기", - "unit": "개소", - "code": None, - "legal": False, - "why": "임도기술교본 11장(실무 참고) — 법령·행정규칙에는 없음. 설치 개소는 설계 판단", - }, - { - "key": "site_container", - "item": "가설창고(컨테이너)", - "unit": "개소", - "code": "FP-11-01", - "legal": False, - "why": "품셈 11-1 콘테이너형 가설건축물 — ⚠ 개소는 현장 조건이라 설계가 정함", - }, - { - "key": "flood_supplies", - "item": "수방대책 자재", - "unit": "식", - "code": None, - "legal": False, - "why": ( - "임도기술교본 11장 — 비닐·말뚝·마대·삽 등을 현장 입구에 비치. " - "품목·수량이 원문에 없어 **한 벌(식)로 받는다**" - ), - }, -) - -#: 품셈에 그 이름의 공종이 아예 없는 줄에 적는 사유. 「아직 안 만든 것」과 갈라 쓴다. -REASON_NO_WORK_ITEM = "품셈에 그 이름의 공종이 없음 — 금액은 별도 단가로만 설 수 있음" - - -def ancillary_rows(counts: dict[str, Any] | None = None) -> list[dict[str, Any]]: - """부대시설·가설공사 줄. **개소를 안 넣어도 줄은 선다** — 빠진 것이 보이게. - - ⚠ 개소를 **지어내지 않는다**(확정 ⑬). 설정 `ancillary_counts` 에 넣은 값만 쓴다. - """ - given = {str(key): value for key, value in (counts or {}).items()} - rows: list[dict[str, Any]] = [] - for spec in ANCILLARY_ITEMS: - raw = given.get(spec["key"]) - try: - amount = float(raw) if raw is not None and str(raw).strip() != "" else None - except (TypeError, ValueError): - amount = None - reasons = [spec["why"]] - if spec["code"] is None: - reasons.append(REASON_NO_WORK_ITEM) - if amount is None: - reasons.append("개소가 아직 입력되지 않았습니다 — 넣으면 물량이 섭니다") - status = STATUS_PENDING - else: - status = STATUS_READY if spec["code"] else STATUS_PENDING - rows.append( - { - "group": "부대시설", - "item": spec["item"], - "unit": spec["unit"], - "amount": amount, - "status": status, - "work_item_code": spec["code"], - "legal_required": bool(spec["legal"]), - "reason": " · ".join(reasons), - } - ) - return rows - - def build_table( slope_totals: dict[str, float] | None = None, structures: Iterable[dict[str, Any]] = (), diff --git a/B08_Quantity/B08_Quantity_Engine_Preparation_Ancillary.py b/B08_Quantity/B08_Quantity_Engine_Preparation_Ancillary.py new file mode 100644 index 00000000..a30d9b55 --- /dev/null +++ b/B08_Quantity/B08_Quantity_Engine_Preparation_Ancillary.py @@ -0,0 +1,112 @@ +"""준비공 — 부대시설·가설공사 다섯 줄. + +⚠ `B08_Quantity_Engine_Preparation` 에서 갈라 나온 파일이다(2026-09-09, 700줄 제한). + **내용은 그대로 옮겼고 규칙도 그대로다** — 개소를 지어내지 않고, 「품셈에 공종이 없음」과 + 「개소 미입력」을 갈라 적으며, 값이 없어도 줄은 세운다. +""" + +from __future__ import annotations + +from typing import Any + +from B08_Quantity.B08_Quantity_Engine_Preparation_Status import ( + REASON_NO_WORK_ITEM, + STATUS_PENDING, + STATUS_READY, +) + +#: 부대시설·가설공사 — **법이 요구하는데 우리가 안 내던 다섯 줄**(2026-09-09 사용자 확정 ⑬). +#: `key` 는 설정의 `ancillary_counts` 칸 이름, `code` 는 품셈 공종(없으면 `None`). +#: ⚠ 다섯 중 **품셈에 공종이 있는 것은 가설창고 하나뿐**이다(마스터 전수 확인). +#: 나머지 넷은 **금액이 못 선다** — 줄은 세우되 「공종 자체가 품셈에 없음」이라고 적는다. +#: 「아직 안 만든 것」과 갈라 적어야 다음에 할 일이 달라진다. +ANCILLARY_ITEMS: tuple[dict[str, Any], ...] = ( + { + "key": "national_point_sign", + "item": "국가지점번호판", + "unit": "개소", + "code": None, + "legal": True, + "why": ( + "⚠ 법정 의무 — 임도규정 제26조제5항 「국가지점번호판을 제작하여 500미터 마다 " + "설치·관리하되, 필요시 거리를 조정할 수 있으며」. ⚠ 기점 포함·종점 잔여·갈림길 " + "중복을 원문이 정하지 않아 **연장÷500 을 산식으로 쓰지 않는다** — 개소를 넣으면 섬" + ), + }, + { + "key": "guide_sign", + "item": "임도 안내판", + "unit": "개소", + "code": None, + "legal": True, + "why": ( + "⚠ 법정 의무 — 임도규정 제26조제6항 「임도의 시점 및 종점에 안내판…을 설치하여야 " + "한다」. 노선을 이어 가면 최초 시점·최종 종점에 둘 수 있어 **개소는 설계 판단**임" + ), + }, + { + "key": "gate", + "item": "차단기", + "unit": "개소", + "code": None, + "legal": False, + "why": "임도기술교본 11장(실무 참고) — 법령·행정규칙에는 없음. 설치 개소는 설계 판단", + }, + { + "key": "site_container", + "item": "가설창고(컨테이너)", + "unit": "개소", + "code": "FP-11-01", + "legal": False, + "why": "품셈 11-1 콘테이너형 가설건축물 — ⚠ 개소는 현장 조건이라 설계가 정함", + }, + { + "key": "flood_supplies", + "item": "수방대책 자재", + "unit": "식", + "code": None, + "legal": False, + "why": ( + "임도기술교본 11장 — 비닐·말뚝·마대·삽 등을 현장 입구에 비치. " + "품목·수량이 원문에 없어 **한 벌(식)로 받는다**" + ), + }, +) + +#: 품셈에 그 이름의 공종이 아예 없는 줄에 적는 사유. 「아직 안 만든 것」과 갈라 쓴다. + + +def ancillary_rows(counts: dict[str, Any] | None = None) -> list[dict[str, Any]]: + """부대시설·가설공사 줄. **개소를 안 넣어도 줄은 선다** — 빠진 것이 보이게. + + ⚠ 개소를 **지어내지 않는다**(확정 ⑬). 설정 `ancillary_counts` 에 넣은 값만 쓴다. + """ + given = {str(key): value for key, value in (counts or {}).items()} + rows: list[dict[str, Any]] = [] + for spec in ANCILLARY_ITEMS: + raw = given.get(spec["key"]) + try: + amount = float(raw) if raw is not None and str(raw).strip() != "" else None + except (TypeError, ValueError): + amount = None + reasons = [spec["why"]] + if spec["code"] is None: + reasons.append(REASON_NO_WORK_ITEM) + if amount is None: + reasons.append("개소가 아직 입력되지 않았습니다 — 넣으면 물량이 섭니다") + status = STATUS_PENDING + else: + status = STATUS_READY if spec["code"] else STATUS_PENDING + rows.append( + { + "group": "부대시설", + "item": spec["item"], + "unit": spec["unit"], + "amount": amount, + "status": status, + "work_item_code": spec["code"], + "legal_required": bool(spec["legal"]), + "reason": " · ".join(reasons), + } + ) + return rows diff --git a/B08_Quantity/B08_Quantity_Engine_Preparation_Status.py b/B08_Quantity/B08_Quantity_Engine_Preparation_Status.py new file mode 100644 index 00000000..79c3f49a --- /dev/null +++ b/B08_Quantity/B08_Quantity_Engine_Preparation_Status.py @@ -0,0 +1,16 @@ +"""준비공 줄의 **상태 낱말 한 벌**. + +⚠ `_Preparation` 과 `_Preparation_Ancillary` 가 같은 문자열을 각자 적으면 **조용히 갈린다** + (받는 쪽이 `status == "값 있음"` 으로 판정하므로 한 글자만 달라도 줄이 막힌 것으로 읽힌다). + 그래서 700줄 제한으로 파일을 가를 때 상수만 여기 한 벌 둔다(2026-09-09). +""" + +from __future__ import annotations + +STATUS_READY = "값 있음" +STATUS_PENDING = "값을 낼 근거가 없음" +STATUS_COUNTED_ELSEWHERE = "다른 표에서 이미 섬" +STATUS_NOT_APPLICABLE = "해당 없음" + +#: 품셈에 그 이름의 공종이 아예 없는 줄 — 「아직 안 만든 것」과 갈라 적는다. +REASON_NO_WORK_ITEM = "품셈에 그 이름의 공종이 없음 — 금액은 별도 단가로만 설 수 있음"