- 등록부: 두께 칸 필수 걷고 「비면 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
172 lines
8.3 KiB
Python
172 lines
8.3 KiB
Python
"""콘크리트 포장 전개식 — **면적 × 두께 · 거푸집 양면 · 수축줄눈** (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
|