From 5098318c969a58ef55797603dcebe5fc71bd79a7 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 22 Oct 2026 20:16:50 +0900 Subject: [PATCH] =?UTF-8?q?feat(513-3=20=EC=9A=B0=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=82=AD=EC=A0=9C):=20=EC=A2=85?= =?UTF-8?q?=EB=8B=A8=20=C2=B7=20=ED=8F=89=EB=A9=B4=20=EC=9A=B0=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=20=E3=80=8C=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=E3=80=8D=20=3D=20=EA=B4=80=20=EC=95=84=EB=8B=8C=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EB=AC=BC=EB=8F=84=20=E2=80=94=20=EC=B8=A1?= =?UTF-8?q?=EC=A0=90=20=EC=A3=BC=EC=9D=B8=20=EA=B0=80=EB=A5=B4=EA=B8=B0=20?= =?UTF-8?q?=EA=B3=B5=EC=9A=A9=20removeStationOwner(design=5Fpipe=5Fids=20?= =?UTF-8?q?=C2=B7=20=EA=B4=80=20=EA=B8=B0=EC=A4=80=20=EC=B8=A1=EC=A0=90=20?= =?UTF-8?q?=3D=20=EA=B4=80=20=EC=A7=80=EC=A0=90=20=EC=A0=95=EB=B3=B8=20?= =?UTF-8?q?=C2=B7=20=EA=B4=80=20=EA=B5=AC=EA=B0=84=20=EB=81=9D=20=3D=20?= =?UTF-8?q?=EC=9E=A5=EB=B6=80=20=EC=9E=90=EB=A6=AC=20=EA=B4=80=20=C2=B7=20?= =?UTF-8?q?=EA=B7=B8=20=EB=B0=96=20=3D=20=EC=A2=8C=EC=B8=A1=20=EC=B9=B8=20?= =?UTF-8?q?[=EC=82=AD=EC=A0=9C]=EC=99=80=20=EA=B0=99=EC=9D=80=20removeById?= =?UTF-8?q?)=20=C2=B7=20B05=20=EC=A2=85=EB=8B=A8=20=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=C2=B7=20B06=20=ED=9A=A1=EB=8B=A8=20=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20=EA=B8=B8=20=C2=B7=20=EC=BA=90=EC=8B=9C?= =?UTF-8?q?=EC=97=90=EB=A7=8C=20=C2=B7=20[=EC=A0=80=EC=9E=A5]=20=EB=95=8C?= =?UTF-8?q?=20=EB=82=98=EA=B0=90=20=C2=B7=20=EC=8B=9C=ED=97=98=20test=5F51?= =?UTF-8?q?3=5F3=20=E2=80=94=20=EC=9A=94=EA=B5=AC=20327-1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- A00_Common/design/design_pipe_ids.ts | 14 ++++++ A00_Common/design/design_view_profile.ts | 11 +++-- .../B06_Section_UI_Section_View_Profile.ts | 8 +++- .../helper_513_3_remove_station_owner.cjs | 38 ++++++++++++++++ .../tester/test_513_3_remove_station_owner.py | 44 +++++++++++++++++++ 5 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 resources/tester/helper_513_3_remove_station_owner.cjs create mode 100644 resources/tester/test_513_3_remove_station_owner.py diff --git a/A00_Common/design/design_pipe_ids.ts b/A00_Common/design/design_pipe_ids.ts index 1c8a2cf10..05228a613 100644 --- a/A00_Common/design/design_pipe_ids.ts +++ b/A00_Common/design/design_pipe_ids.ts @@ -129,3 +129,17 @@ export function followMovedPipe(projectId: string | null, fromM: number, toM: nu { partKey: picked.partKey }, ); } + +/** 종단 측점 하나를 지움(513-3 · 우클릭 「배관 · 구조물 삭제」 종단 · 평면 공용) — 측점 id = 주인 · `주인:역할` + * (`plannedIrregular`) · 관 기준 측점 = 그 자리 · 관 구간 끝 = 관 자리 · 그 밖 = 구조물 id(좌측 칸 [삭제]와 같은 길). */ +export function removeStationOwner( + station: { id: string; chainage_m: number }, + pipe: boolean, + remove: { pipe: (chainageM: number) => void; structure: (structureId: string) => void }, +): void { + if (pipe) return remove.pipe(station.chainage_m); + const owner = station.id.replace(/:(start|end)$/, ""); + const at = pipeChainageOf(owner); + if (at !== null) remove.pipe(at); + else remove.structure(owner); +} diff --git a/A00_Common/design/design_view_profile.ts b/A00_Common/design/design_view_profile.ts index bf8e88b5e..4aa105054 100644 --- a/A00_Common/design/design_view_profile.ts +++ b/A00_Common/design/design_view_profile.ts @@ -66,7 +66,7 @@ import "../../B05_Profile/B05_Profile_UI_Style.css"; import "../../B05_Profile/B05_Profile_UI_Style_Table.css"; import "../../B05_Profile/B05_Profile_UI_Style_Drainage.css"; import "../../B05_Profile/B05_Profile_UI_Style_Structures.css"; -import { pipeIdAt } from "./design_pipe_ids"; +import { pipeIdAt, removeStationOwner } from "./design_pipe_ids"; import { useDesignStore } from "./design_store"; import { pageSettingsPanelValues } from "../../B05_Profile/B05_Profile_UI_Page_Latest"; import { setPlusStations } from "../../common_util/common_util_structure_station"; @@ -194,9 +194,12 @@ export async function mountProfileView( onStructureMove: (from, to, station) => { if (isPipeStation(station)) profilePanel.drainage.movePipe(from, to); }, - onStructureRemove: (station) => { - if (isPipeStation(station)) profilePanel.drainage.removePipe(station.chainage_m); - }, + // 우클릭 삭제 — 관 = 관 지점 정본 · 그 밖 = 좌측 칸 [삭제]와 같은 길(513-3) + onStructureRemove: (station) => + removeStationOwner(station, isPipeStation(station), { + pipe: (chainage) => profilePanel.drainage.removePipe(chainage), + structure: (structureId) => panel.structures.removeById(structureId), + }), // 우클릭 [배관 추가] — 사이드 폼에 종류·측점을 실어 주고 [추가]로 확정(2026-08-18). onPipeAdd: (chainage) => panel.structures.addAt(chainage, "pipe"), onStructureAdd: (chainage, type) => diff --git a/B06_Section/B06_Section_UI_Section_View_Profile.ts b/B06_Section/B06_Section_UI_Section_View_Profile.ts index b1a9bf0b2..71b5e9c2b 100644 --- a/B06_Section/B06_Section_UI_Section_View_Profile.ts +++ b/B06_Section/B06_Section_UI_Section_View_Profile.ts @@ -24,6 +24,7 @@ import { } from "../A00_Common/design/design_selection"; import { paintMapPick } from "../A00_Common/design/design_pick_profile"; import { ESCAPE_FROM } from "../A00_Common/design/design_pick_escape"; +import { removeStationOwner } from "../A00_Common/design/design_pipe_ids"; import { plantsStation } from "../B05_Profile/B05_Profile_UI_Profile_Structures"; import { fetchLatestRoute, @@ -156,7 +157,12 @@ export function createSectionProfile( if (isPipeStation(station)) host.edit()?.movePipe(from, to); }, onStructureRemove: (station) => { - if (isPipeStation(station)) host.edit()?.removePipe(station.chainage_m); + const edit = host.edit(); + if (edit) + removeStationOwner(station, isPipeStation(station), { + pipe: edit.removePipe, + structure: edit.removeStructure, + }); }, onPipeAdd: (chainage) => host.edit()?.addPipe(chainage), onStructureAdd: (chainage, type) => diff --git a/resources/tester/helper_513_3_remove_station_owner.cjs b/resources/tester/helper_513_3_remove_station_owner.cjs new file mode 100644 index 000000000..c1fdc5d9d --- /dev/null +++ b/resources/tester/helper_513_3_remove_station_owner.cjs @@ -0,0 +1,38 @@ +/* 513-3 우클릭 「구조물 삭제」 관 아닌 구조물도(요구 327-1) — `design_pipe_ids.removeStationOwner` 를 Node 로 + * 돌려 JSON 한 줄(`test_513_3_remove_station_owner.py` 가 판정). 관 장부 · 고름은 가짜 · 주인 가르기는 진짜. + * node helper_513_3_remove_station_owner.cjs + */ +const path = require("path"); +const { makeLoader } = require("./helper_ts_stub_loader.cjs"); + +const BOOK = { next: 4, ids: [{ id: "pipe-s3", at: 55 }] }; +const STUBS = { + "../b_page_state": { readState: () => BOOK, writeState() {} }, + "./design_selection": { + PIPE_MATCH_M: 0.51, + isNear: () => false, + nearestWithin: () => null, + readSelection: () => null, + selectDesign() {}, + }, + common_util_pick_tag: { setPipeTagSource() {} }, +}; +const { loadTs } = makeLoader({ root: path.join(__dirname, "..", ".."), stubs: STUBS }); +const ids = loadTs(path.join(__dirname, "..", "..", "A00_Common", "design", "design_pipe_ids.ts")); +ids.usePipeIds("p1"); + +const calls = []; +const remove = { + pipe: (m) => calls.push(["pipe", m]), + structure: (id) => calls.push(["structure", id]), +}; +const cases = [ + [{ id: "pipe-s1", chainage_m: 40 }, true], // 관 기준 측점 + [{ id: "wall-7", chainage_m: 80 }, false], // 구조물 기준 측점 + [{ id: "wall-8:start", chainage_m: 100 }, false], // 구간 구조물 시작 + [{ id: "wall-8:end", chainage_m: 130 }, false], // 구간 구조물 끝 + [{ id: "pipe-s3:end", chainage_m: 60 }, false], // 관 구간 끝 = 장부 자리 + [{ id: "pipe-12.50:start", chainage_m: 10 }, false], // 옛 자리 이름 관 +]; +for (const [station, pipe] of cases) ids.removeStationOwner(station, pipe, remove); +process.stdout.write(JSON.stringify({ calls })); diff --git a/resources/tester/test_513_3_remove_station_owner.py b/resources/tester/test_513_3_remove_station_owner.py new file mode 100644 index 000000000..84769522c --- /dev/null +++ b/resources/tester/test_513_3_remove_station_owner.py @@ -0,0 +1,44 @@ +"""513-3 종단 · 평면 우클릭 「구조물 삭제」 관 아닌 구조물도(요구 327-1) — 측점 주인을 갈라 관 = 관 지점 정본 · +그 밖 = 좌측 칸 [삭제]와 같은 `removeById`(공용 `removeStationOwner`). + +TS 셈은 `helper_513_3_remove_station_owner.cjs`(Node · 주인 가르기는 진짜). + + ./venv/Scripts/python.exe -m pytest resources/tester/test_513_3_remove_station_owner.py -q +""" + +import json +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +HELPER = Path(__file__).with_name("helper_513_3_remove_station_owner.cjs") + + +def test_주인대로_지움() -> None: + run = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", str(HELPER)], + capture_output=True, + text=True, + encoding="utf-8", + timeout=120, + ) + assert run.returncode == 0, run.stderr + assert json.loads(run.stdout)["calls"] == [ + ["pipe", 40], + ["structure", "wall-7"], + ["structure", "wall-8"], + ["structure", "wall-8"], + ["pipe", 55], + ["pipe", 12.5], + ] + + +def test_두_보기_배선() -> None: + view = (ROOT / "A00_Common" / "design" / "design_view_profile.ts").read_text(encoding="utf-8") + assert "removeStationOwner(station, isPipeStation(station)" in view + assert "panel.structures.removeById(structureId)" in view + cross = (ROOT / "B06_Section" / "B06_Section_UI_Section_View_Profile.ts").read_text( + encoding="utf-8" + ) + assert "removeStationOwner(station, isPipeStation(station)" in cross + assert "structure: edit.removeStructure" in cross