"""거푸집 사용횟수 — 접촉 면적에 **몇 회 쓰는 거푸집인지**를 붙인다 (B08 일감 ⑩). ⚠ 사용횟수는 **관측값이 아니라 법이다** 품셈 1-7-1 이 구조물 종류별로 정해 둔다 — 「3회 … 옹벽, 파라펫트, 날개벽 등 약간 복잡한 구조」. 그래서 실무 관측값으로 갈음하지 않고 **원문 문구를 그대로 데이터에 싣고** 우리 구조물이 그 줄의 어느 예시에 걸리는지를 적는다. 걸리는 예시가 없으면 지어내지 않는다. ⚠⚠ **횟수별 재료 환산은 여기서 하지 않는다** (이중계상) 품셈 12-4 의 「사용횟수별 기준수량에 대한 비율(%)」(합판 3회 46.1 % 등)은 **일위대가 재료비**에 걸리는 값이다. B08 이 면적에 그 비율을 곱해 넘기면 B09 가 또 곱해 두 번 준다. **B08 이 내는 것은 「접촉 면적 + 몇 회짜리인가」까지다.** 비율표는 참고로만 싣는다. ⚠ 동바리는 지금 대상이 없다 강관동바리(12-20)는 **슬래브를 떠받칠 때** 쓴다. 지금 서는 구조물(옹벽·집수정)은 벽체 거푸집뿐이라 대상이 아니고, 대상이 될 BOX암거·세월교는 치수·원단위가 미확보라 슬래브 면적 자체가 안 나온다. **없는 것을 0 으로 적지 않고 「대상 없음」이라고 말한다.** """ from __future__ import annotations import json from dataclasses import dataclass, field from pathlib import Path from typing import Any DATASET_DIR = Path(__file__).resolve().parents[1] / "resources" / "data_formwork" DATASET_PREFIX = "formwork_reuse_" #: 거푸집으로 보는 성분 이름. 정확히 같은 이름으로만 본다 — 부분일치면 「거푸집씻기」가 걸린다. FORMWORK_NAMES = frozenset({"합판거푸집", "유로폼", "문양거푸집", "거푸집"}) NOTE_REUSE_MISSING = "사용횟수 미확보" NOTE_NOT_APPLICABLE = "거푸집 대상 아님" def _latest_dataset_path(directory: Path | None = None) -> Path | None: folder = directory or DATASET_DIR if not folder.is_dir(): return None files = sorted(folder.glob(DATASET_PREFIX + "*.json")) return files[-1] if files else None @dataclass class FormworkTable: """사용횟수표 한 판.""" effective_date: str = "" source: dict[str, Any] = field(default_factory=dict) type_map: list[dict[str, Any]] = field(default_factory=list) reuse_by_class: list[dict[str, Any]] = field(default_factory=list) reuse_ratio_pct: dict[str, Any] = field(default_factory=dict) shoring: dict[str, Any] = field(default_factory=dict) #: 유로폼 설치·해체 유형 — 품셈 12-38-3 [주]④ 가 시설 예시로 갈라 둔 것. euroform_type: dict[str, Any] = field(default_factory=dict) def for_type(self, type_id: str) -> dict[str, Any] | None: for row in self.type_map: if row.get("type_id") == type_id: return row return None def load_formwork_table(path: Path | None = None) -> FormworkTable: """사용횟수표를 읽는다. 파일이 없으면 **빈 표** — 전부 「미확보」로 드러난다.""" target = path or _latest_dataset_path() if target is None or not target.is_file(): return FormworkTable() payload = json.loads(target.read_text(encoding="utf-8")) return FormworkTable( effective_date=str(payload.get("effective_date") or ""), source=payload.get("source") or {}, type_map=list(payload.get("type_map") or []), reuse_by_class=list(payload.get("reuse_by_class") or []), reuse_ratio_pct=payload.get("reuse_ratio_pct") or {}, shoring=payload.get("shoring") or {}, euroform_type=payload.get("euroform_type") or {}, ) def annotate( structures: list[dict[str, Any]], table: FormworkTable | None = None ) -> tuple[list[str], list[str]]: """산출물의 거푸집 성분에 사용횟수를 달아 준다. (알림, 미확보 종류) 를 돌려준다. 성분 딕셔너리를 **그 자리에서** 고친다 — 거푸집 줄만 손대고 나머지는 건드리지 않는다. """ found = table or load_formwork_table() notes: list[str] = [] missing: list[str] = [] for structure in structures: type_id = str(structure.get("type_id") or "") entry = found.for_type(type_id) targets = [ component for component in structure.get("components") or [] if str(component.get("name") or "").strip() in FORMWORK_NAMES ] if not targets: continue if entry is None: missing.append(type_id) for component in targets: component["reuse_count"] = None component["reuse_note"] = NOTE_REUSE_MISSING continue count = entry.get("reuse_count") for component in targets: component["reuse_count"] = count component["reuse_note"] = ( f"품셈 1-7-1 {count}회 — 「{entry.get('matched_example')}」" if count else NOTE_NOT_APPLICABLE ) if count: notes.append(f"{structure.get('name') or type_id} 거푸집 {count}회 (품셈 1-7-1)") else: missing.append(type_id) return notes, sorted(set(missing)) def shoring_status(table: FormworkTable | None = None) -> dict[str, Any]: """동바리 — **대상이 없으면 없다고 말한다.** 0 으로 적으면 「없음」과 구별이 안 된다.""" found = table or load_formwork_table() shoring = found.shoring or {} return { "applicable": False, "reason": str(shoring.get("note") or "슬래브 구조물이 없어 동바리 대상이 아님"), "pending_types": list(shoring.get("targets_pending") or []), }