fix(B05·B06 한 페이지): 노선 바뀐 저장 때 관 조절 옮김 — 관 열쇠 규칙이 갈려도(세션 id ↔ 저장 관 id) 관의 새 자리로 (417-1 M2)

- carryChainageStates 에 시설 자리 풀이 — 세션 id = 관 장부 · 저장 관 id = 서버가 옮긴 관 · 없으면 같은 자리 측점
- 시험 test_417 관 id 갈림 · 346-3 차례(관 받기)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
This commit is contained in:
2026-10-18 12:48:40 +09:00
co-authored by Claude Opus 5.5
parent e304f4c8c3
commit 42af5add77
7 changed files with 57 additions and 2 deletions
+13 -1
View File
@@ -27,6 +27,8 @@ import {
useStationKeys,
} from "../B06_Section/B06_Section_Station_Keys";
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 {
replanRoute,
@@ -151,9 +153,19 @@ export async function carryAfterRouteSave(
invalidateSectionDetail(projectId, toRouteId);
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 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;
};
const outside =
carryStationDrafts(projectId, fromRouteId, toRouteId, { quiet: true }) +
carryChainageStates(projectId, fromRouteId, toRouteId);
carryChainageStates(projectId, fromRouteId, toRouteId, pipeAt);
// 옮긴 고침을 새 상세 설계 칸에 — 같은 [저장]의 횡단 패치가 실음(사면 쉬프트 · 조절 · 소단 깊이 · 417)
withDraftAdjusts(detail, projectId, toRouteId);
withCarriedEdits(detail, projectId, toRouteId);
+9 -1
View File
@@ -387,20 +387,28 @@ const CHAINAGE_HEAD = /^(b?)(-?\d+(?:\.\d+)?)(:.*)?$/;
* 노선 저장 뒤(새 노선 측점 열쇠를 세운 뒤) 누가거리 열쇠 칸(`ROUTE_CARRY_STATES` 중 측점 열쇠 칸 빼고)을 옛 노선
* → 새 노선 측점으로 옮김 — 옛 누가거리 → 옛 측점 열쇠 → 새 누가거리. 구조물 조절은 관 · 구조물 열쇠만 ·
* 못 옮긴 고침은 버림. 돌려주는 값 = 버린 측점 수(「노선 밖 고침」 · 측점 단위).
* `facilityAt` = 관 · 구조물 열쇠 → 새 노선 누가거리(관 id 규칙이 갈림 — 세션 `pipe-s3` · 계산 줄 `pipe:p8017`).
*/
export function carryChainageStates(
projectId: string,
fromRouteId: number,
toRouteId: number,
facilityAt: (key: string) => number | null | undefined = () => null,
): number {
const outside = new Set<string>();
const moveAt = (state: string, text: string): string | null => {
const stable = keyAt(projectId, fromRouteId, Number(text));
const structural = stable.startsWith("pipe:") || stable.startsWith("structure:");
// 새 목록에 그 열쇠가 없으면 그 시설의 새 자리 · 없으면 같은 자리 측점(초안 측점 = 저장 측점)
const toAt =
stable.startsWith("at:") || (STRUCTURE_STATES.has(state) && !structural)
? null
: chainageOfKey(projectId, toRouteId, stable);
: chainageOfKey(
projectId,
toRouteId,
stable,
(structural ? facilityAt(stable) : null) ?? Number(text),
);
if (toAt === null) outside.add(stable);
return toAt === null ? null : chainageKey(toAt);
};
@@ -54,6 +54,10 @@ const STUBS = {
withDraftAdjusts: (_d, _p, routeId) => calls.push(`adjusts:${routeId}`),
withCarriedEdits: (_d, _p, routeId) => calls.push(`edits:${routeId}`),
},
"../B04_PreProcess/B04_PreProcess_Api_Fetch": {
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_Replan": {
replanRoute: async (_p, body) => {
+1
View File
@@ -4,6 +4,7 @@
export {
carryChainageStates,
carryStationDrafts,
useStationKeyList,
useStationKeys,
} from "../../B06_Section/B06_Section_Station_Keys";
export {
@@ -123,4 +123,28 @@ const pick = (entry) =>
out.input40 = pick(source?.input.byKey["grid:2"]);
out.input20 = pick(source?.input.byKey["grid:1"]);
/* 5. 관 id 규칙이 갈림(옛 목록 세션 id · 새 목록 저장 관 id) — 시설 자리 풀이로 옮김 · 없으면 버림 */
const R3 = 9;
const R4 = 10;
const list = [
{ key: "end:BP", chainageM: 0 },
{ key: "grid:1", chainageM: 20 },
{ key: "grid:2", chainageM: 40 },
{ key: "pipe:p9", chainageM: 52 },
{ key: "end:EP", chainageM: 60 },
];
m.useStationKeyList(P, R3, list);
m.useStationKeyList(P, R4, list);
m.writeState("pipe-ids", { next: 2, ids: [{ id: "pipe-s1", at: 45 }] }, P);
m.useStationKeys(
P,
R,
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, R4);
out.splitResolved = raw("revetx", R3);
out.splitDropped = raw("revetx", R4);
process.stdout.write(JSON.stringify(out));
@@ -79,6 +79,7 @@ def test_초안_넘김_차례(out):
"invalidate:9",
"load:9",
"keys:9:9",
"pipes", # 관 열쇠 → 새 자리(서버가 옮긴 관)
"carry:7->9:quiet",
"carryAt:7->9", # 417 — 조절 · 측점 폭 · 상세 표도 새 노선 측점으로
"adjusts:9",
+5
View File
@@ -98,6 +98,11 @@ def test_조절은_관_따라_옮김_일반_측점_옹벽은_버림(out):
assert out["chainageOutside"] == 1 # 40 m 옹벽 조절(일반 측점)
def test_관_id_규칙이_갈려도_시설_자리로_옮김(out):
assert out["splitResolved"] == {"52.00:outlet": {"x": 0.3, "d": None, "h": 1.5, "m": None}}
assert out["splitDropped"] == {} # 풀이 없음 = 같은 자리 측점 없음 → 버림
def test_새_상세_설계_칸에_얹힘(out):
d40, d47 = out["design40"], out["design47"]
assert d40["shoulder_extend_left_m"] == 0.5 and d40["berm_depth_right_m"] == 1.2