- 서버 pipe_summary 응답 unlinked = 셈한 칸에 사본 없는 시설 · 칸 전부 + 연결 여부(몸 = Summary_Sheet) - 같은 측점 박스 + 관 = 종류로 가름 · 세월교 필수 잇기(gford · gfordin · gfordout) 칸 포함 - 화면 = 그 측점 줄 「· 미연결」 · 알림 글 「「미연결」 줄을 눌러 조합 연결」 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
136 lines
5.5 KiB
Python
136 lines
5.5 KiB
Python
"""B08 구조물 집계표 모음 상자 — 열 · 측점 줄 · 계 · 사본 주인별 값 · 칸 출처.
|
|
|
|
`B08_Quantity_Router_Summary.py` 가 700줄을 넘어 뗌(10-9 통합) — 셈 규칙은 그쪽 그대로.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from common_util.common_util_summary_item import GROUP_JOIN
|
|
|
|
|
|
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
|
|
|
|
|
|
class _Sheet:
|
|
"""열 · 측점 줄 · 계를 모음 — 같은 측점(1 cm)의 같은 열은 더함."""
|
|
|
|
def __init__(self) -> None:
|
|
self.columns: dict[str, dict[str, Any]] = {}
|
|
self.rows: dict[int, dict[str, Any]] = {}
|
|
self.owner = "" # 지금 셈하는 시설 — 사본 없어 안 센 칸의 시설을 모음(143-1)
|
|
self.kind = "" # 그 시설 종류 — 같은 측점 박스 + 관은 주인 id 가 같음(154-1)
|
|
self.unlinked: set[tuple[str, str]] = set()
|
|
self.sources: list[list[Any]] = [] # 칸마다 [열, 값, 시설, 조합] — 자재 재연결(143-4)
|
|
|
|
def add(
|
|
self,
|
|
chainage: float,
|
|
key: str,
|
|
sort: tuple[Any, ...],
|
|
group: list[str],
|
|
label: str,
|
|
value: float | None,
|
|
combos: list[str | None],
|
|
) -> None:
|
|
if not any(combos): # 그 칸을 낸 조합 사본이 없음 — 안 셈(143-1)
|
|
self.unlinked.add((self.kind, self.owner))
|
|
return
|
|
cid = f"pipe:{key}"
|
|
column = self.columns.setdefault(
|
|
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(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)
|
|
self.sources.append([cid, value, self.owner, [c for c in combos if c]])
|
|
|
|
def result(self) -> dict[str, Any]:
|
|
columns = sorted(self.columns.values(), key=lambda column: column["_sort"])
|
|
rows = [self.rows[key] for key in sorted(self.rows)]
|
|
total: dict[str, float] = {}
|
|
for row in rows:
|
|
for cid, value in row["values"].items():
|
|
total[cid] = round(total.get(cid, 0.0) + value, 3)
|
|
return {
|
|
"columns": [{k: v for k, v in c.items() if k != "_sort"} for c in columns],
|
|
"rows": rows,
|
|
"total": total,
|
|
"sources": self.sources,
|
|
}
|
|
|
|
|
|
#: 옵션 그룹 정의 밖 필수 컨테이너 잇기(`design_structure_ford.ts` FORD_SPEC `link` · 제목)
|
|
FIXED_SLOTS = {
|
|
"ford_bridge": (("gford", "세월교"), ("gfordin", "유입 구조"), ("gfordout", "유출 구조"))
|
|
}
|
|
|
|
|
|
def unlinked_facilities(
|
|
items: list[dict[str, Any]],
|
|
linked: dict[str, Any],
|
|
missing: set[tuple[str, str]],
|
|
type_names: dict[str, str],
|
|
stored: dict[tuple[str, str], dict[str, Any]],
|
|
) -> list[dict[str, Any]]:
|
|
"""미연결 시설(셈한 칸 기준 `missing` · 154-1) — 시설마다 칸 전부 + 연결 여부(154-2 바로 연결).
|
|
|
|
`{id, type_id, name, facility(관 지점만), chainage_m, slots[{copy, group, item, linked}]}` ·
|
|
본체 칸 `group` = None · `stored` = 시설마다 저장된 `group_items`(필수 컨테이너 잇기 칸)."""
|
|
out: dict[tuple[str, str], dict[str, Any]] = {}
|
|
for item in items:
|
|
owner = item["복사본"].partition(GROUP_JOIN)[0]
|
|
key = (item["type_id"], owner)
|
|
if key not in missing:
|
|
continue
|
|
entry = out.setdefault(
|
|
key,
|
|
{
|
|
"id": owner,
|
|
"type_id": item["type_id"],
|
|
"name": type_names.get(item["type_id"], item["type_id"]),
|
|
"facility": item["type_id"] if owner.startswith("pipe@") else None,
|
|
"chainage_m": float(item["chainage_m"]),
|
|
"slots": [],
|
|
},
|
|
)
|
|
entry["slots"].append(
|
|
{
|
|
"copy": item["복사본"],
|
|
"group": item.get("그룹") if item["복사본"] != owner else None,
|
|
"item": item.get("summary_item"),
|
|
"linked": item["복사본"] in linked,
|
|
}
|
|
)
|
|
for (type_id, owner), entry in out.items():
|
|
have = {slot["copy"] for slot in entry["slots"]}
|
|
links = stored.get((type_id, owner)) or {}
|
|
for gid, title in FIXED_SLOTS.get(type_id, ()):
|
|
copy = f"{owner}{GROUP_JOIN}{gid}"
|
|
if copy not in have:
|
|
entry["slots"].append(
|
|
{"copy": copy, "group": title, "item": links.get(gid), "linked": copy in linked}
|
|
)
|
|
return sorted(out.values(), key=lambda entry: (entry["chainage_m"], entry["id"]))
|