Files
Aislo/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Pavement.py
T
eomsangdonandClaude Opus 5 3cfe3ef58a feat(b05·b08): 콘크리트 포장 수량식 — 폭·확폭·두께 B06 잇기 + 칸 덮어쓰기 · 면적×두께 · 거푸집 2L · 수축줄눈 6m · 비닐·철망 고르기(12장 D 포장 둘 · 브레인 판정 ①~⑤·ⓐ)
- 등록부: 두께 칸 필수 걷고 「비면 B06 포장층 두께」 · 폭·확폭 면적 칸(비면 B06) · 줄눈 간격 제안 6m(교본 부록 4-7 4-6m · 소광) · 비닐·철망 고르기(기본값 없음)
- 연장은 구간(끝 − 시작) — 물넘이로 나뉜 조각에 길이 칸이 복사되는 자리
- 묶음 12-06#두께 · 12-08 · 12-07-01(12-7-2 줄눈설치는 안 씀 — 소광 실무가 컷팅)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-15 03:55:12 +09:00

172 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""콘크리트 포장 전개식 — **면적 × 두께 · 거푸집 양면 · 수축줄눈** (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