diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff.py b/B08_Quantity/B08_Quantity_Engine_Handoff.py index dbeeae62..983589c4 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff.py @@ -61,6 +61,11 @@ ORIGIN_HAUL = "haul" METHOD_TO_GROUND = {"ripping": "리핑암", "blasting": "발파암"} NOTE_METHOD_MISSING = "시공법 미지정으로 공종을 못 고름" +#: 철근으로 보는 성분 이름 조각. **정확한 낱말이 아니라 앞머리**로 본다 — +#: 「이형철근 D13」·「원형철근」처럼 규격이 뒤에 붙기 때문이다. `철근콘크리트`는 성분 이름이 +#: 아니라 공종 이름이라 성분 목록에는 안 온다. +REBAR_PREFIXES = ("이형철근", "원형철근", "철근") + #: 사면 계열 — 토공집계에 함께 실리지만 출처가 사면적이다. SLOPE_GROUPS = frozenset({"성토면다짐", "초류종자살포", "지장목제거", "층따기", "면고르기"}) @@ -86,6 +91,7 @@ class WorkItemMapping: structure: list[dict[str, Any]] = field(default_factory=list) pending_user: dict[str, Any] = field(default_factory=dict) composite: dict[str, Any] = field(default_factory=dict) + concrete_placing: dict[str, Any] = field(default_factory=dict) def for_earthwork(self, group: str, ground: str | None) -> dict[str, Any] | None: # noqa: D401 """공종+지반유형으로 찾는다. 지반이 갈리지 않는 공종은 `ground` 없는 줄이 받는다.""" @@ -136,9 +142,33 @@ def load_mapping(path: Path | None = None) -> WorkItemMapping: structure=list(payload.get("structure") or []), pending_user=payload.get("pending_user") or {}, composite=payload.get("composite") or {}, + concrete_placing=payload.get("concrete_placing") or {}, ) +def structure_kind(structure: dict[str, Any]) -> str: + """콘크리트 구조물 종류 — **원단위에 철근이 있나 없나로 판정한다.** + + 사람이 고르는 값이 아니다(2026-09-07 3자 확정). 옹벽 관측 원단위에 `D13`·`D16` 이 + 실려 있으므로 철근구조물로 자동으로 선다. 소형구조물 판정 기준은 아직 없다. + """ + for component in structure.get("components") or []: + name = str(component.get("name") or "").strip() + if any(name.startswith(prefix) for prefix in REBAR_PREFIXES): + return "철근구조물" + return "무근구조물" + + +def placing_code(mapping: WorkItemMapping, method: str | None) -> tuple[str | None, bool]: + """(타설 공종코드, 기본값을 쓴 것인가). 모르는 방식이면 기본으로 떨어지되 그 사실을 알린다.""" + table = mapping.concrete_placing or {} + codes = table.get("method_codes") or {} + default = str(table.get("default_method") or "") + if method in codes: + return codes[method], False + return codes.get(default), True + + def _spec_detail(structure: dict[str, Any]) -> str: """규격 표기 — 저장된 제원에서 만든다. 없는 값은 적지 않는다.""" parts: list[str] = [] @@ -276,6 +306,7 @@ def _structure_rows( entry = mapping.for_structure(type_id) or {} code = entry.get("work_item_code") composite = mapping.composite_for(type_id) if code is None else None + kind = structure_kind(structure) if composite else None if code is None and composite is None: unmatched.append(f"구조물({type_id})") length = float(structure.get("length_m") or 0.0) @@ -298,6 +329,10 @@ def _structure_rows( "spec_detail": _spec_detail(structure), # 품셈에 그 이름의 공종이 없어 여러 공종을 묶는 자리 — 빈 코드와 구별한다. "composite_parts": (composite or {}).get("parts"), + # 철근이 있나 없나로 자동 판정 — 사람이 고르는 값이 아니다. + "structure_kind": kind, + # ⚠ 아직 일위대가가 안 선 공종 — 지금 세우면 절반짜리가 된다. + "composite_not_ready": (composite or {}).get("not_ready"), "in_bill": True, "in_bill_reason": (composite or {}).get("why", ""), "origin": ORIGIN_STRUCTURE, diff --git a/common_util/common_util_project_settings.py b/common_util/common_util_project_settings.py index 9fb57302..c274cf4d 100644 --- a/common_util/common_util_project_settings.py +++ b/common_util/common_util_project_settings.py @@ -93,6 +93,10 @@ def default_settings() -> dict[str, Any]: # ⚠ **법이 아니라 발주 결정**이라 기본은 비워 둔다. 안 정한 자재는 「미분류」로 # 화면에 드러난다 — 사급으로 조용히 넘기면 관급자재대가 새 나간다. "material_supply": {}, + # 콘크리트 타설 방식 — `ready_mixed`(FP-12-01-01) / `machine_mixed`(-02) / + # `hand_mixed`(-03). **설계 판단**이라 사용자가 고르는 값이고, 기본은 실무가 + # 쓰는 레디믹스트로 둔다. ⚠ 잠정이며 사용자 확정 대기 항목이다. + "concrete_placing_method": "ready_mixed", "dataset_versions": {}, }, "estimation": { diff --git a/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json b/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json index af19bd3b..423e8e63 100644 --- a/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json +++ b/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json @@ -165,14 +165,36 @@ { "type_id": "retaining_wall", "parts": [ - "FP-12-01 콘크리트 타설", + "FP-12-01-01#철근구조물", "FP-12-04 합판거푸집", "FP-12-03 철근 현장가공 및 조림", "FP-12-25 기초잡석" ], "why": "품셈 12장에 「옹벽」 공종이 없음. 실무 내역은 「반중력식옹벽 H=2.0」 한 줄이고 그 일위대가가 위 공종을 묶음.", - "needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김" + "needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김", + "placing_note": "타설 코드는 프로젝트 설정의 타설 방식으로 갈림(기본 레디믹스트). 철근구조물 판정은 원단위의 D13·D16 에서 자동으로 나옴.", + "not_ready": [ + "FP-12-03", + "FP-12-25" + ], + "not_ready_why": "B09 일위대가가 아직 안 섬 — 지금 세우면 절반짜리가 됨(2026-09-07 조율 창)" } ] + }, + "concrete_placing": { + "note": "콘크리트 타설은 **타설 방식 × 구조물 종류**로 갈린다. 방식은 설계 판단이라 프로젝트 설정(`quantity.concrete_placing_method`)이 고르고, 종류는 **원단위에 철근이 있나 없나로 자동 판정**한다 — 사람이 고르는 값이 아니다(2026-09-07 3자 확정).", + "method_codes": { + "ready_mixed": "FP-12-01-01", + "machine_mixed": "FP-12-01-02", + "hand_mixed": "FP-12-01-03" + }, + "default_method": "ready_mixed", + "default_is_provisional": true, + "structure_kinds": [ + "무근구조물", + "철근구조물", + "소형구조물" + ], + "kind_rule": "원단위 성분에 철근(이형철근·원형철근)이 있으면 철근구조물, 없으면 무근구조물. 소형구조물 판정 기준은 미확보." } }