From 42cd2b8fdd836b5db2d359aa3dec06e723d016c9 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 19:07:41 +0900 Subject: [PATCH] =?UTF-8?q?feat(B08):=20=EB=B6=80=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EC=84=A4=20=EB=8B=A4=EC=84=AF=20=EC=A4=84=EC=9D=84=20=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=EC=97=90=20=EC=B6=94=EA=B0=80=20(=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=ED=99=95=EC=A0=95=20=E2=91=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 법이 요구하는데 우리가 안 내던 줄들임 — 국가지점번호판·안내판·차단기·가설창고·수방자재. ⚠ 개소를 **지어내지 않음.** 임도규정 제26조제5항이 「500미터 마다 설치·관리하되 **필요시 거리를 조정**할 수 있으며」라 하고 기점 포함·종점 잔여·갈림길 중복을 원문이 정하지 않음 ⇒ `연장÷500` 을 산식으로 쓰지 않고 설정 `ancillary_counts` 로 **개소를 받음**. ⚠ 사유를 **갈라 적음** — 「품셈에 그 이름의 공종이 없음」과 「개소 미입력」은 다음에 할 일이 다름. 법정 의무인 둘(번호판 제26조5항 · 안내판 제26조6항)은 그 사실을 사유에 적어 화면에서 왜 채워야 하는지 보이게 함. 차단기는 교본에만 있어 「법정」이라 적지 않음. ⚠ 다섯 중 **품셈에 공종이 있는 것은 가설창고(FP-11-01) 하나뿐**(마스터 전수 확인). 나머지 넷은 금액이 못 서지만 **줄은 세움** — 빼면 빠진 줄을 아무도 못 봄. 개소를 안 넣어도 줄이 서고 사유가 함께 감(준비공 표 · 인계 양쪽). 시험 749 통과(새 6건 포함). Co-Authored-By: Claude Opus 5 (1M context) --- .../B08_Quantity_Engine_Preparation.py | 105 +++++++++++++++++- B08_Quantity/B08_Quantity_Router_Earthwork.py | 2 + common_util/common_util_project_settings.py | 7 ++ 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Engine_Preparation.py b/B08_Quantity/B08_Quantity_Engine_Preparation.py index ba97fa77..0973ab9f 100644 --- a/B08_Quantity/B08_Quantity_Engine_Preparation.py +++ b/B08_Quantity/B08_Quantity_Engine_Preparation.py @@ -246,16 +246,117 @@ 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]] = (), slope_rows: Iterable[dict[str, Any]] = (), topsoil_thickness_m: float | None = None, names: dict[str, str] | None = None, + ancillary_counts: dict[str, Any] | None = None, ) -> dict[str, Any]: """화면·인계가 그대로 쓰는 모양. **못 서는 줄도 목록에 남는다.**""" - rows = preparation_rows(slope_totals, slope_rows, topsoil_thickness_m) + erosion_rows( - structures, names + rows = ( + preparation_rows(slope_totals, slope_rows, topsoil_thickness_m) + + erosion_rows(structures, names) + + ancillary_rows(ancillary_counts) ) return { "columns": ["구분", "공종", "단위", "수량", "상태", "사유"], diff --git a/B08_Quantity/B08_Quantity_Router_Earthwork.py b/B08_Quantity/B08_Quantity_Router_Earthwork.py index fc4aeaff..63894e8c 100644 --- a/B08_Quantity/B08_Quantity_Router_Earthwork.py +++ b/B08_Quantity/B08_Quantity_Router_Earthwork.py @@ -131,6 +131,8 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse: slope.get("rows") or [], settings.get("topsoil_thickness_m"), {type_id: definition.name for type_id, definition in structure_type_map().items()}, + # 부대시설 개소 — 산식으로 만들지 않고 **설계자가 넣은 값**만 쓴다(확정 ⑬). + settings.get("ancillary_counts") or {}, ) method, method_is_default = concrete_placing_method(settings) # ⚠ 금액에 바로 걸리는 값이라 「기본값으로 돌고 있음」을 응답에 실어 화면이 띄우게 한다. diff --git a/common_util/common_util_project_settings.py b/common_util/common_util_project_settings.py index 879ce77d..8ab40ad4 100644 --- a/common_util/common_util_project_settings.py +++ b/common_util/common_util_project_settings.py @@ -102,6 +102,13 @@ def default_settings() -> dict[str, Any]: # ⚠ 기본은 `None` — 「안 정함」과 「일부러 레디믹스트를 고른 것」을 갈라야 # 화면이 「기본값 적용 중」을 정직하게 띄운다. 값을 미리 넣으면 그 구별이 사라진다. "concrete_placing_method": None, + # 부대시설 개소 — `{항목키: 개소}` (2026-09-09 사용자 확정 ⑬). + # ⚠ **산식으로 만들지 않는다.** 국가지점번호판은 임도규정 제26조제5항이 + # 「500미터 마다 설치·관리하되 **필요시 거리를 조정**」이라 하고, 기점 포함· + # 종점 잔여·갈림길 중복을 원문이 안 정한다(지식DB 부대시설 [구현]). + # ⇒ `ceil(연장÷500)` 을 확정 산식으로 쓰지 않고 **개소를 받는다.** + # 비워 두면 물량을 안 낸다(0 으로 때우지 않음). + "ancillary_counts": {}, "dataset_versions": {}, }, "estimation": {