fix(443-2 B05 저장): 노선 바꾼 저장의 상단측 = 측점 열쇠로 새 측점에 옮겨 횡단 패치 앞에 보냄 · 못 맞춘 것은 빼고 알림

- 옮김 목록(ROUTE_CARRY_STATES)에 uphill 줄 — 노선 저장 · 측점 갈아끼움 때 새 측점으로
- 노선을 저장했으면 상단측 차례 건너뜀 → carryAfterRouteSave 가 옮긴 뒤 보냄(옛 누가거리로 엉뚱한 측점에 안 얹힘)
- 새 노선에 같은 측점이 없으면 뺌 · 「상단측 N곳 … 뺐습니다」 알림 · 노선 밖 고침 수에도
- 시험 test_443_2_uphill_carry.py · 346-3 · 저장 차례 시험 기대값 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
This commit is contained in:
2026-10-18 18:54:13 +09:00
co-authored by Claude Opus 5.5
parent f291f10edd
commit b7af85fa98
9 changed files with 161 additions and 5 deletions
+2
View File
@@ -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);
@@ -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);
+1
View File
@@ -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<string>([
+4 -1
View File
@@ -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)));
+5
View File
@@ -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";
@@ -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) }),
);
@@ -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",
@@ -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 # 못 맞춤 알림
+1 -2
View File
@@ -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",