From f91c59a2f3c705d48d6b9f2a1751a51b175387c7 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 18 Oct 2026 09:19:22 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05=20=ED=8F=89=EB=A9=B4=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=AC=BC=20=EB=A7=88=EC=BB=A4):=20=EB=A7=88=EC=BB=A4?= =?UTF-8?q?=20=EA=B3=A0=EB=A6=84=20=ED=92=82=20=3D=20=EC=A2=8C=EC=B8=A1=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EA=B0=95=EC=A1=B0=20=C2=B7=20=ED=8F=BC?= =?UTF-8?q?=EB=8F=84=20=ED=92=82=20(409-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 평면 고름 이름 PLAN_PICK_FROM 을 design_pick_escape 로 옮김(Cross · 마커가 같이 씀) - 종단 고름 구독 = Esc 처럼 b05-plan 풂(값 없음)이면 폼 닫음 · 패널 접힘 · 펼침 그대로 · 오른쪽 판 그대로 - 시험 409 에 409-2 더함(새 helper_409_2) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte --- A00_Common/design/design_pick_escape.ts | 4 + A00_Common/design/design_pick_profile.ts | 10 +- B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts | 4 +- ...B05_Profile_UI_RouteEdit_StructureMarks.ts | 5 +- .../helper_399_plan_structure_marks.cjs | 2 +- .../tester/helper_409_2_plan_release_form.cjs | 102 ++++++++++++++++++ .../tester/helper_411_a_cross_structure.cjs | 1 + .../tester/test_409_mark_top_deselect.py | 46 ++++++++ 8 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 resources/tester/helper_409_2_plan_release_form.cjs diff --git a/A00_Common/design/design_pick_escape.ts b/A00_Common/design/design_pick_escape.ts index 691c328e5..c7ccca581 100644 --- a/A00_Common/design/design_pick_escape.ts +++ b/A00_Common/design/design_pick_escape.ts @@ -12,6 +12,10 @@ import { NO_SELECTION, selectDesign } from "./design_selection"; /** Esc 로 푼 고름의 출처 이름 — 구독자가 폼까지 닫을지 가르는 데 씀. */ export const ESCAPE_FROM = "esc"; +/** 평면 노선 편집 창이 낸 고름(눈금 클릭 · 구조물 마커 고름 풂) — 오른쪽 횡단 판은 제 것이라 거름 · + * 풂(값 없음)이면 Esc 처럼 좌측 폼까지 닫음(409-2). */ +export const PLAN_PICK_FROM = "b05-plan"; + const OWN_ESCAPE = "input, select, textarea, [contenteditable='true'], [role='dialog'], .ui-modal__backdrop"; diff --git a/A00_Common/design/design_pick_profile.ts b/A00_Common/design/design_pick_profile.ts index 795dea194..a7a1454a8 100644 --- a/A00_Common/design/design_pick_profile.ts +++ b/A00_Common/design/design_pick_profile.ts @@ -15,7 +15,7 @@ import { subscribeSelection, type DesignSelection, } from "./design_selection"; -import { bindEscapeRelease, ESCAPE_FROM } from "./design_pick_escape"; +import { bindEscapeRelease, ESCAPE_FROM, PLAN_PICK_FROM } from "./design_pick_escape"; import { pipeIdAt } from "./design_pipe_ids"; import { graphTargetAt } from "../../B05_Profile/B05_Profile_UI_Selection"; import { irregularStationId } from "../../B05_Profile/B05_Profile_UI_IrregularStations"; @@ -149,10 +149,12 @@ export function bindProfilePick( if (target.profile.drainage) paintMapPick(target.profile.drainage, selection); }; host.onDispose( - subscribeSelection(projectId, (_selection, from) => { + subscribeSelection(projectId, (selection, from) => { paint(); - if (from === ESCAPE_FROM && host.visible() && target.form?.hasSelection()) - target.form.selectById(null); + // 풀기 = Esc · 평면 마커 고름 풂(409-2) — 폼 닫기는 펴지 않음(좌측 패널 접힘 · 펼침 그대로 · 398-1) + const release = + from === ESCAPE_FROM || (from === PLAN_PICK_FROM && selection.chainageM === null); + if (release && host.visible() && target.form?.hasSelection()) target.form.selectById(null); }), ); bindEscapeRelease(projectId, host); diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts index 6479e1049..177be2ab0 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts @@ -35,6 +35,7 @@ import { type DesignSelection, } from "../A00_Common/design/design_selection"; import { useDesignStore } from "../A00_Common/design/design_store"; +import { PLAN_PICK_FROM } from "../A00_Common/design/design_pick_escape"; import { stationSection } from "./logic/B05_Profile_Pipeline_Detail"; import { buildCrossSvg, @@ -79,9 +80,6 @@ export interface CrossWatch { structure?: true; } -/** 공용 고름에 적는 이름 — 평면 눈금 클릭(제 알림은 거름). */ -export const PLAN_PICK_FROM = "b05-plan"; - /** 법선을 긋는 측점 갈래 — 일반측점(시점 · 종점 · 곡선 측점 포함). */ const isGridStation = (station: PipelineStation): boolean => station.kind === "grid" || station.kind === "end" || station.kind === "curve"; diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks.ts b/B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks.ts index f316717ab..6c82835d8 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks.ts @@ -9,7 +9,8 @@ * · 끌기 · 방향키(마커 고른 때 · 교각점 안 고른 때) = 노선 따라 정수 m · 1 m 씩 · 시점 · 종점 밖 안 감 · * 놓을 때 · 뗄 때 한 번 옮김(`move` = 알약 끌기와 같은 길 · 관 · 구조물 초안 · [저장]이 정본). * · 그림 · 집기 차례 = 맨 위 · 맨 앞(409 · 재기 다음) — `_Render` · `_Pointer` 가 부름. - * · 고름 풀기 = 마커 아닌 곳 누름 · Esc(409) — 공용 고름을 `PLAN_PICK_FROM` 으로 비움(오른쪽 횡단 판 그대로). + * · 고름 풀기 = 마커 아닌 곳 누름 · Esc(409) — 공용 고름을 `PLAN_PICK_FROM` 으로 비움(오른쪽 횡단 판 그대로 · + * 좌측 목록 강조 · 폼은 풂 409-2). * 목록 · 고름 · 옮김은 종단 보기가 `setPlanMarkSource` 로 건넴. 걸음 셈은 `logic/…_StructureMarks`. * ========================================================================== */ @@ -38,7 +39,7 @@ import { HOLD_DELAY_MS, HOLD_INTERVAL_MS } from "./B05_Profile_UI_Profile_Edit"; import { drawStationNormal } from "./B05_Profile_UI_RouteEdit_Hover"; import { routePointAtScreen } from "./B05_Profile_UI_RouteEdit_Input"; import type { RouteEditPointerParams } from "./B05_Profile_UI_RouteEdit_Pointer"; -import { PLAN_PICK_FROM } from "./B05_Profile_UI_RouteEdit_Cross"; +import { PLAN_PICK_FROM } from "../A00_Common/design/design_pick_escape"; import type { RouteEditScene } from "./B05_Profile_UI_RouteEdit_Render"; import { stationFrameAt } from "./logic/B05_Profile_UI_RouteEdit_Frame"; import type { Vertex } from "./logic/B05_Profile_UI_RouteEdit_Curve"; diff --git a/resources/tester/helper_399_plan_structure_marks.cjs b/resources/tester/helper_399_plan_structure_marks.cjs index 12a2a3e16..a33ee9a40 100644 --- a/resources/tester/helper_399_plan_structure_marks.cjs +++ b/resources/tester/helper_399_plan_structure_marks.cjs @@ -68,7 +68,7 @@ const STUBS = { selection = value.chainageM === null ? null : value; }, }, - "./B05_Profile_UI_RouteEdit_Cross": { PLAN_PICK_FROM: "b05-plan" }, + "../A00_Common/design/design_pick_escape": { PLAN_PICK_FROM: "b05-plan" }, "../B06_Section/B06_Section_UI_Cross_Panel_Base": { lastPointerWithin: () => true }, "./B05_Profile_UI_Profile_Edit": { HOLD_DELAY_MS: 500, HOLD_INTERVAL_MS: 100 }, "./B05_Profile_Api_Structures": { diff --git a/resources/tester/helper_409_2_plan_release_form.cjs b/resources/tester/helper_409_2_plan_release_form.cjs new file mode 100644 index 000000000..00f71f6ff --- /dev/null +++ b/resources/tester/helper_409_2_plan_release_form.cjs @@ -0,0 +1,102 @@ +/* 409-2 평면 마커 고름 풂(b05-plan · 값 없음) = 좌측 폼 · 목록 강조도 풂 — 진짜 `design_selection` · + * `design_pick_escape` · `design_pick_profile` 을 Node 로 돌림. 종단도 · 폼 · 오른쪽 횡단 판은 흉내. + * 표준출력 JSON 한 벌 · 짝 = test_409_mark_top_deselect.py. + */ +const fs = require("fs"); +const path = require("path"); +const ROOT = path.join(__dirname, "..", ".."); +const ts = require(path.join(ROOT, "config", "node_modules", "typescript")); + +function loadTs(file, stubs = {}) { + const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, file), "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + const localRequire = (request) => { + if (stubs[request]) return stubs[request]; + throw new Error(`stub 없음: ${request}`); + }; + new Function("exports", "module", "require", js)(box.exports, box, localRequire); + return box.exports; +} + +globalThis.window = { addEventListener() {}, removeEventListener() {} }; +globalThis.document = { querySelector: () => null }; + +const store = new Map(); +const DSel = loadTs("A00_Common/design/design_selection.ts", { + "../b_page_state": { + readState: (key, pid) => store.get(`${key}:${pid}`) ?? null, + writeState: (key, value, pid) => store.set(`${key}:${pid}`, value), + }, +}); +const ESC = loadTs("A00_Common/design/design_pick_escape.ts", { "./design_selection": DSel }); +const IRR = { irregularStationId: (id) => `irregular:${id}`, isPipeStation: () => true }; +const SY = loadTs("B05_Profile/B05_Profile_UI_Selection.ts", { + "../A00_Common/design/design_selection": DSel, + "./B05_Profile_UI_IrregularStations": IRR, +}); +const PP = loadTs("A00_Common/design/design_pick_profile.ts", { + "./design_selection": DSel, + "./design_pipe_ids": { pipeIdAt: (at) => `pipe-${at.toFixed(2)}` }, + "./design_pick_escape": ESC, + "../../B05_Profile/B05_Profile_UI_Selection": SY, + "../../B05_Profile/B05_Profile_UI_IrregularStations": IRR, +}); + +const PID = "p409"; +const sel = (chainageM, structureId, from) => + DSel.selectDesign(PID, chainageM === null ? DSel.NO_SELECTION : { chainageM, structureId }, { + from, + }); + +/* 폼 — 관 폼을 닫으면 진짜 폼처럼 관 고름도 비움(`resetForm` → onPipeSelect(null) → b05 풂) */ +const form = { open: null, resets: 0 }; +const profile = { structure: null }; +PP.bindProfilePick( + PID, + { + profile: { + setSelectedStation: () => (profile.structure = null), + setSelectedStructure: (id) => (profile.structure = id), + }, + form: { + hasSelection: () => form.open !== null, + selectById: () => { + const wasPipe = form.open === "pipe"; + form.open = null; + form.resets += 1; + if (wasPipe) sel(null, null, "b05"); + }, + }, + }, + { visible: () => true, onDispose() {} }, + { irregular: () => [], pipes: () => [{ chainage_m: 80.17 }], sections: () => [] }, +); +/* 오른쪽 횡단 판 흉내 — 받은 알림(낸 쪽) */ +const crossSaw = []; +DSel.subscribeSelection(PID, (selection, from) => crossSaw.push([selection.chainageM, from])); + +const out = {}; +// 관 마커 고름 → 폼 열림 → 평면 마커 고름 풂 +sel(80.17, null, "b05"); +form.open = "pipe"; +crossSaw.length = 0; +sel(null, null, ESC.PLAN_PICK_FROM); +out.pipeRelease = { form: { ...form }, crossSaw: [...crossSaw], session: DSel.readSelection(PID) }; +// 구조물 고름 → 폼 → 풂 +sel(40, "A", "b05"); +form.open = "A"; +sel(null, null, ESC.PLAN_PICK_FROM); +out.structureRelease = { form: { ...form }, pill: profile.structure }; +// 평면 눈금 클릭(값 있음 · b05-plan) = 폼 안 닫음(옛 그대로) +form.open = "A"; +sel(20, null, ESC.PLAN_PICK_FROM); +out.tickKeepsForm = form.open; +// 폼 없음 = 안 부름 +form.open = null; +const resets = form.resets; +sel(null, null, ESC.PLAN_PICK_FROM); +out.noFormNoReset = form.resets - resets; +out.planName = ESC.PLAN_PICK_FROM; +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/helper_411_a_cross_structure.cjs b/resources/tester/helper_411_a_cross_structure.cjs index aadd50bb2..d5a6c4f98 100644 --- a/resources/tester/helper_411_a_cross_structure.cjs +++ b/resources/tester/helper_411_a_cross_structure.cjs @@ -60,6 +60,7 @@ const STUBS = { selectDesign() {}, subscribeSelection: (_projectId, fn) => ((listener = fn), () => {}), }, + "../A00_Common/design/design_pick_escape": { PLAN_PICK_FROM: "b05-plan" }, "../A00_Common/design/design_store": { useDesignStore: () => ({ subscribe: () => () => {}, get: () => null }), }, diff --git a/resources/tester/test_409_mark_top_deselect.py b/resources/tester/test_409_mark_top_deselect.py index 4eb802637..d8a4f23a5 100644 --- a/resources/tester/test_409_mark_top_deselect.py +++ b/resources/tester/test_409_mark_top_deselect.py @@ -74,3 +74,49 @@ def test_오른쪽_판은_제_이름_알림을_거름() -> None: assert "if (from !== PLAN_PICK_FROM) follow(selection);" in cross marks = _read("B05_Profile", "B05_Profile_UI_RouteEdit_StructureMarks.ts") assert "selectDesign(projectId, NO_SELECTION, { from: PLAN_PICK_FROM });" in marks + + +# ── 409-2 마커 고름 풂 = 좌측 목록 강조 · 폼도 풂 · 패널 접힘 · 펼침 그대로 · 오른쪽 판 그대로 ── + +HELPER_2 = Path(__file__).with_name("helper_409_2_plan_release_form.cjs") + + +@pytest.fixture(scope="module") +def got2() -> dict: + run = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", str(HELPER_2)], + capture_output=True, + text=True, + encoding="utf-8", + timeout=120, + ) + assert run.returncode == 0, run.stderr + return json.loads(run.stdout) + + +def test_풂_관_폼_닫힘_오른쪽_판은_제_알림만(got2) -> None: + release = got2["pipeRelease"] + assert release["form"] == {"open": None, "resets": 1} + assert release["crossSaw"] == [[None, "b05-plan"]] # 폼이 낸 b05 풂은 알림 안 감(되울림 막이) + assert release["session"] is None + + +def test_풂_구조물_폼_닫힘(got2) -> None: + assert got2["structureRelease"] == {"form": {"open": None, "resets": 2}, "pill": None} + + +def test_눈금_클릭은_폼_그대로(got2) -> None: + assert got2["tickKeepsForm"] == "A" + assert got2["noFormNoReset"] == 0 + assert got2["planName"] == "b05-plan" + + +def test_폼_닫기는_패널_안_폄() -> None: + panel = _read("B05_Profile", "B05_Profile_UI_Structures_Panel.ts") + head = panel[panel.index(" selectById(structureId) {") :] + assert head.index("if (structureId !== null) callbacks.onReveal?.();") < head.index( + "resetForm();" + ) + assert "is-collapsed" not in head[: head.index(" addAt(")] # 접힘 · 펼침 안 건드림 + pick = _read("A00_Common", "design", "design_pick_profile.ts") + assert "from === PLAN_PICK_FROM && selection.chainageM === null" in pick