fix(B08·B09): 석축 · 옹벽 길이 → 구조물 집계표 열 · 설계내역서 곱하기(PLAN 701-1)

- B08 구조물 집계표 — 구조물 표 형상 wall(옹벽 · 돌쌓기 · 큰돌쌓기 · 흙막이) 높이마다 길이 m 열 · 빈 높이 = 「미입력」 열 값 빈칸 · 사본 없으면 안 셈(미연결)
- 응답 types 에 벽 종류 — 화면 조합 열에서 뺌
- B09 설계내역서 — 벽 열 = 2.3 기타구조물 · owners 수량 × 대가
- test_120 종류 집합 낡은 값 갱신 · 시험 test_701_1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 12:31:45 +09:00
co-authored by Claude Opus 5.5
parent 935bd7d04e
commit 80bae255bd
4 changed files with 164 additions and 3 deletions
+33 -1
View File
@@ -17,6 +17,8 @@
관 몫 = 관 공 › 관매설 · 커플링에 더함(B06 새 키 `ford_pipe_length_m` × 련 · 131)
물넘이포장 › 월류 폭 › H={월류 높이} m
노출형 횡단수로 · 개거 · 맹암거(B05 구조물) 개소
석축 · 옹벽(C군 벽 · 구조물 표 형상 wall) › H=높이 m 길이 = 시작~종료(옛 길이 칸) ·
빈 높이 = 「미입력」 열 · 값 빈칸(금액 안 듦 · 701-1)
흙막이 · 돌붙임 열은 관 · 박스 · 세월교 공용(실무 꼴) — 빈 높이: 관 = 관경 최소 ·
박스 = 박스 높이 · 세월교 = 「미입력」(본체 끝 높이는 기하에만)
열 id 접두 `pipe:` = 이 집계표(횡단배수) 이름공간 · 같은 측점 박스 + 관은 종류로 갈라 셈.
@@ -50,6 +52,7 @@ from B06_Section.B06_Section_Repository import (
)
from B08_Quantity.B08_Quantity_Router_Hydro import _project_root
from B08_Quantity.B08_Quantity_Summary_Sheet import _linked, _Sheet, unlinked_facilities
from common_util.common_util_structure_catalog import type_ids_with
from common_util.common_util_drainage_pipes import pipe_points_path_in, read_pipe_points_file
from common_util.common_util_summary_item import GROUP_JOIN
from config.config_db import get_db_pool
@@ -79,6 +82,8 @@ SIDE_NAMES = {"inlet": "유입", "outlet": "유출"}
SIMPLE = ("cross_drain_exposed", "open_ditch", "cross_underdrain")
#: 이 집계표가 펼치는 종류 — 화면은 이 종류를 조합 열에서 뺌(응답 `types`)
DRAIN_TYPES = ("pipe", "box_culvert", "ford_bridge", "ford_pavement", *SIMPLE)
#: 석축 · 옹벽 — 구조물 표 형상 wall(본보기 복사 종류 포함 · 701-1)
WALL_TYPES = tuple(sorted(type_ids_with(shape="wall")))
FORMS = ("돌쌓기(찰)", "돌쌓기(메)", "콘크리트", "돌망태", "통나무·목재틀", "바자")
SHAPES = ("I", "L", "U")
BONDS = ("찰붙임", "메붙임")
@@ -460,6 +465,26 @@ def _drain(
_chutes(sheet, at, options, links, pipe_names)
def _wall(sheet: _Sheet, item: dict[str, Any], combo: str | None, kinds: dict[str, Any]) -> None:
"""석축 · 옹벽 한 곳 — 높이마다 길이 m · 빈 높이 = 「미입력」 열 · 값 빈칸(701-1)."""
type_id = item["type_id"]
sheet.owner, sheet.kind = item["복사본"], type_id
title = next(
(t["name"] for t in kinds.get("types") or [] if t.get("type_id") == type_id), type_id
)
height = _num(item["options"].get("height_m"))
text = fill._label("height_m", height)
sheet.add(
float(item["chainage_m"]),
f"wall|{type_id}|{text}",
(7, _order(WALL_TYPES, type_id), math.inf if height is None else height),
[title, fill.EMPTY_TEXT if height is None else f"H={text}"],
"m",
fill._length(item) if height is not None else None,
[combo],
)
def pipe_summary(
project_root: str | Path,
designs: list[dict[str, Any]],
@@ -492,11 +517,14 @@ def pipe_summary(
by_owner: dict[tuple[str, str], dict[str, Any]] = {}
# 집계표 밖 구조물 — 사본 하나라도 있나(조합 열은 화면이 그림)
others: dict[tuple[str, str], bool] = {}
walls: list[dict[str, Any]] = []
for item in items:
owner, _, link = item["복사본"].partition(GROUP_JOIN)
if item["type_id"] not in DRAIN_TYPES:
key = (item["type_id"], owner)
others[key] = others.get(key, False) or item["복사본"] in linked
if item["type_id"] in WALL_TYPES and not link:
walls.append(item) # 석축 · 옹벽 본체 = 길이 열(701-1)
continue
entry = by_owner.setdefault((item["type_id"], owner), {"links": {}})
# 거름 한 곳 — 사본 없는 잇기는 조합 없음으로(그 칸만 안 셈 · 143-1)
@@ -596,6 +624,10 @@ def pipe_summary(
_revets(sheet, at, wall_options, links, sources, wall, names, wall_in)
_chutes(sheet, at, options, links, names)
for item in walls:
linked_combo = _combo(item.get("summary_item")) if item["복사본"] in linked else None
_wall(sheet, item, linked_combo, kinds)
out = sheet.result()
notes = []
missing = sheet.unlinked | {key for key, found in others.items() if not found}
@@ -618,7 +650,7 @@ def pipe_summary(
**out,
"notes": notes,
"one_length_m": one_m,
"types": list(DRAIN_TYPES),
"types": [*DRAIN_TYPES, *WALL_TYPES],
"pipes": pipes,
"linked": linked,
# 154-1 · 줄 누름 → 바로 연결(154-2)
+2 -2
View File
@@ -4,7 +4,7 @@
토공(1.1~1.6) = B08 토공집계표 줄 짝(`EARTH` — 엑셀 항목 이름 ↔ 토공집계표 구분 · 종류 · 규격) ·
단가 = 조합 밖 토공 산근 · 준비공 대가 — 원천 없음 → 빈칸 「연결 없음」(지시)
구조물(2.1~2.3) = B08 구조물 집계표 열(사본 있을 때만 · 143) ·
구조물군 → 공종 짝(`STRUCTURE_SECTION`)
구조물군 → 공종 짝(`STRUCTURE_SECTION` · 석축 · 옹벽 = 2.3 · 701-1)
· 커플링 열은 자재(관급자재대)라 뺌 · 단가 = 대가 호표(145-2 가 `hopyo` 로 줌 · 그전엔 빈칸)
운반(3) = 엑셀 운반공 줄 · 수량 = B08 총괄자재 짝 · 거리 · 단가 = 운반 산근(원천 없음 → 빈칸)
자재대 · 관급자재대 = 화면 짝이 대비표 「별도」 · 「관급」 표시로 짬(사용자 조작 즉시)
@@ -45,7 +45,7 @@ EARTH: list[tuple[str, str, str, tuple[str, str, str | None] | None]] = [
]
#: 구조물 집계표 열 id 머리 → 공종(그 밖 = 2.2 배수공) · 커플링 = 자재라 뺌
STRUCTURE_SECTION = {"pipe:revet|": "2.1", "pipe:chute|": "2.3"}
STRUCTURE_SECTION = {"pipe:revet|": "2.1", "pipe:chute|": "2.3", "pipe:wall|": "2.3"}
SKIP_COLUMNS = ("pipe:cp|",)
@@ -245,6 +245,7 @@ def test_차례_계_종류(out):
"cross_drain_exposed",
"open_ditch",
"cross_underdrain",
*summary.WALL_TYPES, # 석축 · 옹벽 길이 열(701-1)
}
@@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
"""701-1 — 석축 · 옹벽 길이 → B08 구조물 집계표 열 · B09 설계내역서 구조물 줄(owners 수량).
견본: 옹벽 H=2.0 구간 100~112(12 m) · 돌쌓기(찰) H=2.0 구간 196~204(옛 길이 칸 99 안 씀) ·
돌쌓기(찰) 높이 빈칸(null · 금액 안 듦 = 값 빈칸) · 큰돌쌓기 사본 없음(안 셈 · 미연결)
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from B08_Quantity import B08_Quantity_Router_Summary as summary
from B09_Estimation.B09_Estimation_Bill_Items import structure_items
from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition
def _item(key: str) -> dict:
return {"layer": "system", "owner": None, "key": key, "name": key}
STRUCTURES = [
{
"structure_id": "s-wall",
"type_id": "retaining_wall",
"placement": "interval",
"chainage_m": 106,
"start_m": 100,
"end_m": 112,
"options": {"height_m": 2},
"summary_item": _item("SS000201"),
},
{
"structure_id": "s-wet",
"type_id": "masonry_wet",
"placement": "interval",
"chainage_m": 200,
"start_m": 196,
"end_m": 204,
"options": {"height_m": 2.0, "length_m": 99},
"summary_item": _item("SS000202"),
},
{
"structure_id": "s-wet-blank",
"type_id": "masonry_wet",
"placement": "interval",
"chainage_m": 300,
"start_m": 295,
"end_m": 305,
"options": {"height_m": None},
"summary_item": _item("SS000202"),
},
{
"structure_id": "s-boulder",
"type_id": "boulder_masonry",
"placement": "interval",
"chainage_m": 400,
"start_m": 395,
"end_m": 405,
"options": {"height_m": 3},
"summary_item": _item("SS000203"),
},
]
@pytest.fixture
def out(tmp_path: Path) -> dict:
root = tmp_path / "proj"
route = root / "B05_Profile/route"
route.mkdir(parents=True)
(route / "structures.json").write_text(
json.dumps({"revision": 1, "structures": STRUCTURES}, ensure_ascii=False),
encoding="utf-8",
)
names = definition.read()["종류"]["pipe"]["이름"]
linked = {"s-wall": {}, "s-wet": {}, "s-wet-blank": {}} # 큰돌쌓기 = 사본 없음
return summary.pipe_summary(root, [], names, 6.0, linked=linked)
def _col(out: dict, cid: str) -> dict:
return next(c for c in out["columns"] if c["id"] == cid)
def test_옹벽_석축_높이마다_길이_열(out):
wall = _col(out, "pipe:wall|retaining_wall|2.0")
assert wall["group"] == ["옹벽", "H=2.0"] and wall["label"] == "m"
assert wall["combos"] == ["system||SS000201"]
assert wall["owners"] == {"s-wall": 12.0} # 구간 시작~종료
wet = _col(out, "pipe:wall|masonry_wet|2.0")
assert wet["owners"] == {"s-wet": 8.0} # 구간이 옛 길이 칸을 이김
assert out["total"]["pipe:wall|retaining_wall|2.0"] == 12.0
def test_빈_높이_미입력_열_값_빈칸(out):
blank = _col(out, "pipe:wall|masonry_wet|-")
assert blank["group"] == ["돌쌓기(찰)", "미입력"]
assert blank["owners"] == {} and "pipe:wall|masonry_wet|-" not in out["total"]
def test_사본_없는_벽_안_셈_미연결(out):
assert not any("boulder_masonry" in c["id"] for c in out["columns"])
assert [f["id"] for f in out["unlinked"]] == ["s-boulder"]
assert {"retaining_wall", "masonry_wet", "boulder_masonry"} <= set(out["types"])
def test_B09_구조물_줄_기타구조물_수량(out):
rows = {r["key"]: r for r in structure_items(out)}
wall = rows["st:pipe:wall|retaining_wall|2.0"]
assert (wall["section"], wall["name"], wall["spec"], wall["unit"]) == (
"2.3",
"옹벽",
"H=2.0",
"m",
)
assert wall["qty"] == 12.0 and wall["owners"] == {"s-wall": 12.0}
assert rows["st:pipe:wall|masonry_wet|-"]["qty"] is None
def test_B09_금액_길이_곱하기_대가(out):
from B09_Estimation.B09_Estimation_Calc_Bill import compute_estimation
price = {"no": "대가1", "key": "blk:w", "labor": 96868, "material": 11309, "expense": 16417}
inputs = {"bill": {"items": structure_items(out)}}
sheets = compute_estimation(inputs, {}, {"s-wall": price}.get)
row = next(r for r in sheets["bill"]["rows"] if r["key"].endswith("#blk:w"))
assert (row["qty"], row["sum"], row["note"]) == (12.0, 1495128, "대가1 · system||SS000201")