"""콘크리트 포장 전개식 — **면적 × 두께 · 거푸집 양면 · 수축줄눈** (2026-09-15 브레인 포장 둘). 근거 ① 임도기술교본 3-2 기본설계 — 「포장 폭의 3.0m, 두께 0.2m를 기준으로 곡선부에는 확폭」 ② 임도기술교본 부록 4-7 7-1 3.5.1 — 「수축줄눈의 간격은 4-6m를 기준」 ③ 산림품셈 12-6 콘크리트 포장(인력) — 비닐·철망깔기·포설·양생 포함 · 거푸집·줄눈 제외 · [주]② 비닐·양생재·철망 재료 별도 · 12-7-1 포장절단 · 12-8 콘크리트 포장 거푸집(연장 m) ④ 실무 울진소광 `02-1-공종수량산출` 「포장및난간개거」 — 면적 = B × L · 거푸집(1면기준) = 2L · 수축줄눈 = 내림(L÷6) × B · 「포장확폭」 면적 = 거리 × 확폭 · 신축줄눈 = 면적 ÷ 6 ⭐ 폭·확폭·두께는 **B06 이 이미 앎** — 빈 칸만 B06 에서 잇고 칸이 이김(판정 ①④ · 한 값 원칙). """ from __future__ import annotations import math from typing import Any from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Base import Component, _num, routed SOURCES = ( "산식 = 임도기술교본 3-2(폭·두께·곡선부 확폭) · 부록 4-7 7-1(수축줄눈 4-6m) · 실무 울진소광" " 「포장및난간개거」·「포장확폭」(면적 B×L · 거푸집 2L · 줄눈 내림(L÷간격)×B" " · 확폭 줄눈 면적÷간격)" ) NOTE_JOINT = ( "수축줄눈 = 12-7-1 포장절단(m)으로 이음 — 12-7-2 줄눈설치는 안 씀 — 소광 실무가 컷팅" "(「수축줄눈(컷팅)」) · 줄눈재를 넣는 공법이면 12-7-2 로 물을 것" ) NOTE_FORM = ( "포장거푸집 = 양쪽 가장자리 2 × 연장(소광 「1면기준」) · 12-8 이 강재 3m·핀폴·20회를 품에 둠" ) SHEET_UNCHOSEN = ( "비닐(깔기·양생)을 안 고름 — 품셈 12-6 [주]② 「양생에 필요한 재료비(비닐, 양생재 등)는 별도" " 계상」이라 설계자가 고를 것(기본값 없음)" ) MESH_UNCHOSEN = ( "철망을 안 고름 — 품셈 12-6 [주]② 「철망재료비는 별도 계상」 · 교본 부록 7-1 3.4 「설계도서에" " 따라」 · 실무 소광은 「철망포함」 관측 — 설계자가 고를 것(기본값 없음)" ) MISSING = "콘크리트 포장 {what} — 칸이 비고 B06 에서도 못 읽어 수량이 안 섬 · 칸에 적을 것" JOINT = "joint_spacing_m" def _stations(designs: list[dict[str, Any]] | None) -> list[tuple[float, dict[str, Any]]]: rows = [ (_num(item.get("chainage_m")), item.get("design")) for item in designs or () if isinstance(item, dict) and isinstance(item.get("design"), dict) ] return sorted(rows, key=lambda row: row[0]) def _widening(design: dict[str, Any]) -> float: return _num(design.get("widening_left_m")) + _num(design.get("widening_right_m")) def _widening_at(stations: list[tuple[float, dict[str, Any]]], at: float) -> float: """그 자리의 확폭 — 앞뒤 측점 사이 직선 보간 · 끝 밖이면 가장 가까운 측점.""" if at <= stations[0][0]: return _widening(stations[0][1]) for (s0, d0), (s1, d1) in zip(stations, stations[1:]): if s0 <= at <= s1: t = 0.0 if s1 == s0 else (at - s0) / (s1 - s0) return _widening(d0) + (_widening(d1) - _widening(d0)) * t return _widening(stations[-1][1]) def pavement_from_designs( structure: dict[str, Any], designs: list[dict[str, Any]] | None ) -> dict[str, Any]: """빈 칸(폭·확폭·두께)을 B06 구간 측점에서 채운 **새** 구조물 — 칸이 차 있으면 그대로.""" from B05_Profile.B05_Profile_Structures_Schema import structure_type_map options = dict(structure.get("options") or {}) notes = list(structure.get("notes") or []) # 상세 칸은 배치 폼에 없어 제안값이 저장에 안 들어감 — 등록부 한 벌에서 채움(값 두 벌 금지). spacing = next(o for o in structure_type_map()["pavement_concrete"].options if o.key == JOINT) if options.get(JOINT) in (None, "") and spacing.default is not None: options[JOINT] = spacing.default notes.append(f"수축줄눈 간격 {spacing.default:g}m — {spacing.default_basis}") stations = _stations(designs) start, end = sorted((_num(structure.get("start_m")), _num(structure.get("end_m")))) inside = [design for chainage, design in stations if start <= chainage <= end] if stations and not inside: inside = [min(stations, key=lambda row: abs(row[0] - (start + end) / 2))[1]] if options.get("width_m") in (None, "") and inside: widths = {round(_num(d.get("carriageway_standard_width_m")), 3) for d in inside} - {0.0} if len(widths) == 1: options["width_m"] = widths.pop() notes.append(f"포장 폭 {options['width_m']:g}m — B06 노폭(구간 측점 {len(inside)}곳)") elif widths: notes.append(f"포장 폭 — 구간 측점의 B06 노폭이 갈림({sorted(widths)}) · 칸에 적을 것") if options.get("thickness_cm") in (None, "") and inside: thick = {round(_num(d.get("pavement_thickness_m")), 4) for d in inside} - {0.0} if len(thick) == 1: options["thickness_cm"] = round(thick.pop() * 100, 2) notes.append(f"포장 두께 {options['thickness_cm']:g}㎝ — B06 포장층 두께") if options.get("widening_area_m2") in (None, "") and stations and end > start: cuts = [start, *[s for s, _d in stations if start < s < end], end] area = sum( (_widening_at(stations, a) + _widening_at(stations, b)) / 2 * (b - a) for a, b in zip(cuts, cuts[1:]) ) options["widening_area_m2"] = round(area, 4) notes.append(f"확폭 면적 {area:.2f}㎡ — B06 곡선부 확폭 × 측점 사이 거리(평균)") return {**structure, "options": options, "notes": notes} def concrete_pavement(length: float, options: dict[str, Any]) -> tuple[list[Component], list[str]]: """콘크리트 포장 한 구간 — (성분, 사유). 면적 밑수가 없으면 성분 없이 사유만.""" width = _num(options.get("width_m")) thickness = _num(options.get("thickness_cm")) / 100 widening = options.get("widening_area_m2") missing = [ what for what, value in ( ("연장", length), ("폭", width), ("두께", thickness), ("확폭 면적", 1.0 if isinstance(widening, (int, float)) else 0.0), ) if value <= 0 ] if missing: return [], [MISSING.format(what="·".join(missing))] spacing = _num(options.get("joint_spacing_m")) widening = float(widening) area = width * length + widening rows: list[tuple[str, str, float, str]] = [ ( "콘크리트", "㎥", area * thickness, f"(폭 {width:g} × 연장 {length:g} + 확폭 {widening:g}) × 두께 {thickness:g}", ), ("포장거푸집", "m", 2 * length, f"양쪽 2 × 연장 {length:g}"), ] notes = [SOURCES, NOTE_FORM] if spacing > 0: joints = math.floor(length / spacing) * width + widening / spacing rows.append( ( "수축줄눈", "m", joints, f"내림({length:g} ÷ {spacing:g}) × 폭 {width:g} + 확폭 {widening:g} ÷ {spacing:g}", ) ) notes.append(NOTE_JOINT) for key, name, unchosen in ( ("separation_sheet", "비닐", SHEET_UNCHOSEN), ("wire_mesh", "철망", MESH_UNCHOSEN), ): choice = options.get(key) if choice == "있음": rows.append((name, "㎡", area, f"포장 면적 {area:.4g}")) elif choice != "없음": notes.append(unchosen) components = [ Component(name, unit, amount, destination, basis) for name, unit, amount, basis in rows if (destination := routed(name, notes)) ] return components, notes def billing(length: float, options: dict[str, Any]) -> tuple[str, float]: """내역 줄 — 포장 면적 ㎡(소광 「콘크리트포장 T=20 ㎡」).""" widening = options.get("widening_area_m2") extra = float(widening) if isinstance(widening, (int, float)) else 0.0 return "㎡", _num(options.get("width_m")) * length + extra