From dbc9a32b7263e8845848bea62c36fefeedea7af3 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 9 Oct 2026 15:20:56 +0900 Subject: [PATCH] =?UTF-8?q?feat(B08):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=A7=91=EA=B3=84=ED=91=9C=20=EC=97=B4=EB=A7=88=EB=8B=A4=20?= =?UTF-8?q?=EC=82=AC=EB=B3=B8=20=EC=A3=BC=EC=9D=B8=EB=B3=84=20=EA=B0=92=20?= =?UTF-8?q?owners(145-3=20=EC=9D=B4=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 열 owners = {사본 주인 id: 그 주인이 낸 값} — 주인 id = linked_snapshots 키(pipe@측점 · 구조물 id · {id}__{그룹}) - 칸을 낸 잇기 사본이 먼저 · 없으면 시설 본체 · owners 합 = 열 계 · 기존 키 · 화면 그대로 - 새 test_145_3 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz --- B08_Quantity/B08_Quantity_Router_Summary.py | 39 ++++++++++++++-- .../tester/test_145_3_b08_summary_owners.py | 46 +++++++++++++++++++ 2 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 resources/tester/test_145_3_b08_summary_owners.py diff --git a/B08_Quantity/B08_Quantity_Router_Summary.py b/B08_Quantity/B08_Quantity_Router_Summary.py index 4e840da35..416c2c161 100644 --- a/B08_Quantity/B08_Quantity_Router_Summary.py +++ b/B08_Quantity/B08_Quantity_Router_Summary.py @@ -22,6 +22,9 @@ 열 id 접두 `pipe:` = 이 집계표(횡단배수) 이름공간 · 같은 측점 박스 + 관은 종류로 갈라 셈. 머리 글 = M02 정의 이름(`구조물정의.json` types.name · 종류.{종류}.이름 — 사용자가 M02 에서 고침) · 열마다 `combos` = 그 값을 낸 컨테이너에 이은 조합 id(`{층}|{주인}|{키}` · 95-3 모달 · 맨 윗 머리). +열마다 `owners` = {사본 주인 id: 그 주인이 낸 값}(145-3) — 주인 id = `linked_snapshots` 키 +(`pipe@측점` · 구조물 id · `{id}__{그룹}`) · 칸을 낸 잇기 사본이 먼저 · 없으면 시설 본체 · +합 = 열 계. 설계 = B08 토공과 같은 길(확정 노선 먼저) · 관 · 컨테이너 줄 = M02 채움 `collect_items` 재사용. `pipes` = 관마다 길이(커플링 열 · 측점) — 좌측 패널 1본 길이를 바꾸면 화면이 같은 식으로 바로 다시 셈 (짝 = `couplings` ↔ `B08_Quantity_Options_Calc.ts` `couplings` · `recountCouplings` · PLAN 122). @@ -111,6 +114,21 @@ def _combo(item: Any) -> str | None: return f"{item.get('layer')}|{item.get('owner') or ''}|{item['key']}" +class _Copy(str): + """잇기 조합 id + 그 사본 주인 id(`linked_snapshots` 키 · `{구조물 id}__{그룹}`) — 145-3.""" + + copy: str + + +def _linked(combo: str | None, copy: str) -> str | None: + """잇기 조합에 사본 주인 id 를 붙임 — 본체 조합(그냥 글)은 시설 id 가 주인.""" + if combo is None: + return None + out = _Copy(combo) + out.copy = copy + return out + + def one_pipe_length(project_root: str | Path) -> float | None: """1본 길이(m) — 프로젝트 층 구조물집계표 변수만(시스템 층으로 안 채움 · 143-2) · 빈칸 None.""" found = layers.read_template(layers.project_dir(project_root), "table", TABLE_NAME) @@ -193,14 +211,19 @@ class _Sheet: return cid = f"pipe:{key}" column = self.columns.setdefault( - cid, {"id": cid, "group": group, "label": label, "combos": [], "_sort": sort} + cid, + {"id": cid, "group": group, "label": label, "combos": [], "owners": {}, "_sort": sort}, ) for combo in combos: if combo and combo not in column["combos"]: - column["combos"].append(combo) + column["combos"].append(str(combo)) row = self.rows.setdefault(round(chainage * 100), {"chainage_m": chainage, "values": {}}) if value is not None: # 열은 세움 — 놓였는데 값이 빈칸인 것이 보여야 함 row["values"][cid] = round((row["values"].get(cid) or 0.0) + value, 3) + # 사본 주인별 값(145-3 · 설계내역서가 대가 호표에 이음) — 잇기 사본이 먼저 · 없으면 본체 + owner = next((c.copy for c in combos if isinstance(c, _Copy)), self.owner) + owners = column["owners"] + owners[owner] = round(owners.get(owner, 0.0) + value, 3) def result(self) -> dict[str, Any]: columns = sorted(self.columns.values(), key=lambda column: column["_sort"]) @@ -472,7 +495,11 @@ def _drain( return # 필수 컨테이너 잇기 — 정의 그룹에 없는 것(세월교)은 저장된 잇기에서 required = {"box_culvert": BOX_LINKS, "ford_bridge": FORD_LINKS}.get(type_id, PAVE_LINKS) - combos = {part: links.get(link) or _combo(group.get(link)) for part, link in required.items()} + combos = { + part: links.get(link) + or _linked(_combo(group.get(link)), f"{sheet.owner}{GROUP_JOIN}{link}") + for part, link in required.items() + } if type_id == "ford_pavement": _pavement(sheet, at, base, combos, names) return @@ -527,7 +554,7 @@ def pipe_summary( # 거름 한 곳 — 사본 없는 잇기는 조합 없음으로(그 칸만 안 셈 · 143-1) combo = _combo(item.get("summary_item")) if item["복사본"] in linked else None if link: - entry["links"][link] = combo + entry["links"][link] = _linked(combo, item["복사본"]) else: entry["base"] = {**item, "summary_item": item.get("summary_item") if combo else None} @@ -552,7 +579,9 @@ def pipe_summary( } _drain(sheet, at, type_id, base, options, links, group, kinds, names, box_length) body_link = BOX_LINKS["body"] if type_id == "box_culvert" else FORD_LINKS["body"] - body = links.get(body_link) or _combo(group.get(body_link)) + body = links.get(body_link) or _linked( + _combo(group.get(body_link)), f"{owner}{GROUP_JOIN}{body_link}" + ) counted = bool(base["summary_item"] or body) # 본체 칸을 셈(사본 있음) no_box += type_id == "box_culvert" and counted and not box_length if type_id == "ford_bridge": diff --git a/resources/tester/test_145_3_b08_summary_owners.py b/resources/tester/test_145_3_b08_summary_owners.py new file mode 100644 index 000000000..6e09df059 --- /dev/null +++ b/resources/tester/test_145_3_b08_summary_owners.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +"""145-3 이음 — B08 구조물 집계표 열마다 `owners` = {사본 주인 id: 그 주인이 낸 값}. + +주인 id = `linked_snapshots` 키(`pipe@측점` · 구조물 id · `{id}__{그룹}`) · 열 owners 합 = 열 계 · +칸을 낸 잇기 사본이 먼저(없으면 시설 본체). +""" + +from __future__ import annotations + +import pytest +from test_120_b08_drain_summary import out # noqa: F401 — 픽스처(관 밖 횡단배수 견본) +from test_143_1_b08_linked_snapshots import _run as _run_143 +from test_143_1_b08_linked_snapshots import root # noqa: F401 — 픽스처(실제 사본 폴더) + +from M02_MasterTemplete import M02_Item_Snapshot as snapshots + + +def _check_sums(result: dict, keys: set[str]) -> None: + for column in result["columns"]: + owners = column["owners"] + assert set(owners) <= keys, column["id"] + assert sum(owners.values()) == pytest.approx(result["total"].get(column["id"], 0.0)) + + +def test_owners_합_열_계_주인은_사본_키(out): # noqa: F811 + cols = {c["id"]: c for c in out["columns"]} + _check_sums(out, set(out["linked"])) + # 본체 칸 = 본체 잇기 사본(gboxbody) · 날개벽 = 유입 잇기 · 흙막이 = 유출 잇기 + assert cols["pipe:box|body|2.0×2.0"]["owners"] == {"pipe@300.0__gboxbody": 9.6} + assert cols["pipe:box_culvert|wing|inlet"]["owners"] == {"pipe@300.0__gboxin": 1.0} + assert cols["pipe:revet|돌쌓기(메)|2.0"]["owners"] == {"pipe@300.0__gboxout": 10.0} + # 세월교 월류 폭 = 저장된 잇기(gford) · 관 공은 본체(사본 키 = 관 지점 id) + assert cols["pipe:ford|1000|2|width"]["owners"] == {"pipe@400.0__gford": 12.0} + assert cols["pipe:len|800"]["owners"] == {"pipe@300.0": 18.0} + # B05 구조물 = 구조물 id · 그룹 잇기 = {id}__{그룹} + assert cols["pipe:open_ditch"]["owners"] == {"s-ditch": 1.0} + assert cols["pipe:cross_underdrain"]["owners"] == {"s-ud__gcrossud": 1.0} + + +def test_실제_사본_폴더_키와_같다(root): # noqa: F811 + result = _run_143(root) + linked = set(snapshots.linked_snapshots(root)) + _check_sums(result, linked) + cols = {c["id"]: c for c in result["columns"]} + assert cols["pipe:len|800"]["owners"] == {"pipe@100.0": 16.0} + assert cols["pipe:len|1000"]["owners"] == {"pipe@500.0__gford": 18.0}