From a42fcd920ed55432384888fb2454d271618c291a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 8 Oct 2026 18:43:59 +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=BB=A4=ED=94=8C=EB=A7=81=20=3D?= =?UTF-8?q?=20=EC=A2=8C=EC=B8=A1=20=ED=8C=A8=EB=84=90=201=EB=B3=B8=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=EB=A1=9C=20=EB=B0=94=EB=A1=9C=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EC=85=88=20=C2=B7=20=EC=9D=91=EB=8B=B5=EC=97=90=20?= =?UTF-8?q?=EA=B4=80=EB=A7=88=EB=8B=A4=20=EA=B8=B8=EC=9D=B4=20pipes(122-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ --- B08_Quantity/B08_Quantity_Router_Summary.py | 6 ++++- B08_Quantity/B08_Quantity_UI_Summary.ts | 24 ++++++++++++++++---- resources/tester/test_98_b08_pipe_summary.py | 17 ++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Router_Summary.py b/B08_Quantity/B08_Quantity_Router_Summary.py index c70159575..cdec4ccfe 100644 --- a/B08_Quantity/B08_Quantity_Router_Summary.py +++ b/B08_Quantity/B08_Quantity_Router_Summary.py @@ -13,6 +13,8 @@ 머리 글 = M02 정의 이름(`구조물정의.json` 종류.pipe.이름 — 사용자가 M02 에서 고침) · 열마다 `combos` = 그 값을 낸 컨테이너에 이은 조합 id(`{층}|{주인}|{키}` · 95-3 모달 · 맨 윗 머리). 설계 = B08 토공과 같은 길(확정 노선 먼저) · 관 · 컨테이너 줄 = M02 채움 `collect_items` 재사용. +`pipes` = 관마다 길이(커플링 열 · 측점) — 좌측 패널 1본 길이를 바꾸면 화면이 같은 식으로 바로 다시 셈 +(짝 = `couplings` ↔ `B08_Quantity_Options_Calc.ts` `couplings` · `recountCouplings` · PLAN 122). """ from __future__ import annotations @@ -181,6 +183,7 @@ def pipe_summary( entry["base"] = item sheet = _Sheet() + pipes: list[dict[str, Any]] = [] no_length = no_basin = 0 soil_name = names.get(f"outlet.structure={SOIL}", SOIL) for owner, entry in by_owner.items(): @@ -213,6 +216,7 @@ def pipe_summary( couplings(length, one_m), pipe_combos, ) + pipes.append({"chainage_m": at, "column": f"pipe:cp|{diameter}", "length_m": length}) shape = fill._nearest(basins, at) no_basin += shape is None if shape: @@ -288,7 +292,7 @@ def pipe_summary( notes.append(f"집수정 모양 기록 없음 {no_basin}곳 — B06 횡단 [저장] 뒤 채워짐") if one_m is None: notes.append(f"1본 길이 변수({ONE_LENGTH_VAR}) 빈칸 — 커플링 빈칸") - return {**out, "notes": notes, "one_length_m": one_m} + return {**out, "notes": notes, "one_length_m": one_m, "pipes": pipes} def _summary(root: Path, designs: list[dict[str, Any]]) -> dict[str, Any]: diff --git a/B08_Quantity/B08_Quantity_UI_Summary.ts b/B08_Quantity/B08_Quantity_UI_Summary.ts index 3686f449c..cddde59b7 100644 --- a/B08_Quantity/B08_Quantity_UI_Summary.ts +++ b/B08_Quantity/B08_Quantity_UI_Summary.ts @@ -8,6 +8,7 @@ * 배관(횡단배수관) 열 · 칸 = GET …/quantity/summary(서버 단독 · PLAN 98) — 머리 구조물 · 형식 · 규격 * 3단 + 단위 줄 · 「관 공」 맨 윗 머리 = 관에 이은 조합 경로(있으면) · 다른 구조물 열은 조합 그대로 * 줄을 누르면 그 측점의 구조물도 모달(PLAN 95-3 · `B08_Quantity_UI_Drawing` 은 누를 때 `import()`) + * 커플링 열 = 좌측 패널 1본 길이로 바로 다시 셈(관마다 길이 `pipes` · `recountCouplings` · PLAN 122) * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; @@ -32,6 +33,8 @@ import { type PlacedItem, type StationCombo, } from "./B08_Quantity_Summary_Data"; +import { recountCouplings, type PipeLength } from "./B08_Quantity_Options_Calc"; +import { liveOnePipe, onOptionsChange } from "./B08_Quantity_UI_Options"; function L(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; @@ -65,6 +68,8 @@ interface PipeSummary { rows: Array<{ chainage_m: number; values: Record }>; total: Record; notes: string[]; + one_length_m: number | null; + pipes: PipeLength[]; } async function fetchPipeSummary(projectId: string): Promise { @@ -204,20 +209,29 @@ export async function renderStructureSummary(host: HTMLElement, projectId: strin ...placed.filter((point) => (point.facility ?? "pipe") !== "pipe"), ]; const columns = summaryColumns(others, describe); - const summary = typeof pipe === "string" ? null : pipe; + const saved = typeof pipe === "string" ? null : pipe; + const live = (): PipeSummary | null => + saved && recountCouplings(saved, liveOnePipe(projectId, saved.one_length_m)); + const summary = live(); const parts: HTMLElement[] = []; if (typeof pipe === "string") parts.push(note(`${L("B08_Quantity_Summary_Failed")} — ${pipe}`)); for (const text of summary?.notes ?? []) parts.push(note(text)); if (!columns.length && !summary?.columns.length) parts.push(note(L("B08_Quantity_Summary_NoCombo"))); - parts.push( + const draw = (): HTMLElement => stations.length - ? buildTable(stations, columns, summary, comboPath, interval, (station, label) => + ? buildTable(stations, columns, live(), comboPath, interval, (station, label) => openSheets(projectId, label, combosAt(things, station), m02, describe), ) - : note(L("B08_Quantity_Summary_NoStation")), - ); + : note(L("B08_Quantity_Summary_NoStation")); + let table = draw(); + parts.push(table); host.replaceChildren(...parts); + onOptionsChange(host, () => { + const next = draw(); + table.replaceWith(next); + table = next; + }); } catch (error) { const reason = error instanceof Error ? error.message : String(error); host.replaceChildren(note(`${L("B08_Quantity_Summary_Failed")} — ${reason}`)); diff --git a/resources/tester/test_98_b08_pipe_summary.py b/resources/tester/test_98_b08_pipe_summary.py index be9f0c99a..477f3fc20 100644 --- a/resources/tester/test_98_b08_pipe_summary.py +++ b/resources/tester/test_98_b08_pipe_summary.py @@ -210,6 +210,23 @@ def test_1본_길이는_프로젝트_층_변수(root, tmp_path, monkeypatch): assert summary.one_pipe_length(root) is None +def test_관마다_길이_칸_화면이_1본_바꿔_다시_셈(root): + # 122 — 응답 `pipes` = 관마다 길이 · 커플링 열(화면 recountCouplings 가 같은 식으로 셈) + six, eight = _run(root, 6.0), _run(root, 8.0) + assert six["pipes"] == eight["pipes"] + assert [(round(p["chainage_m"]), p["column"], p["length_m"]) for p in six["pipes"]] == [ + (1433, "pipe:cp|1200", 22), + (1567, "pipe:cp|800", 18), + (1680, "pipe:cp|800", 16), + (1790, "pipe:cp|800", None), + ] + for out, one in ((six, 6.0), (eight, 8.0)): + total = sum( + summary.couplings(p["length_m"], one) or 0 for p in out["pipes"] if "800" in p["column"] + ) + assert out["total"]["pipe:cp|800"] == total + + def test_라우터_등록(): text = (Path(__file__).resolve().parents[2] / "main.py").read_text(encoding="utf-8") assert "app.include_router(b08_summary_router, dependencies=protected_with_company)" in text