diff --git a/M02_MasterTemplete/M02_Table_Fill.py b/M02_MasterTemplete/M02_Table_Fill.py index a55dc4341..1021a017c 100644 --- a/M02_MasterTemplete/M02_Table_Fill.py +++ b/M02_MasterTemplete/M02_Table_Fill.py @@ -138,6 +138,7 @@ def _with_group_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]: 복사본 `{구조물 id}__{그룹 id}`. 정의된 그룹만(미지정 그룹 = 「미지정」 알림에 셈) · 정의가 사라진 그룹 연결은 기록에만 남고 표에 안 들어감.""" from common_util.common_util_summary_item import GROUP_JOIN + from M02_MasterTemplete.M02_Table_Fill_Spec import container_rows from M02_MasterTemplete.StructOptions.M02_SO_Groups import groups_of defined = groups_of() @@ -154,6 +155,7 @@ def _with_group_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]: "그룹": group["name"], } ) + out.extend(container_rows(item, links)) # 선택 컨테이너마다 한 건(PLAN 52-1b) return out diff --git a/M02_MasterTemplete/M02_Table_Fill_Spec.py b/M02_MasterTemplete/M02_Table_Fill_Spec.py new file mode 100644 index 000000000..2de0d16c3 --- /dev/null +++ b/M02_MasterTemplete/M02_Table_Fill_Spec.py @@ -0,0 +1,63 @@ +"""집계표 채움 — 구조물 명세 선택 컨테이너마다 한 건 더(PLAN 52-1b). + +선택 컨테이너 인스턴스(옵션 `{머리}_{n}_{항목}`)마다 같은 구조물 · 자리에 한 건 — +집계 항목 = 잇기 `group_items[{잇기}{n}]` · 복사본 `{구조물 id}__{잇기}{n}` · 옵션 = 그 컨테이너 값. +열 바인딩이 읽는 칸이 갈래마다 달라(독립 기슭막이 `form` · `length_m` … · 관 `{유입|유출}_revet_*`) +컨테이너 값을 그 칸들에 함께 얹음 — 어느 출처로 읽어도 그 컨테이너 값. +짝: `A00_Common/design/design_structure_pipe.ts` 선택 컨테이너(머리 · 잇기 · 제목) — 시험이 대조. +""" + +from __future__ import annotations + +import re +from typing import Any + +from common_util.common_util_summary_item import GROUP_JOIN + +#: 종류 → 선택 컨테이너 (머리, 잇기, 제목) +SPEC_CONTAINERS: dict[str, tuple[tuple[str, str, str], ...]] = { + "pipe": ( + ("soil_add", "gsoiladd", "흙막이 추가"), + ("chute_add", "gchuteadd", "도수로/돌붙임"), + ), +} +_TEXT = ("structure", "form") + + +def _as_options(values: dict[str, Any]) -> dict[str, Any]: + """컨테이너 값 → 열이 읽는 칸 — 구조 = `structure` · `{쪽}_type` · + 나머지 = 맨 칸 + `{쪽}_revet_*`.""" + out: dict[str, Any] = {} + for item, value in values.items(): + if item == "structure": + out.update(structure=value, inlet_type=value, outlet_type=value) + continue + name = item if item in _TEXT else f"{item}_m" + out[name] = value + for side in ("inlet", "outlet"): + out[f"{side}_revet_{name}"] = value + return out + + +def container_rows(item: dict[str, Any], links: dict[str, Any]) -> list[dict[str, Any]]: + """한 구조물의 선택 컨테이너 인스턴스마다 한 건(n 차례) — 컨테이너 없는 종류는 빈 목록.""" + rows: list[dict[str, Any]] = [] + for prefix, link, title in SPEC_CONTAINERS.get(item["type_id"], ()): + head = re.compile(rf"^{prefix}_(\d+)_(.+)$") + found: dict[int, dict[str, Any]] = {} + for key, value in item["options"].items(): + hit = head.match(key) + if hit: + found.setdefault(int(hit.group(1)), {})[hit.group(2)] = value + for n in sorted(found): + gid = f"{link}{n}" + rows.append( + { + **item, + "options": {**item["options"], **_as_options(found[n])}, + "summary_item": links.get(gid), + "복사본": f"{item['복사본']}{GROUP_JOIN}{gid}", + "그룹": f"{title} {n}", + } + ) + return rows diff --git a/resources/tester/test_52_1b_pipe_links.py b/resources/tester/test_52_1b_pipe_links.py new file mode 100644 index 000000000..c0cc03f99 --- /dev/null +++ b/resources/tester/test_52_1b_pipe_links.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +"""배관 남은 연동 — PLAN 52-1b. + +못박는 것 + - 집계표: 선택 컨테이너(흙막이 추가 · 도수로/돌붙임)마다 한 건 — 잇기 `{link}{n}` 항목 · + 그 컨테이너 값. + - 짝: 서버 컨테이너 표(머리 · 잇기) = 배관 명세 TS. +""" + +from __future__ import annotations + +import json +import os +import re + +from M02_MasterTemplete import M02_Table_Fill as fill +from M02_MasterTemplete.M02_Table_Fill_Spec import SPEC_CONTAINERS, container_rows +from resources.tester.test_structures_summary_snapshot import ( # noqa: F401 — world 고정물 + MS, + PIPE, + RV, + _pipe, + _save, + world, +) + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(HERE)) +SOIL = { + "soil_add_1_structure": "기슭막이", + "soil_add_1_form": "바자", + "soil_add_1_length": 6, + "soil_add_1_height": 2, + "soil_add_2_structure": "기슭막이", + "chute_add_1_structure": "돌붙임", +} + + +def test_컨테이너마다_한_건_잇기와_그_값(): + item = {"type_id": "pipe", "options": {"pipe_diameter_mm": 1000, **SOIL}, "복사본": "pipe@40.0"} + rows = container_rows(item, {"gsoiladd1": RV, "gchuteadd1": MS}) + assert [r["그룹"] for r in rows] == ["흙막이 추가 1", "흙막이 추가 2", "도수로/돌붙임 1"] + assert [r["복사본"] for r in rows] == [ + "pipe@40.0__gsoiladd1", + "pipe@40.0__gsoiladd2", + "pipe@40.0__gchuteadd1", + ] + assert [r["summary_item"] for r in rows] == [RV, None, MS] + first = rows[0]["options"] + assert first["length_m"] == first["inlet_revet_length_m"] == first["outlet_revet_length_m"] == 6 + assert ( + first["form"] == first["outlet_revet_form"] == "바자" and first["outlet_type"] == "기슭막이" + ) + assert first["pipe_diameter_mm"] == 1000 # 구조물 옵션은 그대로 + assert container_rows({"type_id": "masonry_dry", "options": SOIL, "복사본": "st0"}, {}) == [] + # 옛 값 — 컨테이너 없는 옛 관 · 인스턴스 없는 잇기는 줄 없음 + old = {"type_id": "pipe", "options": {"inlet_revet_length_m": 5}, "복사본": "pipe@9.0"} + assert container_rows(old, {"gsoiladd9": RV}) == [] + + +def test_집계표_채움에_컨테이너_줄(world): + """흙막이 추가 1 → 돌기슭막이 열(rv) 6 m · 관 본체 · 미지정 컨테이너는 표에 안 듦.""" + root, system = world["root"], world["system"] + point = {**_pipe(PIPE), "group_items": {"gsoiladd1": RV}} + point["options"] = {**point["options"], **SOIL} + _save(root, [], [point]) + table = json.loads((system / "table/구조물집계표.json").read_text("utf-8")) + doc = fill.fill_table(root, table) + values = [row["값"] for row in doc["줄"] if not row.get("고정")] + rv = [v for row in values for k, v in row.items() if k.split("|")[0] == "rv"] + assert rv == [6] + copies = [i["복사본"] for i in fill.collect_items(root)] + assert "pipe@40.0__gsoiladd1" in copies and "pipe@40.0__gchuteadd1" in copies + + +def test_서버_컨테이너_표는_배관_명세와_짝(): + with open( + os.path.join(ROOT, "A00_Common/design/design_structure_pipe.ts"), encoding="utf-8" + ) as f: + text = f.read() + found = re.findall( + r'title: "([^"]+)",\s*required: false,\s*prefix: "(\w+)",\s*link: "(\w+)"', text + ) + assert [(p, link, t) for t, p, link in found] == list(SPEC_CONTAINERS["pipe"]) diff --git a/resources/tester/ui_checks/52-1_배관_컨테이너.js b/resources/tester/ui_checks/52-1_배관_컨테이너.js index 02840470e..8d0a7cc51 100644 --- a/resources/tester/ui_checks/52-1_배관_컨테이너.js +++ b/resources/tester/ui_checks/52-1_배관_컨테이너.js @@ -9,6 +9,8 @@ * 기대 수치(edit): kinds 5 · required 3 늘 보임 · formBasin I형/L형/ㄷ형 · formSoil 6 · split 길이 7 · * soilAdd 둘 → 하나 빼면 하나(콘크리트) · chute 하나(돌붙임) · 잇기 유입 지정 · 유출 지정 뒤 풂 * 기대 수치(verify): 저장 값 그대로 · 같은 그리기(B05 = B06) · 관 길이 수(B06) + * 52-1b: edit 가 흙막이 추가 · 도수로/돌붙임 컨테이너도 이음 → verify 집계표 줄 = 처음 + 3 + * (유입 잇기 1 · 컨테이너마다 1) */ (async () => { const MODE = window.__m521 || "edit"; @@ -28,6 +30,15 @@ .filter((n) => n.includes(part)) .pop(); const fetchApi = await import(modUrl("/B04_PreProcess/B04_PreProcess_Api_Fetch.ts")); + const m02Api = await import( + modUrl("/B04_PreProcess/B04_PreProcess_Api_Fetch.ts").replace( + /B04_PreProcess\/B04_PreProcess_Api_Fetch\.ts.*$/, + "M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts", + ) + ); + /** 구조물집계표 채움 줄 수(전구간 고정 줄 뺌) — 52-1b. */ + const filledRows = async () => + (await m02Api.readFilled(projectId, "구조물집계표")).문서.줄.filter((r) => !r.고정).length; const spec = () => document.querySelector(".b05-structure__spec"); const box = (c, i = 0) => spec().querySelectorAll(`[data-spec-container="${c}"]`)[i]; const ctl = (c, item, i = 0) => @@ -86,6 +97,8 @@ if (MODE === "edit") { const before = await fetchApi.fetchDetailPipePoints(projectId); sessionStorage.setItem("__521_original", JSON.stringify(before.pipe_points)); + out.baseRows = await filledRows(); + sessionStorage.setItem("__521_baseRows", String(out.baseRows)); await pickPipe(); const s0 = shape(); out.kinds = @@ -128,6 +141,10 @@ await w(200); await set("chute_add", "structure", "돌붙임"); out.chuteChoices = labels("chute_add", "structure"); + // 52-1b — 컨테이너마다 구조물 조합 잇기(집계표 한 줄씩) + await pickLink("soil_add"); + await pickLink("chute_add"); + out.linkContainers = [linkName("soil_add"), linkName("chute_add")]; // 구조물 조합 잇기 — 유입 지정 · 유출 지정 뒤 풂 await pickLink("inlet"); out.linkInlet = linkName("inlet"); @@ -169,6 +186,8 @@ out.sameDrawing = JSON.stringify(b05) === JSON.stringify(b06); if (!out.sameDrawing) out.diff = { b05, b06 }; out.b06Length = ctl("pipe", "length").value; + out.baseRows = Number(sessionStorage.getItem("__521_baseRows")); + out.rows = await filledRows(); } if (MODE === "restore") { @@ -191,7 +210,7 @@ ); await fetchApi.saveDetailPipePoints(projectId, points); for (const k of Object.keys(sessionStorage)) - if (k.startsWith("aislo:draft:pipes:") || k === "__521_original") + if (k.startsWith("aislo:draft:pipes:") || k.startsWith("__521_")) sessionStorage.removeItem(k); const now = await fetchApi.fetchDetailPipePoints(projectId); const pick = (list) =>