diff --git a/B06_Section/B06_Section_Station_Keys.ts b/B06_Section/B06_Section_Station_Keys.ts index 211e09fda..2de267c95 100644 --- a/B06_Section/B06_Section_Station_Keys.ts +++ b/B06_Section/B06_Section_Station_Keys.ts @@ -17,7 +17,9 @@ * `ROUTE_CARRY_STATES`)도 측점 열쇠로 새 노선 측점에 옮김(구조물 조절 = 관 · 구조물 열쇠만 · 서버와 같음). * ========================================================================== */ -import { readState, writeState, type StateName } from "../A00_Common/b_page_state"; +import { readState, stateKey, writeState, type StateName } from "../A00_Common/b_page_state"; +import { renameChainageKeys } from "./logic/B06_Section_Station_Swap"; +import { afterAnySwap } from "./B06_Section_Swap_Bus"; import { pipeIdAt, pipeIdNear } from "../A00_Common/design/design_pipe_ids"; import { showToast } from "@ui/ui_template_elements"; import { @@ -466,3 +468,36 @@ export function carryChainageStates( } return outside.size; } + +/** + * 측점이 움직임(갈아끼움 · 옛 0.01 m 글 → 새 글) — 조정창 맵이 안 선 누가거리 칸(B05 만 연 페이지 · 새로고침 · + * 세월 · BOX · 연동)을 세션에서 바로 새 자리로(415-3). 칸 = `ROUTE_CARRY_STATES`(측점 열쇠 칸 빼고) · 조정창 + * 맵이 이번에 옮긴 칸(`owned`)은 그대로. + */ +function moveIdleStates(moves: ReadonlyMap, owned: ReadonlySet): void { + if (!moves.size || !current) return; + const { projectId, routeId } = current; + const move = (cells: Record) => + Object.fromEntries(renameChainageKeys(new Map(Object.entries(cells)), moves)); + for (const state of Object.keys(ROUTE_CARRY_STATES)) { + if ((KEYED_STATES as readonly string[]).includes(state)) continue; + const name = state as StateName; + const key = stateKey(name, projectId, routeId); + if (!key || owned.has(key)) continue; + const raw = readState>(name, projectId, routeId); + if (!raw || typeof raw !== "object") continue; + // 연동 = { detached: {누가거리: …}, grade: {…} } 두 겹 + const next = + state === "revetlink" + ? Object.fromEntries( + Object.entries(raw).map(([part, cells]) => [ + part, + cells && typeof cells === "object" ? move(cells as Record) : cells, + ]), + ) + : move(raw); + writeState(name, next, projectId, routeId); + } +} + +afterAnySwap((_detail, moves, owned) => moveIdleStates(moves, owned)); diff --git a/B06_Section/B06_Section_Swap_Bus.ts b/B06_Section/B06_Section_Swap_Bus.ts index 277ea26e8..4255493a1 100644 --- a/B06_Section/B06_Section_Swap_Bus.ts +++ b/B06_Section/B06_Section_Swap_Bus.ts @@ -21,7 +21,37 @@ export function onAnySwap(fn: DetailListener): () => void { return () => anyListeners.delete(fn); } +/** 알림 한 번 동안 조정창 맵이 옮긴 세션 칸(열쇠 글) — 뒤 차례(`afterAnySwap`)는 그 칸을 다시 안 옮김(415-3). */ +const moved = new Set(); +const afterListeners = new Set< + ( + detail: SectionDetailResponse, + moves: ReadonlyMap, + owned: ReadonlySet, + ) => void +>(); + +/** 조정창 맵이 세션 칸을 옮겼다고 적음(`_UI_Page_Link_Session`). */ +export function markMovedStates(keys: Iterable): void { + for (const key of keys) moved.add(key); +} + +/** 갈아끼움 알림이 다 돈 뒤 부름 — `owned` = 그 사이 조정창 맵이 옮긴 세션 칸. */ +export function afterAnySwap( + fn: ( + detail: SectionDetailResponse, + moves: ReadonlyMap, + owned: ReadonlySet, + ) => void, +): () => void { + afterListeners.add(fn); + return () => afterListeners.delete(fn); +} + /** 갈아끼운 쪽(`Section_Store`)이 부름. */ export function emitSwap(detail: SectionDetailResponse, moves: ReadonlyMap): void { + moved.clear(); anyListeners.forEach((fn) => fn(detail, moves)); + const owned = new Set(moved); + afterListeners.forEach((fn) => fn(detail, moves, owned)); } diff --git a/B06_Section/B06_Section_UI_Page_Link_Session.ts b/B06_Section/B06_Section_UI_Page_Link_Session.ts index 0ddc38301..bb8e83e9c 100644 --- a/B06_Section/B06_Section_UI_Page_Link_Session.ts +++ b/B06_Section/B06_Section_UI_Page_Link_Session.ts @@ -27,7 +27,7 @@ import { import { culvertCardState } from "./B06_Section_UI_Cross_View_Structure"; import type { PipeRevetChoice } from "../A00_Common/design/design_structure_pipe_calc"; import { renameChainageKeys } from "./logic/B06_Section_Station_Swap"; -import { onAnySwap } from "./B06_Section_Swap_Bus"; +import { markMovedStates, onAnySwap } from "./B06_Section_Swap_Bus"; import { readByKey, writeByKey } from "../A00_Common/b_page_state"; /** 세션에 담기는 측점별 조작값 한 겹 맵. */ @@ -65,6 +65,7 @@ export function moveSessionMaps(moves: ReadonlyMap): void { next.forEach((value, key) => map.values.set(key, value)); map.persist(); } + markMovedStates(latest.keys()); // 나머지 누가거리 칸은 `Station_Keys` 가 옮김(415-3) } // 상세 갈아끼움(어느 보기가 했든)마다 한 번 — 조정창 값을 옮긴 측점 자리로 diff --git a/resources/tester/helper_415_swap.mjs b/resources/tester/helper_415_swap.mjs index 238141fbe..61270a445 100644 --- a/resources/tester/helper_415_swap.mjs +++ b/resources/tester/helper_415_swap.mjs @@ -122,6 +122,10 @@ const snap = () => ({ m.writeCrossDesignChoice(P, R, 45.054, choice("left_cut")); // 관 측점 고침 m.writeState("shoulderext", { "@at": { "grid:1": 20 }, "grid:1": { left: 0.7 } }, P, R); // 사면 쉬프트 m.writeState("bermdepth", { "@at": { "grid:3": 60 }, "grid:3": { right: 0.4 } }, P, R); // 소단 깊이 + // 누가거리 조절(415-3) — 조정창 맵 없이(B05 만 연 페이지 · 새로고침) · 관 자리 옹벽 · 세월 · 연동 두 겹 · 격자 폭 + m.writeState("revetx", { "45.05:outlet": { x: 0.3 }, "20.00:inlet": { x: 0.1 } }, P, R); + m.writeState("fordadjust", { 45.05: { w: 1 } }, P, R); + m.writeState("revetlink", { detached: { 45.05: true }, grade: {} }, P, R); out.before = snap(); const stop = m.followSwaps( P, @@ -134,6 +138,11 @@ const snap = () => ({ m.useDesignStore(P, null).set("stations", list(50)); await new Promise((resolve) => setTimeout(resolve, 30)); out.pipeMoved = { ...snap(), toasts: globalThis.__toasts.length }; + out.adjusts = { + revetx: m.readState("revetx", P, R), + fordadjust: m.readState("fordadjust", P, R), + revetlink: m.readState("revetlink", P, R), + }; // 관 한 번 더(10 m) — 둘째 옮김도 그대로(첫 옮김 뒤 관 id 를 앎) m.writeState("pipe-ids", { next: 2, ids: [{ id: "pipe-s1", at: 55 }] }, P); m.useDesignStore(P, null).set("stations", list(55)); diff --git a/resources/tester/test_415_page_split.py b/resources/tester/test_415_page_split.py index f0504c1da..4229822c7 100644 --- a/resources/tester/test_415_page_split.py +++ b/resources/tester/test_415_page_split.py @@ -6,6 +6,7 @@ 소단 깊이 · 관 측점 고침 그대로 · 두 번 옮겨도) · 노선 입력이 바뀐 때만 넘김 규칙(대조). - H2 [저장] 도중 계산 줄 먹이기 멈춤 — 관 초안이 비어도 열 때 읽은 옛 관 목록으로 안 돎 · 맞춤 고리도 쉼 · 저장 끝 = 저장 목록 다시 읽음 · 새 자리 그대로(다시 셈 0). + - 415-3 누가거리 조절(옹벽 · 세월 · BOX · 집수 · 연동)도 관 새 자리로 — 조정창 맵이 안 선 페이지도. - 415-2 관 옮김 → 되돌리기 = 묶음 입력 · 공용 측점도 원래 · 다시하기 = 옮긴 자리. - M4 새 시설 측점 서버 미리보기 걷음 — 미리보기 요청 0(`test_273_2` · `test_318` 이 맡음) · 여기는 코드 자리. @@ -107,9 +108,10 @@ def test_H1_관_옮김_뒤_고침_그대로(h1): def test_H1_노선이_바뀌면_넘김_규칙(h1): routed = h1["routeMoved"] - # 대조 — 노선 입력이 바뀐 때 = 지반 유형만 넘기고 단면 유형 · 사면 쉬프트 · 소단 깊이 버림 + # 대조 — 노선 입력이 바뀐 때 = 넘김 규칙(단면 유형 버림 · 지반 유형 · 사면 쉬프트 · 소단 깊이 넘김 = 417) assert routed["choices"] == [[40, "soil", None], [55, "soil", None]] - assert routed["shoulder"] == [] and routed["berm"] == [] + assert routed["shoulder"] == [["grid:1", {"left": 0.7}]] + assert routed["berm"] == [["grid:3", {"right": 0.4}]] def test_H2_저장_도중_먹이기_멈춤(h2): @@ -150,3 +152,11 @@ def test_415_2_관_옮김_되돌리기_측점_원래(h2): assert undo["undone"]["input"] == base and undo["undone"]["stations"] == base assert undo["redone"]["input"] == undo["movedAgain"]["input"] assert undo["redone"]["stations"] == undo["movedAgain"]["stations"] + + +def test_415_3_누가거리_조절_관_새_자리로(h1): + # 조정창 맵이 안 선 페이지(B05 만 · 새로고침)도 관 측점 누가거리 조절이 새 자리로 · 격자 값 그대로 + adjusts = h1["adjusts"] + assert adjusts["revetx"] == {"50.00:outlet": {"x": 0.3}, "20.00:inlet": {"x": 0.1}} + assert adjusts["fordadjust"] == {"50.00": {"w": 1}} + assert adjusts["revetlink"] == {"detached": {"50.00": True}, "grade": {}}