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
This commit is contained in:
@@ -2532,8 +2532,58 @@
|
||||
"input": "number",
|
||||
"unit": "cm",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 B06 포장층 두께(표준 횡단면 설정 · 기본 0.2m = 임도기술교본 3-2 「두께 0.2m」)를 이음 — 적으면 적은 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "width_m",
|
||||
"label": "포장 폭",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 B06 노폭(구간 측점의 차도 표준 폭 · 교본 3-2 「포장 폭 3.0m」 · 실무 관측 3.5~4.0)을 이음 — 적으면 적은 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "widening_area_m2",
|
||||
"label": "확폭 면적",
|
||||
"input": "number",
|
||||
"unit": "㎡",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 B06 곡선부 확폭을 측점 사이 평균 × 거리로 더함(교본 3-2 「곡선부에는 확폭」) — 확폭이 없으면 0 을 적을 것"
|
||||
},
|
||||
{
|
||||
"key": "joint_spacing_m",
|
||||
"label": "수축줄눈 간격",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 6,
|
||||
"default_basis": "제안값 · 임도기술교본 부록 4-7 7-1 3.5.1 「수축줄눈의 간격은 4-6m를 기준」 · 실무 소광 6m 관측 · 바꿀 수 있음",
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "separation_sheet",
|
||||
"label": "비닐(깔기·양생)",
|
||||
"input": "select",
|
||||
"choices": ["있음", "없음"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 비닐 재료가 안 서고 사유가 뜸 — 품셈 12-6 [주]② 「양생 재료비(비닐 등)는 별도 계상」이라 설계자가 고름(기본값 없음)"
|
||||
},
|
||||
{
|
||||
"key": "wire_mesh",
|
||||
"label": "철망",
|
||||
"input": "select",
|
||||
"choices": ["있음", "없음"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 철망 재료가 안 서고 사유가 뜸 — 품셈 12-6 [주]② 「철망재료비는 별도 계상」 · 교본 부록 7-1 3.4 「설계도서에 따라」 · 실무 소광 「철망포함」 관측"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -419,6 +419,18 @@ def composite_quantities(
|
||||
entry["not_ready"] = True
|
||||
entry["why"] = why
|
||||
missing.append({"code": spec.get("code"), "reason": why})
|
||||
if suffix == "pavement_thickness":
|
||||
# 12-6 갈래는 포장두께 20·30·40㎝ — 원단위가 쓴 두께(칸 또는 B06) 그대로(2026-09-15).
|
||||
thickness = (structure.get("options") or {}).get("thickness_cm")
|
||||
label = f"{thickness:g}㎝" if isinstance(thickness, (int, float)) else ""
|
||||
if label in ("20㎝", "30㎝", "40㎝"):
|
||||
entry["kind"] = label
|
||||
entry["code"] = f"{spec.get('code')}#{label}"
|
||||
elif found:
|
||||
why = f"12-6 표는 포장두께 20·30·40㎝ 갈래뿐 — 두께 {label or '없음'} 은 못 고름"
|
||||
entry["not_ready"] = True
|
||||
entry["why"] = why
|
||||
missing.append({"code": spec.get("code"), "reason": why})
|
||||
if suffix == "rebar_complexity":
|
||||
# 갈래는 원문이 정한다 — 화면·인계에 이름과 근거를 함께 실어 사람이 검증하게 한다.
|
||||
complexity, why = rebar_complexity(
|
||||
|
||||
@@ -118,6 +118,8 @@ EXPANDERS = {
|
||||
"bed_sill": lambda h, l, o, f=None, r="": _bed_sill()(_num(o.get("area_m2"), 0.0), o),
|
||||
# BOX암거 — m당 × 연장(산림과임업기술 예제 · 울진 2×2 · 2026-09-15 브레인 12장 D).
|
||||
"box_culvert": lambda h, l, o, f=None, r="": _box_culvert()(o),
|
||||
# 콘크리트 포장 — 면적 × 두께 · 거푸집 2L · 수축줄눈(교본 3-2 · 부록 4-7 · 소광 · 2026-09-15).
|
||||
"pavement_concrete": lambda h, l, o, f=None, r="": _pavement().concrete_pavement(l, o),
|
||||
# ⚠⚠ **큰돌쌓기(`boulder_masonry`)를 여기에 두지 않는다** (2026-09-07 발견).
|
||||
# 큰돌쌓기는 품셈 **13-6** 이고 돌쌓기는 **13-4** 다 — **규격 축이 다르다.**
|
||||
# 돌쌓기는 **뒷길이**(35·45·55·60㎝), 큰돌쌓기는 **직경**(40~60·60~80·80~100㎝).
|
||||
@@ -135,6 +137,13 @@ def _box_culvert():
|
||||
return box_culvert
|
||||
|
||||
|
||||
def _pavement():
|
||||
"""콘크리트 포장 모듈을 늦게 가져온다."""
|
||||
from B08_Quantity import B08_Quantity_Engine_UnitQuantity_Pavement
|
||||
|
||||
return B08_Quantity_Engine_UnitQuantity_Pavement
|
||||
|
||||
|
||||
def _soil_guard():
|
||||
"""흙막이 사유 함수를 늦게 가져온다."""
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Revetment import soil_guard
|
||||
@@ -183,12 +192,17 @@ EXPANDER_WITHHELD: dict[str, str] = {
|
||||
#: ⚠ 안 적으면 「m · 연장」으로 읽혀 — 골막이·바닥막이(점 배치라 연장 0)는 **수량 0 이라 내역에 안 서고**
|
||||
#: 구조물도가 전 줄 「단위당 못 냄」, 떼흙막이는 개소당 값이 연장으로 나뉘어 **떼 0.139/m** 로 보였음.
|
||||
EXPANDER_BILLING: dict[str, Any] = {
|
||||
"erosion_check": lambda options: ("개소", 1.0), # 정본 「골막이(찰)(치수조서연결)」 개소당
|
||||
"soil_guard": lambda options: ("개소", 1.0), # 정본 「떼흙막이」 개소당(평균 붙박이)
|
||||
"bed_sill": lambda options: ("㎡", _num(options.get("area_m2"), 0.0)), # 정본 돌붙임 ㎡당
|
||||
"box_culvert": lambda options: ("m", _num(options.get("length_m"), 0.0)), # 점 배치라 연장 칸
|
||||
"erosion_check": lambda options, length: ("개소", 1.0), # 정본 「골막이(찰)」 개소당
|
||||
"soil_guard": lambda options, length: ("개소", 1.0), # 정본 「떼흙막이」 개소당(평균 붙박이)
|
||||
"bed_sill": lambda options, length: ("㎡", _num(options.get("area_m2"), 0.0)), # 돌붙임 ㎡당
|
||||
"box_culvert": lambda options, length: ("m", _num(options.get("length_m"), 0.0)), # 점 배치
|
||||
"pavement_concrete": lambda options, length: _pavement().billing(length, options), # 면적 ㎡
|
||||
}
|
||||
|
||||
#: 연장을 **구간(끝 − 시작)** 에서 읽는 종류 — 포장은 물넘이로 나뉘면 길이 칸(10)이 조각마다
|
||||
#: 그대로 복사돼 칸 값이 구간과 안 맞음(`B05_Profile_UI_Structures_Panel_Commit` 나누기).
|
||||
SPAN_LENGTH_TYPES = frozenset({"pavement_concrete"})
|
||||
|
||||
|
||||
#: 한 구조물이 **여러 내역 줄**을 낳는 자리. 배수관은 관 자체와 유입부 집수정이 따로 선다
|
||||
#: (품셈도 관부설과 집수정을 다른 공종으로 둔다). 한 줄로 합치면 어느 쪽 물량인지 못 가른다.
|
||||
@@ -288,6 +302,11 @@ def expand(
|
||||
start = _num(structure.get("start_m"))
|
||||
end = _num(structure.get("end_m"))
|
||||
length = _num(options.get("length_m")) or abs(end - start)
|
||||
if type_id in SPAN_LENGTH_TYPES and None not in (
|
||||
structure.get("start_m"),
|
||||
structure.get("end_m"),
|
||||
):
|
||||
length = abs(end - start)
|
||||
height = _num(options.get("height_m"))
|
||||
# ⚠ 레지스트리 이름이 없으면 **코드값(`retaining_wall`)이 그대로 내역에 뜬다** —
|
||||
# `_Wording.type_label` 이 이미 대비표를 들고 있으므로 그것을 쓴다(2026-09-09 감사).
|
||||
@@ -340,7 +359,7 @@ def expand(
|
||||
result.notes.extend(notes)
|
||||
billing = EXPANDER_BILLING.get(type_id)
|
||||
if billing is not None and result.components:
|
||||
result.billing_unit, result.billing_quantity = billing(options)
|
||||
result.billing_unit, result.billing_quantity = billing(options, length)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -62,6 +62,11 @@ DESTINATION = {
|
||||
"합판거푸집": "unit_price",
|
||||
"동바리": "unit_price",
|
||||
"비계": "unit_price",
|
||||
# ⭐ 2026-09-15 콘크리트 포장 — 거푸집(12-8)·수축줄눈(12-7-1 절단)은 품 · 비닐·철망은 12-6 [주]② 재료 별도.
|
||||
"포장거푸집": "unit_price",
|
||||
"수축줄눈": "unit_price",
|
||||
"비닐": "material",
|
||||
"철망": "material",
|
||||
}
|
||||
|
||||
#: ⛔ **갈 곳 기본값 금지**(명세 13장 「빠진 것이 조용해짐」) — 표에 없는 이름은 막고 사유를 냄.
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
"""콘크리트 포장 전개식 — **면적 × 두께 · 거푸집 양면 · 수축줄눈** (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
|
||||
@@ -97,6 +97,11 @@ def _collect_structures(
|
||||
if definition.reference_only:
|
||||
skipped.append(f"{definition.name}: 전문 상세설계 대상 — 배치까지만")
|
||||
continue
|
||||
if type_id == "pavement_concrete":
|
||||
# 폭·확폭·두께는 B06 이 앎 — 빈 칸만 이음(2026-09-15 브레인 포장 둘 ①④).
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Pavement import pavement_from_designs
|
||||
|
||||
payload = pavement_from_designs(payload, designs)
|
||||
if definition.group == "B":
|
||||
# ⚠ B군(종단배수)은 **연장표**로 간다 — `common_util_structure_lengths` 가
|
||||
# 겹친 구간을 합쳐 주기 때문이다. 구조물별로 세면 겹친 구간을 두 번 센다.
|
||||
|
||||
@@ -492,6 +492,33 @@
|
||||
],
|
||||
"why": "품셈 12장에 「BOX암거」 공종이 없음 — 산림과임업기술(임도) 5장 수량산출 예제가 구체콘크리트·기초·거푸집·동바리·철근으로 가르고, 실무 울진 기번3 「암거수량집계표(2×2)」가 레미콘·버림·유로폼·합판·철근·동바리·비계로 셈(2026-09-15 브레인 12장 D).",
|
||||
"needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김"
|
||||
},
|
||||
{
|
||||
"type_id": "pavement_concrete",
|
||||
"parts": [
|
||||
{
|
||||
"code": "FP-12-06",
|
||||
"name": "콘크리트 포장(인력시공)",
|
||||
"unit": "㎥",
|
||||
"from_components": ["콘크리트"],
|
||||
"kind_suffix": "pavement_thickness"
|
||||
},
|
||||
{
|
||||
"code": "FP-12-08",
|
||||
"name": "콘크리트 포장 거푸집",
|
||||
"unit": "m",
|
||||
"from_components": ["포장거푸집"]
|
||||
},
|
||||
{
|
||||
"code": "FP-12-07-01",
|
||||
"name": "포장절단",
|
||||
"unit": "m",
|
||||
"from_components": ["수축줄눈"],
|
||||
"why": "수축줄눈은 12-7-1 포장절단으로 이음 — 12-7-2 줄눈설치는 안 씀 — 소광 실무가 컷팅(「수축줄눈(컷팅)」 · 2026-09-15 브레인 ⓐ)"
|
||||
}
|
||||
],
|
||||
"why": "실무 내역은 「콘크리트포장 T=20 ㎡」 한 줄이고 품셈은 포설(12-6 ㎥ · 비닐·철망깔기·양생 포함) · 거푸집(12-8 연장 m) · 줄눈(12-7) 을 따로 둠 — 거푸집·줄눈 제외라 묶음으로 셈(2026-09-15 브레인 12장 D 포장 둘). 콘크리트·비닐·철망 재료는 자재 축.",
|
||||
"needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김"
|
||||
}
|
||||
],
|
||||
"unit_conversion": {
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
"""콘크리트 포장 전개식 — 면적 × 두께 · 거푸집 양면 · 수축줄눈 (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 콘크리트 포장 거푸집
|
||||
실무 울진소광 「포장및난간개거」 — 면적 = B × L · 거푸집(1면기준) = 2L ·
|
||||
수축줄눈 = 내림(L÷6) × B (103m·B 3.5 → 59.5 · 418m → 241.5) · 「포장확폭」 줄눈 = 면적 ÷ 6
|
||||
판정 — 폭·확폭·두께는 **B06 에서 잇고 칸이 이김**(한 값 원칙) · 비닐·철망은 고르기(기본값 없음).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
|
||||
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Pavement import pavement_from_designs
|
||||
|
||||
FILLED = {
|
||||
"width_m": 3.0,
|
||||
"widening_area_m2": 6.0,
|
||||
"thickness_cm": 20,
|
||||
"joint_spacing_m": 6,
|
||||
"separation_sheet": "있음",
|
||||
"wire_mesh": "없음",
|
||||
}
|
||||
|
||||
|
||||
def _row(options: dict, start: float = 100.0, end: float = 130.0) -> dict:
|
||||
structure = {
|
||||
"structure_id": "p1",
|
||||
"type_id": "pavement_concrete",
|
||||
"start_m": start,
|
||||
"end_m": end,
|
||||
# 구간을 물넘이로 나누면 길이 칸(10)이 조각마다 그대로 복사됨 — 연장은 구간에서 읽음.
|
||||
"options": {"length_m": 10, **options},
|
||||
}
|
||||
return build_table([structure], {"pavement_concrete": "콘크리트 포장"})["structures"][0]
|
||||
|
||||
|
||||
def test_칸이_찬_구간은_면적_두께_거푸집_줄눈이_선다() -> None:
|
||||
row = _row(FILLED)
|
||||
got = {c["name"]: c for c in row["components"]}
|
||||
area = 3.0 * 30 + 6.0
|
||||
assert (row["billing_unit"], row["billing_quantity"]) == ("㎡", pytest.approx(area))
|
||||
assert got["콘크리트"]["amount"] == pytest.approx(area * 0.2)
|
||||
assert got["콘크리트"]["destination"] == "material"
|
||||
assert got["포장거푸집"]["amount"] == pytest.approx(2 * 30)
|
||||
# 내림(30 ÷ 6) × 3.0 + 확폭 6 ÷ 6
|
||||
assert got["수축줄눈"]["amount"] == pytest.approx(5 * 3.0 + 1.0)
|
||||
assert got["비닐"]["amount"] == pytest.approx(area) and "철망" not in got
|
||||
text = " ".join(row["notes"])
|
||||
assert "12-7-2 줄눈설치는 안 씀 — 소광 실무가 컷팅" in text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("length", "joints"), [(103.0, 59.5), (418.0, 241.5)])
|
||||
def test_수축줄눈은_소광_관측과_같다(length: float, joints: float) -> None:
|
||||
row = _row({**FILLED, "width_m": 3.5, "widening_area_m2": 0}, 0.0, length)
|
||||
got = {c["name"]: c["amount"] for c in row["components"]}
|
||||
assert got["수축줄눈"] == pytest.approx(joints)
|
||||
|
||||
|
||||
def test_비닐_철망을_안_고르면_사유() -> None:
|
||||
row = _row({k: v for k, v in FILLED.items() if k not in ("separation_sheet", "wire_mesh")})
|
||||
names = {c["name"] for c in row["components"]}
|
||||
assert "비닐" not in names and "철망" not in names and "콘크리트" in names
|
||||
text = " ".join(row["notes"])
|
||||
assert "비닐" in text and "철망" in text and "12-6" in text
|
||||
|
||||
|
||||
DESIGNS = [
|
||||
{"chainage_m": 90.0, "design": {"carriageway_standard_width_m": 3.0, "paved": False}},
|
||||
{
|
||||
"chainage_m": 100.0,
|
||||
"design": {
|
||||
"carriageway_standard_width_m": 3.0,
|
||||
"widening_right_m": 1.0,
|
||||
"paved": True,
|
||||
"pavement_thickness_m": 0.2,
|
||||
},
|
||||
},
|
||||
{
|
||||
"chainage_m": 120.0,
|
||||
"design": {
|
||||
"carriageway_standard_width_m": 3.0,
|
||||
"widening_left_m": 0.5,
|
||||
"paved": True,
|
||||
"pavement_thickness_m": 0.2,
|
||||
},
|
||||
},
|
||||
{
|
||||
"chainage_m": 140.0,
|
||||
"design": {"carriageway_standard_width_m": 3.0, "paved": True, "pavement_thickness_m": 0.2},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _structure(**options) -> dict:
|
||||
return {
|
||||
"type_id": "pavement_concrete",
|
||||
"start_m": 100.0,
|
||||
"end_m": 130.0,
|
||||
"options": {"length_m": 10, **options},
|
||||
}
|
||||
|
||||
|
||||
def test_빈_칸은_B06_폭_확폭_두께를_잇는다() -> None:
|
||||
filled = pavement_from_designs(_structure(), DESIGNS)
|
||||
options = filled["options"]
|
||||
assert options["width_m"] == pytest.approx(3.0)
|
||||
assert options["thickness_cm"] == pytest.approx(20.0)
|
||||
# 100 → 1.0 · 120 → 0.5 · 130 → 0.25(120~140 사이 보간): (1.0+0.5)/2×20 + (0.5+0.25)/2×10
|
||||
assert options["widening_area_m2"] == pytest.approx(15.0 + 3.75)
|
||||
assert any("B06" in note for note in filled["notes"])
|
||||
# 상세 칸은 배치 폼에 없어 저장에 없음 — 등록부 제안값 6m 을 사유와 함께 채움.
|
||||
assert options["joint_spacing_m"] == 6 and any("4-6m" in note for note in filled["notes"])
|
||||
|
||||
|
||||
def test_칸을_적으면_칸이_이긴다() -> None:
|
||||
filled = pavement_from_designs(_structure(width_m=3.5, widening_area_m2=0), DESIGNS)
|
||||
assert filled["options"]["width_m"] == 3.5 and filled["options"]["widening_area_m2"] == 0
|
||||
assert filled["options"]["thickness_cm"] == pytest.approx(20.0)
|
||||
|
||||
|
||||
def test_B06_설계도_칸도_없으면_안_선다() -> None:
|
||||
filled = pavement_from_designs(_structure(), [])
|
||||
row = build_table([filled])["structures"][0]
|
||||
assert row["components"] == []
|
||||
assert any("폭" in note for note in row["notes"])
|
||||
|
||||
|
||||
def test_인계_묶음은_12_6_두께_갈래_12_8_12_7_1() -> None:
|
||||
table = build_table(
|
||||
[{**_structure(**FILLED), "structure_id": "p1"}], {"pavement_concrete": "콘크리트 포장"}
|
||||
)
|
||||
row = next(
|
||||
r for r in build_handoff(unit_quantity_table=table)["work_items"] if "포장" in r["name"]
|
||||
)
|
||||
parts = {part["code"]: part for part in row["composite_parts"] or []}
|
||||
area = 3.0 * 30 + 6.0
|
||||
assert parts["FP-12-06#20㎝"]["quantity"] == pytest.approx(area * 0.2)
|
||||
assert parts["FP-12-08"]["quantity"] == pytest.approx(60.0)
|
||||
assert parts["FP-12-07-01"]["quantity"] == pytest.approx(16.0)
|
||||
|
||||
|
||||
def test_등록부_칸() -> None:
|
||||
options = {o.key: o for o in structure_type_map()["pavement_concrete"].options}
|
||||
assert not options["thickness_cm"].required and "B06" in options["thickness_cm"].empty_means
|
||||
assert "B06" in options["width_m"].empty_means and "3.5" in options["width_m"].empty_means
|
||||
assert "B06" in options["widening_area_m2"].empty_means
|
||||
assert (
|
||||
options["joint_spacing_m"].default == 6
|
||||
and "4-6m" in options["joint_spacing_m"].default_basis
|
||||
)
|
||||
for key in ("separation_sheet", "wire_mesh"):
|
||||
assert options[key].default is None and options[key].choices == ["있음", "없음"]
|
||||
Reference in New Issue
Block a user