diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts index 7f4c985d5..c006134a3 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts @@ -119,6 +119,7 @@ export async function saveRouteDraft( projectId: string, body: RouteSaveBody, ): Promise { + pipesBeforeSave.set(projectId, await savedPipes(projectId)); const saved = await replanRoute(projectId, body); useDesignStore(projectId, null).set("routeDraft", null); // 노선이 바뀌면 조회 결과 캐시는 옛 노선 것 — 추천은 새 노선의 출발점이라 다시 적는다. @@ -141,6 +142,15 @@ export function routeSaveSummary(saved: RouteReplanResponse, outside = 0): strin return `노선 저장 — ${parts.join(" · ")}`; } +type SavedPipe = { id?: string; chainage_m: number }; +/** 노선 저장 직전 저장 관(id · 옛 누가거리) — 저장 뒤 같은 id 의 새 자리로 조절을 옮김(초안으로 안 따라간 고침 · 417). */ +const pipesBeforeSave = new Map(); + +const savedPipes = (projectId: string): Promise => + fetchDetailPipePoints(projectId) + .then((res) => res.pipe_points) + .catch(() => []); + /** * 노선 저장 뒤 세션 횡단 초안(옛 노선 측점 열쇠 · 누가거리 열쇠 조절 · 417) → 새 노선으로 넘기고 그 고침을 저장 — * 초안을 통째로 지우지 않는다. 사라진 측점 고침은 버림(수를 돌려줌 · 서버 `cross_dropped` 와 한 줄로). @@ -154,18 +164,22 @@ export async function carryAfterRouteSave( const detail = await loadSectionDetail(projectId, toRouteId); useStationKeys(projectId, toRouteId, detail); // 관 열쇠 → 새 자리 — 세션 id(`pipe-s3`) = 관 장부 · 저장 관 id(`p8017`) = 서버가 옮긴 관 - const saved = await fetchDetailPipePoints(projectId) - .then((res) => res.pipe_points) - .catch(() => []); + const saved = await savedPipes(projectId); + const before = pipesBeforeSave.get(projectId) ?? []; + pipesBeforeSave.delete(projectId); + const newAt = (id: string | undefined): number | null => + saved.find((pipe) => id !== undefined && pipe.id === id)?.chainage_m ?? null; const pipeAt = (key: string): number | null => { const id = key.startsWith("pipe:") ? key.slice(5) : null; if (!id) return null; - if (id.startsWith("pipe-")) return pipeChainageOf(id, projectId); - return saved.find((pipe) => pipe.id === id)?.chainage_m ?? null; + return id.startsWith("pipe-") ? pipeChainageOf(id, projectId) : newAt(id); }; + // 옛 저장 관 자리에 남은 고침(초안으로 안 따라감) = 같은 관 id 의 새 자리 + const movedAt = (chainageM: number): number | null => + newAt(before.find((pipe) => Math.abs(pipe.chainage_m - chainageM) < 0.01)?.id); const outside = carryStationDrafts(projectId, fromRouteId, toRouteId, { quiet: true }) + - carryChainageStates(projectId, fromRouteId, toRouteId, pipeAt); + carryChainageStates(projectId, fromRouteId, toRouteId, { keyAt: pipeAt, movedAt }); // 옮긴 고침을 새 상세 설계 칸에 — 같은 [저장]의 횡단 패치가 실음(사면 쉬프트 · 조절 · 소단 깊이 · 417) withDraftAdjusts(detail, projectId, toRouteId); withCarriedEdits(detail, projectId, toRouteId); diff --git a/B06_Section/B06_Section_Station_Keys.ts b/B06_Section/B06_Section_Station_Keys.ts index 9b22107bc..483340861 100644 --- a/B06_Section/B06_Section_Station_Keys.ts +++ b/B06_Section/B06_Section_Station_Keys.ts @@ -380,6 +380,14 @@ function carriedValue(name: KeyedState, value: unknown): unknown { return Object.keys(picked).length ? picked : null; } +/** 노선 저장 때 시설 측점 풀이(`carryChainageStates`). */ +export interface CarryFacilities { + /** 옛 저장 시설 측점 누가거리 → 새 노선 그 시설 측점 누가거리(없으면 null). */ + movedAt?: (chainageM: number) => number | null | undefined; + /** 관 · 구조물 열쇠 → 새 노선 누가거리(없으면 null). */ + keyAt?: (key: string) => number | null | undefined; +} + /** 누가거리 열쇠 머리(`b` · 0.01 m 글 · `:역할`) — 조정창 맵 열쇠 꼴(`renameChainageKeys` 와 같음). */ const CHAINAGE_HEAD = /^(b?)(-?\d+(?:\.\d+)?)(:.*)?$/; @@ -387,16 +395,20 @@ const CHAINAGE_HEAD = /^(b?)(-?\d+(?:\.\d+)?)(:.*)?$/; * 노선 저장 뒤(새 노선 측점 열쇠를 세운 뒤) 누가거리 열쇠 칸(`ROUTE_CARRY_STATES` 중 측점 열쇠 칸 빼고)을 옛 노선 * → 새 노선 측점으로 옮김 — 옛 누가거리 → 옛 측점 열쇠 → 새 누가거리. 구조물 조절은 관 · 구조물 열쇠만 · * 못 옮긴 고침은 버림. 돌려주는 값 = 버린 측점 수(「노선 밖 고침」 · 측점 단위). - * `facilityAt` = 관 · 구조물 열쇠 → 새 노선 누가거리(관 id 규칙이 갈림 — 세션 `pipe-s3` · 계산 줄 `pipe:p8017`). + * 시설 풀이(`facilities`) = 옛 저장 시설 측점 → 새 노선 그 시설 측점(좌표 짝 · 초안으로 안 따라간 고침) · 관 · + * 구조물 열쇠 → 새 누가거리(관 id 규칙이 갈림 — 세션 `pipe-s3` · 계산 줄 `pipe:p8017`). */ export function carryChainageStates( projectId: string, fromRouteId: number, toRouteId: number, - facilityAt: (key: string) => number | null | undefined = () => null, + facilities: CarryFacilities = {}, ): number { + const facilityAt = facilities.keyAt ?? (() => null); const outside = new Set(); const moveAt = (state: string, text: string): string | null => { + const moved = facilities.movedAt?.(Number(text)); + if (moved !== null && moved !== undefined) return chainageKey(moved); const stable = keyAt(projectId, fromRouteId, Number(text)); const structural = stable.startsWith("pipe:") || stable.startsWith("structure:"); // 새 목록에 그 열쇠가 없으면 그 시설의 새 자리 · 없으면 같은 자리 측점(초안 측점 = 저장 측점) diff --git a/resources/tester/helper_346_3_route_save.cjs b/resources/tester/helper_346_3_route_save.cjs index 5ae056f50..43d6c9a6c 100644 --- a/resources/tester/helper_346_3_route_save.cjs +++ b/resources/tester/helper_346_3_route_save.cjs @@ -38,7 +38,10 @@ const STUBS = { }, "../B06_Section/B06_Section_Section_Store": { invalidateSectionDetail: (_p, routeId) => calls.push(`invalidate:${routeId ?? "all"}`), - loadSectionDetail: async (_p, routeId) => (calls.push(`load:${routeId}`), { tag: routeId }), + loadSectionDetail: async (_p, routeId) => ( + calls.push(`load:${routeId}`), + { tag: routeId, cross_sections: [] } + ), saveCachedCrossPatches: async (_p, routeId) => (calls.push(`crossSave:${routeId}`), 1), }, "../B06_Section/B06_Section_Station_Keys": { diff --git a/resources/tester/helper_417_route_carry.mjs b/resources/tester/helper_417_route_carry.mjs index b22080dc6..94b520113 100644 --- a/resources/tester/helper_417_route_carry.mjs +++ b/resources/tester/helper_417_route_carry.mjs @@ -142,9 +142,14 @@ m.useStationKeys( detailOf([section(0, "bp"), section(45, "irregular", pipe), section(60, "ep")]), ); m.writeState("revetx", { "45.00:outlet": wall }, P, R); -m.carryChainageStates(P, R, R3, (key) => (key === "pipe:pipe-s1" ? 52 : null)); +m.carryChainageStates(P, R, R3, { keyAt: (key) => (key === "pipe:pipe-s1" ? 52 : null) }); m.carryChainageStates(P, R, R4); out.splitResolved = raw("revetx", R3); out.splitDropped = raw("revetx", R4); +// 옛 저장 시설 측점(초안으로 안 따라간 고침) — 좌표 짝으로 새 시설 측점에 +const R5 = 11; +m.useStationKeyList(P, R5, list); +m.carryChainageStates(P, R, R5, { movedAt: (c) => (c === 45 ? 52 : null) }); +out.movedByPlace = raw("revetx", R5); process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_346_3_route_save_screen.py b/resources/tester/test_346_3_route_save_screen.py index edf2a86c1..0453c6973 100644 --- a/resources/tester/test_346_3_route_save_screen.py +++ b/resources/tester/test_346_3_route_save_screen.py @@ -94,7 +94,8 @@ def test_추천_적용은_as_initial_저장_뒤_다시_세움(out): body = out["recommendBody"] assert body["as_initial"] is True and body["initial"] == {"mode": "earthwork"} assert body["draft"]["nodes"] == [[1, 1], [9, 9]] - assert out["recommendCalls"] == ["overlay:on", "replan", "overlay:off"] # 잠금 막 하나 + # 잠금 막 하나 · 저장 직전 저장 관 떠 둠(417 — 저장 뒤 같은 관 id 새 자리로 조절 옮김) + assert out["recommendCalls"] == ["overlay:on", "pipes", "replan", "overlay:off"] assert out["recommendDraftsLeft"] == {"route": None, "pipes": None} # 새 초기값에서 다시 assert out["rebuilt"] == 1 diff --git a/resources/tester/test_417_route_carry.py b/resources/tester/test_417_route_carry.py index 9062fbcb1..00b164b3e 100644 --- a/resources/tester/test_417_route_carry.py +++ b/resources/tester/test_417_route_carry.py @@ -101,6 +101,8 @@ def test_조절은_관_따라_옮김_일반_측점_옹벽은_버림(out): def test_관_id_규칙이_갈려도_시설_자리로_옮김(out): assert out["splitResolved"] == {"52.00:outlet": {"x": 0.3, "d": None, "h": 1.5, "m": None}} assert out["splitDropped"] == {} # 풀이 없음 = 같은 자리 측점 없음 → 버림 + # 초안으로 안 따라간 옛 저장 시설 측점 고침 = 좌표 짝 시설 측점으로 + assert out["movedByPlace"] == {"52.00:outlet": {"x": 0.3, "d": None, "h": 1.5, "m": None}} def test_새_상세_설계_칸에_얹힘(out):