From 14965f811b8087b3b240d4b204f6ef717fbe6bd1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 11:26:43 +0900 Subject: [PATCH] =?UTF-8?q?fix(M02):=20=EC=A7=91=EA=B3=84=ED=91=9C=20?= =?UTF-8?q?=ED=9D=99=EB=A7=89=EC=9D=B4=20=EC=A4=84=EC=9D=80=20=EC=84=A0=20?= =?UTF-8?q?=EB=8B=A4=EB=8B=A8=20=EC=88=98=EA=B9=8C=EC=A7=80=EB=A7=8C(54-11?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 배관 흙막이 컨테이너 줄 = 서버가 [저장] 때 세운 유출 다단 수(design.extra_walls)까지 · 넘친 상자 값은 파일에 그대로 · 단 수를 늘리면 되살아남 - 집계 항목 미지정 알림도 같은 수로 셈(같은 뿌리) - 다단 목록 없는 옛 측점 = 저장된 것 전부(옛 동작) - 시험 test_52_1b_pipe_links 둘 더 · 화면 점검 ui_checks/54-11_흙막이_집계 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ --- .../M02_MasterTemplete_Router_Layers.py | 8 +- M02_MasterTemplete/M02_Table_Fill.py | 5 +- M02_MasterTemplete/M02_Table_Fill_Spec.py | 16 ++ resources/tester/test_52_1b_pipe_links.py | 39 +++++ .../tester/ui_checks/54-11_흙막이_집계.js | 138 ++++++++++++++++++ 5 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 resources/tester/ui_checks/54-11_흙막이_집계.js diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py index f6f44ca31..0baf219ef 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py @@ -32,6 +32,7 @@ from M02_MasterTemplete import M02_MasterTemplete_Router_Items as items_router from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_Table_Fill as fill from M02_MasterTemplete import M02_Template_Layers as layers +from M02_MasterTemplete.M02_Table_Fill_Spec import soil_tiers_from_designs logger = logging.getLogger(__name__) @@ -595,7 +596,10 @@ async def filled_table( raise _bad(error) from error if found is None: raise HTTPException(status_code=404, detail="양식을 찾을 수 없습니다.") - lengths = fill.pipe_lengths_from_designs(await _cross_designs(project_id)) - document = await asyncio.to_thread(fill.fill_table, project["root"], found["문서"], lengths) + designs = await _cross_designs(project_id) + lengths = fill.pipe_lengths_from_designs(designs) + document = await asyncio.to_thread( + fill.fill_table, project["root"], found["문서"], lengths, soil_tiers_from_designs(designs) + ) result = await asyncio.to_thread(fill.recalc, document) return {"이름": name, "판": found["판"], "문서": document, "결과": result} diff --git a/M02_MasterTemplete/M02_Table_Fill.py b/M02_MasterTemplete/M02_Table_Fill.py index 862138d91..4d41ea46b 100644 --- a/M02_MasterTemplete/M02_Table_Fill.py +++ b/M02_MasterTemplete/M02_Table_Fill.py @@ -85,6 +85,7 @@ def collect_items( project_root: str | Path, pipe_lengths: dict[float, float] | None = None, types: dict[str, StructureType] | None = None, + soil_tiers: dict[float, int] | None = None, ) -> list[dict[str, Any]]: """설계 정본 → 구조물 목록. @@ -133,6 +134,7 @@ def collect_items( "end_m": None, "options": _with_defaults(types.get(type_id), dict(point.options or {})), "관연장": _nearest(lengths, chainage) if type_id == "pipe" else None, + "흙막이단": _nearest(soil_tiers or {}, chainage), # 54-11 "summary_item": point.summary_item, "복사본": f"pipe@{round(chainage, 3)}", "group_items": point.group_items, @@ -672,10 +674,11 @@ def fill_table( project_root: str | Path, document: dict[str, Any], pipe_lengths: dict[float, float] | None = None, + soil_tiers: dict[float, int] | None = None, ) -> dict[str, Any]: """프로젝트 설계로 표 문서를 채움 — 저장 안 함 · 지정 항목 모드(미지정은 빈칸).""" types = structure_type_map() - items = collect_items(project_root, pipe_lengths, types) + items = collect_items(project_root, pipe_lengths, types, soil_tiers) doc, chosen, notes = assign_items(project_root, document, items) filled = fill_document(doc, chosen, types, assigned=True) filled["알림"] = [*notes, *filled.get("알림", [])] diff --git a/M02_MasterTemplete/M02_Table_Fill_Spec.py b/M02_MasterTemplete/M02_Table_Fill_Spec.py index 6523e0926..bda74e9a9 100644 --- a/M02_MasterTemplete/M02_Table_Fill_Spec.py +++ b/M02_MasterTemplete/M02_Table_Fill_Spec.py @@ -7,6 +7,7 @@ 짝: `A00_Common/design/design_structure_pipe.ts` · `_road.ts` 선택 컨테이너(머리 · 잇기 · 제목) — 시험이 대조. 기타(52-14) = 사용자가 적은 상세 종류 이름이 집계 건 이름(`그룹`) · 비면 「기타」. +배관 흙막이 = 지형이 세운 유출 다단 수(`흙막이단`)까지만 — 넘친 컨테이너 값은 파일에만(54-11). """ from __future__ import annotations @@ -35,6 +36,18 @@ def etc_label(type_id: str, options: dict[str, Any] | None) -> dict[str, str]: return {"그룹": str((options or {}).get("name") or "").strip() or "기타"} +def soil_tiers_from_designs(designs: Any) -> dict[float, int]: + """B06 횡단 설계 → `{주인 측점: 선 유출 다단 수}` — 서버가 세운 `extra_walls` 목록이 있는 측점만 + (B06 `pipeExtraCount` 와 같은 수 · 54-11).""" + found: dict[float, int] = {} + for row in designs or []: + walls = (row.get("design") or {}).get("extra_walls") if isinstance(row, dict) else None + if isinstance(walls, list): + outlet = [w for w in walls if isinstance(w, dict) and w.get("side") == "outlet"] + found[round(float(row.get("chainage_m") or 0.0), 3)] = len(outlet) + return found + + def _as_options(values: dict[str, Any]) -> dict[str, Any]: """컨테이너 값 → 열이 읽는 칸 — 구조 = `structure` · `{쪽}_type` · 나머지 = 맨 칸 + `{쪽}_revet_*`.""" @@ -53,6 +66,7 @@ def _as_options(values: dict[str, Any]) -> dict[str, Any]: def container_rows(item: dict[str, Any], links: dict[str, Any]) -> list[dict[str, Any]]: """한 구조물의 선택 컨테이너 인스턴스마다 한 건(n 차례) — 컨테이너 없는 종류는 빈 목록.""" rows: list[dict[str, Any]] = [] + tiers = item.get("흙막이단") # None = 모름(저장된 것 그대로) for prefix, link, title in SPEC_CONTAINERS.get(item["type_id"], ()): head = re.compile(rf"^{prefix}_(\d+)_(.+)$") found: dict[int, dict[str, Any]] = {} @@ -61,6 +75,8 @@ def container_rows(item: dict[str, Any], links: dict[str, Any]) -> list[dict[str if hit: found.setdefault(int(hit.group(1)), {})[hit.group(2)] = value for n in sorted(found): + if prefix == "soil_add" and tiers is not None and n > tiers: + continue gid = f"{link}{n}" rows.append( { diff --git a/resources/tester/test_52_1b_pipe_links.py b/resources/tester/test_52_1b_pipe_links.py index 87ce7e553..8f6898236 100644 --- a/resources/tester/test_52_1b_pipe_links.py +++ b/resources/tester/test_52_1b_pipe_links.py @@ -82,3 +82,42 @@ def test_서버_컨테이너_표는_배관_명세와_짝(): 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"]) + + +def test_54_11_흙막이는_선_단_수까지만(): + """단 수 2 → 1 = 흙막이 줄 2 → 1(값은 옵션에 그대로) · 다시 2 = 되살아남 · 모름 = 저장된 것 전부.""" + from M02_MasterTemplete.M02_Table_Fill_Spec import soil_tiers_from_designs + + item = {"type_id": "pipe", "options": SOIL, "복사본": "pipe@40.0"} + + def soil(tiers): + rows = container_rows({**item, "흙막이단": tiers}, {}) + return [r["그룹"] for r in rows if r["그룹"].startswith("흙막이")] + + assert soil(2) == ["흙막이 1", "흙막이 2"] + assert soil(1) == ["흙막이 1"] and "soil_add_2_structure" in item["options"] + assert soil(0) == [] and soil(2) == ["흙막이 1", "흙막이 2"] and len(soil(None)) == 2 + assert len(container_rows({**item, "흙막이단": 0}, {})) == 1 # 도수로/돌붙임은 그대로 + walls = [{"side": "outlet"}, {"side": "basin"}, {"side": "outlet"}] + designs = [ + {"chainage_m": 40.0, "design": {"extra_walls": walls}}, + {"chainage_m": 60.0, "design": {"extra_walls": []}}, + {"chainage_m": 80.0, "design": {"pipe_length_m": 9}}, + ] + assert soil_tiers_from_designs(designs) == {40.0: 2, 60.0: 0} + + +def test_54_11_집계표_미지정_알림도_선_단_수만(world): + 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")) + + def note(tiers): + found = fill.fill_table(root, table, None, tiers)["알림"] + return next((n for n in found if n.startswith("집계 항목 미지정")), "") + + many, one = note(None), note({40.0: 1}) + assert many != one # 숨은 흙막이 2 가 미지정 알림에서 빠짐 + assert int(many.split()[3].rstrip("건")) - int(one.split()[3].rstrip("건")) == 1 diff --git a/resources/tester/ui_checks/54-11_흙막이_집계.js b/resources/tester/ui_checks/54-11_흙막이_집계.js new file mode 100644 index 000000000..e6f94bb91 --- /dev/null +++ b/resources/tester/ui_checks/54-11_흙막이_집계.js @@ -0,0 +1,138 @@ +/* 54-11 — 집계표 · 미지정 알림은 지금 보이는 흙막이 상자(단 수 안)만 셈 + * 시작 화면: B06(#/b06-section) · 검증 프로젝트(sub5_검증) · 목록에 다단이 서는 관(기본 「배관 2」 12+18) + * 부르는 법: orca eval --expression "window.__m5411={mode:'단계'}; $(cat 이 파일)" + * look → 단 수 · 보이는 흙막이 상자 · 파일의 흙막이 값 번호 · 집계표 줄 수 · 미지정 알림 + * prep → 보이는 흙막이 상자마다 구조물 조합 잇기 · 마지막 상자 형태 = form → [저장] → look + * tier+ / tier- → 유출 「추가 기슭막이(단)」 + / - → [저장] → look + * 기대: 단 수 2 → 1 에 상자 2 → 1 · 집계 줄(또는 미지정 건) 1 줄어듦 · 파일 값 그대로 · + * 다시 2 에 줄 · 상자 값 되살아남 + */ +(async () => { + const ARGS = window.__m5411 || { mode: "look" }; + const PIPE = ARGS.pipe || "배관 2"; + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 20000) => { + for (let t = 0; t < ms; t += 200) { + if (fn()) return true; + await w(200); + } + return false; + }; + const projectId = localStorage.getItem("frd_current_project_id"); + const modUrl = (part) => + performance + .getEntriesByType("resource") + .map((e) => e.name) + .filter((n) => n.includes(part)) + .pop(); + const fetchUrl = modUrl("/B04_PreProcess/B04_PreProcess_Api_Fetch.ts"); + const fetchApi = await import(fetchUrl); + const m02Api = await import( + fetchUrl.replace( + /B04_PreProcess\/B04_PreProcess_Api_Fetch\.ts.*$/, + "M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts", + ) + ); + const spec = () => + [...document.querySelectorAll(".b05-structure__spec")].find((n) => n.offsetParent !== null); + const soils = () => [...(spec()?.querySelectorAll('[data-spec-container="soil_add"]') ?? [])]; + const items = () => [...document.querySelectorAll(".b05-route__irregular-list li")]; + const station = () => + items() + .find((l) => l.textContent.trim().endsWith(PIPE)) + ?.textContent.trim() + .split(PIPE)[0]; + const pickPipe = async () => { + await until(() => items().length > 0); + const chosen = document.querySelector(".b05-route__irregular-list li.is-selected"); + if (spec() && chosen?.textContent.trim().endsWith(PIPE)) return; + const li = items().find((l) => l.textContent.trim().endsWith(PIPE)); + (li.querySelector("button") ?? li).click(); + await until(() => spec(), 5000); + await w(800); + }; + const tierRow = () => + spec() + ?.querySelector('[data-spec-container="outlet"]') + ?.querySelector(".b06-structure-panel__struct--tier:not(.is-hidden)"); + const look = async () => { + const filled = (await m02Api.readFilled(projectId, "구조물집계표")).문서; + const points = (await fetchApi.fetchDetailPipePoints(projectId)).pipe_points ?? []; + const sta = station(); + // 흙막이 값이 있는 관 지점마다 파일의 흙막이 번호 + const fileSoil = Object.fromEntries( + points + .map((p) => [ + p.chainage_m, + [ + ...new Set( + Object.keys(p.options ?? {}) + .map((k) => /^soil_add_(\d+)_/.exec(k)?.[1]) + .filter(Boolean), + ), + ].sort(), + ]) + .filter(([, list]) => list.length), + ); + return { + station: sta, + tier: tierRow()?.querySelector(".b06-structure-panel__hval")?.textContent ?? null, + soilBoxes: soils().length, + soilValues: soils().map( + (b) => + b.querySelector( + '[data-spec-item="soil_add.form"] select, [data-spec-item="soil_add.form"] input', + )?.value ?? "", + ), + fileSoil, + rows: filled.줄.filter((r) => !r.고정).length, + notes: (filled.알림 ?? []).filter((n) => n.startsWith("집계 항목")), + }; + }; + const pickLink = async (box) => { + box.querySelector('[data-b05-summary="pick"]').click(); + await until(() => document.querySelector(".ui-item-picker__tree .ui-tree__row")); + for (let k = 0; k < 8; k++) { + const rows = [...document.querySelectorAll(".ui-item-picker__tree .ui-tree__row")]; + const leaf = rows.find((r) => !r.classList.contains("is-branch")); + if (leaf) { + leaf.click(); + break; + } + rows.find((r) => r.querySelector(".ui-tree__caret")?.textContent === "▸")?.click(); + await w(300); + } + await w(300); + [...document.querySelectorAll(".ui-item-picker__actions button")] + .find((b) => b.textContent.includes("적용")) + .click(); + await w(500); + }; + const save = async () => { + document.querySelector('[data-dock-action="save"]').click(); + await w(500); + out.saved = await until(() => !document.querySelector(".ui-overlay.is-active"), 90000); + await w(1500); + }; + await pickPipe(); + const out = { mode: ARGS.mode, pipe: PIPE }; + // prep — 보이는 흙막이 상자마다 구조물 조합 잇기 · 마지막 상자 형태 = ARGS.form → [저장] + if (ARGS.mode === "prep") { + for (const box of soils()) await pickLink(box); + const el = soils().at(-1)?.querySelector('[data-spec-item="soil_add.form"] select'); + if (el && ARGS.form) { + el.value = ARGS.form; + el.dispatchEvent(new Event("change", { bubbles: true })); + await w(1500); + } + await save(); + } + if (ARGS.mode === "tier+" || ARGS.mode === "tier-") { + const btns = [...(tierRow()?.querySelectorAll("button") ?? [])]; + btns.find((b) => b.textContent === (ARGS.mode === "tier+" ? "+" : "-"))?.click(); + await w(1500); + await save(); + } + Object.assign(out, await look()); + return JSON.stringify(out); +})();