「원문이 이미 정해 둠」 **네 번째** — 품셈 12-38-3 [주]④ 「보통: 측구, 수로,
옹벽, 일반적인 벽체, 박스」. 앞선 셋(거푸집 사용횟수 1-7-1 · 철근 갈래
12-3 [주]① · 밑수는 표 위 본문)과 같은 자리라 사람이 고를 칸을 안 만듦.
옹벽 → `FP-12-38#보통`, 근거 문구를 조각에 함께 실음.
- ⚠ 12-38-1 「사용횟수」는 1-7-1 과 **다른 자리**임. 그쪽은 유로폼(강재)의
잔존율(12회 25% · 25회 10%)로 임대료·손료 산정용이고, 1-7-1 은 소모성
거푸집 전용 횟수(옹벽 3회)임. 이름이 같아 하나로 이으면 조용히 틀림 —
데이터에 그 구분을 적고 시험으로 못 박음.
- 유로폼 자재는 12-38-2 가 10㎡당 패널 0.89매로 냄. B08 은 **면적(㎡)** 으로
보내고 매수 환산은 밑수를 아는 B09 가 함 — B08 이 환산하면 밑수가 두 벌.
`composite_not_ready` 를 `{code, reason}` 구조로 바꿈. 받는 쪽이 「단가 없음」과
「물량 없음」을 갈라야 함(기초잡석이 후자).
원단위 자체가 없으면 조각을 늘어놓지 않고 한 줄로 말함 — 같은 사유가 조각마다
다섯 번 반복되면 진짜 사유가 묻힘(화면에서 실제로 그렇게 보였음).
시험 추가 — 같은 공종코드가 지반만 달리해 여러 줄 오는 표본(도자 토사·리핑암).
실물에서 잡힌 모양이라 고정 자료에도 넣음.
검증 — 인계 59건 통과, 전체 600 passed, tsc 오류 0.
화면 그리기는 실제 모듈을 그대로 돌려 확인(사용자 프로젝트·DB 무접촉).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
131 lines
5.8 KiB
Python
131 lines
5.8 KiB
Python
"""거푸집 사용횟수 — 접촉 면적에 **몇 회 쓰는 거푸집인지**를 붙인다 (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 []),
|
|
}
|