diff --git a/A00_Common/design/design_save.ts b/A00_Common/design/design_save.ts index b5dd3183b..2edfd617b 100644 --- a/A00_Common/design/design_save.ts +++ b/A00_Common/design/design_save.ts @@ -220,6 +220,8 @@ export async function saveDesign( } continue; } + // 상단측은 옛 누가거리 열쇠 — 노선이 바뀌면 횡단 차례(`carryAfterRouteSave`)가 새 측점으로 옮긴 뒤 보냄(443-2) + if (step === "uphill" && routeSaved) continue; // 노선이 바뀌면 화면 몫 일(옛 노선 판 목록 · 계획고 판)은 건너뛰고 기본 창구만 새 노선으로. const own = routeSaved ? undefined : ctx.steps?.[step]; const base = defaultStep(step, ctx.projectId, routeId, fromRouteId); diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts index 0a78128db..e1dedb512 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts @@ -11,7 +11,7 @@ * 실패하면 서버가 옛 노선으로 되살리고 화면 초안은 그대로 남는다(오류 한 줄은 부르는 쪽). * ========================================================================== */ -import { clearDrafts, clearResults } from "../A00_Common/b_page_state"; +import { clearDrafts, clearResults, readState } from "../A00_Common/b_page_state"; import type { RouteDraft } from "../A00_Common/design/logic/design_pipeline_types"; import { useDesignStore } from "../A00_Common/design/design_store"; import { currentRoute, remountRoute } from "../A00_Common/router"; @@ -29,7 +29,7 @@ import { import { withCarriedEdits, withDraftAdjusts } from "../B06_Section/B06_Section_Section_Adjusts"; import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { pipeChainageOf } from "../A00_Common/design/design_pipe_ids"; -import { clearRouteLatestCache } from "./B05_Profile_Api_Fetch"; +import { clearRouteLatestCache, flushUphillOverrides } from "./B05_Profile_Api_Fetch"; import { replanRoute, type RouteReplanResponse, @@ -180,9 +180,16 @@ export async function carryAfterRouteSave( // 옛 저장 관 자리에 남은 고침(초안으로 안 따라감) = 같은 관 id 의 새 자리 const movedAt = (chainageM: number): number | null => newAt(before.find((pipe) => Math.abs(pipe.chainage_m - chainageM) < 0.01)?.id); + const uphillCount = (): number => Object.keys(readState("uphill", projectId) ?? {}).length; + const uphillBefore = uphillCount(); const outside = carryStationDrafts(projectId, fromRouteId, toRouteId, { quiet: true }) + carryChainageStates(projectId, fromRouteId, toRouteId, { keyAt: pipeAt, movedAt }); + // 상단측 = 새 측점으로 옮긴 것만 횡단 패치 앞에 보냄(443-2 · 옛 누가거리로 엉뚱한 측점에 안 얹힘) + const uphillDropped = uphillBefore - uphillCount(); + if (uphillDropped > 0) + showToast(`상단측 ${uphillDropped}곳 — 새 노선에 같은 측점이 없어 뺐습니다.`, "info"); + await flushUphillOverrides(projectId); // 옮긴 고침을 새 상세 설계 칸에 — 같은 [저장]의 횡단 패치가 실음(사면 쉬프트 · 조절 · 소단 깊이 · 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 e63b1f823..3ca4309c7 100644 --- a/B06_Section/B06_Section_Station_Keys.ts +++ b/B06_Section/B06_Section_Station_Keys.ts @@ -68,6 +68,7 @@ export const ROUTE_CARRY_STATES = { "detail-table": ["detail_table"], shoulderext: ["shoulder_extend_left_m", "shoulder_extend_right_m"], bermdepth: ["berm_depth_left_m", "berm_depth_right_m"], + uphill: [], // 상단측(측구 방향) — 프로젝트 칸 · 설계 칸 없음 · 노선 바뀐 저장 때 새 측점으로(443-2) } as const; /** 구조물 조절 칸 — 관 · 구조물 열쇠 측점만 따라감(서버 `carry_cross_inputs` 와 같음). */ const STRUCTURE_STATES = new Set([ diff --git a/resources/tester/helper_346_3_route_save.cjs b/resources/tester/helper_346_3_route_save.cjs index 43d6c9a6c..f23f3cafe 100644 --- a/resources/tester/helper_346_3_route_save.cjs +++ b/resources/tester/helper_346_3_route_save.cjs @@ -61,7 +61,10 @@ const STUBS = { fetchDetailPipePoints: async () => (calls.push("pipes"), { pipe_points: [] }), }, "../A00_Common/design/design_pipe_ids": { pipeChainageOf: () => null }, - "./B05_Profile_Api_Fetch": { clearRouteLatestCache: () => calls.push("latest:clear") }, + "./B05_Profile_Api_Fetch": { + clearRouteLatestCache: () => calls.push("latest:clear"), + flushUphillOverrides: async () => calls.push("uphill"), // 옮긴 뒤 · 횡단 패치 앞(443-2) + }, "./B05_Profile_Api_Replan": { replanRoute: async (_p, body) => { bodies.push(JSON.parse(JSON.stringify(body))); diff --git a/resources/tester/helper_443_2_entry.ts b/resources/tester/helper_443_2_entry.ts new file mode 100644 index 000000000..80b1803b4 --- /dev/null +++ b/resources/tester/helper_443_2_entry.ts @@ -0,0 +1,5 @@ +/* 443-2 시험 묶음 진입점 — 노선 바뀐 [저장] 때 상단측(uphill)을 측점 열쇠로 새 측점에 옮김. + * Node 헬퍼 = helper_443_2_uphill_carry.mjs · 짝 = test_443_2_uphill_carry.py. + */ +export { carryChainageStates, useStationKeyList } from "../../B06_Section/B06_Section_Station_Keys"; +export { readState, writeState } from "../../A00_Common/b_page_state"; diff --git a/resources/tester/helper_443_2_uphill_carry.mjs b/resources/tester/helper_443_2_uphill_carry.mjs new file mode 100644 index 000000000..aa1c00f4f --- /dev/null +++ b/resources/tester/helper_443_2_uphill_carry.mjs @@ -0,0 +1,61 @@ +/* 443-2 헬퍼 — vite ssr 로 묶은 진짜 모듈(helper_443_2_entry.ts)을 부르고 결과를 JSON 한 벌로. + * node helper_443_2_uphill_carry.mjs <묶음.mjs> + */ +import { pathToFileURL } from "node:url"; + +const store = () => { + const m = new Map(); + return { + getItem: (k) => (m.has(k) ? m.get(k) : null), + setItem: (k, v) => m.set(k, String(v)), + removeItem: (k) => m.delete(k), + key: (i) => [...m.keys()][i] ?? null, + get length() { + return m.size; + }, + clear: () => m.clear(), + }; +}; +globalThis.window = globalThis; +globalThis.sessionStorage = store(); +globalThis.localStorage = store(); +const any = new Proxy(function () {}, { + get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any), + apply: () => any, + construct: () => any, +}); +globalThis.document = any; +globalThis.requestAnimationFrame = (run) => setTimeout(run, 0); + +const m = await import(pathToFileURL(process.argv[2]).href); +const P = "p443"; +const grid = (pairs) => pairs.map(([key, chainageM]) => ({ key, chainageM })); +// 옛 노선 = 20 m 일반측점 넷 · 새 노선 = 같은 번호가 밀림(21.5 · 43) · grid:3(60 m) 사라짐 +m.useStationKeyList( + P, + 7, + grid([ + ["end:BP", 0], + ["grid:1", 20], + ["grid:2", 40], + ["grid:3", 60], + ]), +); +m.useStationKeyList( + P, + 8, + grid([ + ["end:BP", 0], + ["grid:1", 21.5], + ["grid:2", 43], + ]), +); +m.writeState("uphill", { "20.000": "right", "40.000": "left", "60.000": "right" }, P); +const outside = m.carryChainageStates(P, 7, 8); +const moved = m.readState("uphill", P); +// 세션이 없으면 그대로(빈 칸 안 만듦) +m.writeState("uphill", null, P); +const outsideEmpty = m.carryChainageStates(P, 7, 8); +process.stdout.write( + JSON.stringify({ outside, moved, outsideEmpty, empty: m.readState("uphill", P) }), +); diff --git a/resources/tester/test_346_3_route_save_screen.py b/resources/tester/test_346_3_route_save_screen.py index ece8870a4..527e91883 100644 --- a/resources/tester/test_346_3_route_save_screen.py +++ b/resources/tester/test_346_3_route_save_screen.py @@ -82,6 +82,7 @@ def test_초안_넘김_차례(out): "pipes", # 관 열쇠 → 새 자리(서버가 옮긴 관) "carry:7->9:quiet", "carryAt:7->9", # 417 — 조절 · 측점 폭 · 상세 표도 새 노선 측점으로 + "uphill", # 443-2 — 상단측도 새 측점으로 옮긴 뒤 · 횡단 패치 앞 "adjusts:9", "edits:9", # 옮긴 고침을 새 상세 설계 칸에 → 같은 [저장] 횡단 패치 "crossSave:9", diff --git a/resources/tester/test_443_2_uphill_carry.py b/resources/tester/test_443_2_uphill_carry.py new file mode 100644 index 000000000..098311b45 --- /dev/null +++ b/resources/tester/test_443_2_uphill_carry.py @@ -0,0 +1,77 @@ +"""PLAN 443-2 — 노선 바꾼 [저장]의 상단측(uphill). + +- 옛 누가거리 열쇠 상단측 = 측점 열쇠(옮김 목록 `ROUTE_CARRY_STATES` · 415 · 417 규칙)로 새 노선 측점에. +- 새 노선에 같은 측점이 없으면 뺌 · 수를 돌려줌(「노선 밖 고침」 · 상단측 알림). +- 저장 한 줄기: 노선을 저장했으면 상단측 차례는 건너뛰고 `carryAfterRouteSave` 가 옮긴 뒤 · 횡단 패치 앞에 보냄. +Node 헬퍼 `helper_443_2_uphill_carry.mjs` 가 vite ssr 로 묶은 진짜 모듈(`helper_443_2_entry.ts`)을 부름. + +./venv/Scripts/python.exe -m pytest resources/tester/test_443_2_uphill_carry.py -q +""" + +import json +import subprocess +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] + + +@pytest.fixture(scope="module") +def out(tmp_path_factory): + work = tmp_path_factory.mktemp("b443_2") + build = subprocess.run( + [ + "node", + str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"), + "build", + "--configLoader", + "runner", + "--ssr", + "../resources/tester/helper_443_2_entry.ts", + "--outDir", + str(work), + "--logLevel", + "error", + ], + cwd=ROOT, + capture_output=True, + text=True, + encoding="utf-8", + timeout=300, + ) + assert build.returncode == 0, build.stderr + bundle = next(work.glob("*.js"), None) or next(work.glob("*.mjs")) + proc = subprocess.run( + ["node", str(HERE / "helper_443_2_uphill_carry.mjs"), str(bundle)], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_상단측이_새_노선_같은_측점으로(out): + assert out["moved"] == {"21.50": "right", "43.00": "left"} + + +def test_못_맞춘_상단측은_빼고_수를_돌려줌(out): + assert out["outside"] == 1 + + +def test_세션_없으면_그대로(out): + assert out["outsideEmpty"] == 0 and out["empty"] is None + + +def test_노선_저장이면_상단측_차례는_옮긴_뒤(): + save = (ROOT / "A00_Common" / "design" / "design_save.ts").read_text("utf-8") + assert 'if (step === "uphill" && routeSaved) continue;' in save + apply = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Apply.ts").read_text("utf-8") + body = apply.split("export async function carryAfterRouteSave(")[1].split("\n}\n")[0] + carry = body.index("carryChainageStates(") + flush = body.index("await flushUphillOverrides(projectId)") + assert carry < flush < body.index("saveCachedCrossPatches(") # 옮긴 뒤 · 횡단 패치 앞 + assert "상단측 ${uphillDropped}곳" in body # 못 맞춤 알림 diff --git a/resources/tester/test_design_store_save.py b/resources/tester/test_design_store_save.py index ba80df0fa..900248884 100644 --- a/resources/tester/test_design_store_save.py +++ b/resources/tester/test_design_store_save.py @@ -102,8 +102,7 @@ def test_노선이_바뀌면_노선_먼저_뒤_차례는_새_노선(out): "plusStations", 'alignment:9:{"30.000":0.2}', "culvert:aislo:draft:culvertopt:p1:7", - "uphill", - "carry:7->9", + "carry:7->9", # 상단측은 넘김(carryAfterRouteSave)이 새 측점으로 옮긴 뒤 보냄(443-2) "summary:9:2", "toast:info", "done:false",