fix(B05·B06): 노선 고친 [저장] 계획고 = 서버 옮김 위에 사용자 손댐만 (PLAN 498-2 · 요구 411~417)

- 브라우저 — 노선 바뀐 [저장]의 계획고 본문 = 묶음 계획고 대 저장분 옮김(같은 carryEdits · 이 초안 첫 기준)의 다른 열쇠 · 지운 열쇠만(`gradeTouch` · `routeGradeTouch`) · 못 가르면 옛 길(노선 0 칸 통째)
- 서버 — `alignment_cleared` 가 오면 관 · 구조물 · 추가 측점 차례 뒤 새 종단 편집(서버 옮김) 위에 손댐만 얹어 한 번 저장(`touched_alignment` · 얹기 실패도 되돌림 안)
- follow 옮김 측점 = stations 단계와 같은 추가 측점(`plus` 넘김)
- 재현: 꼬리 추가 측점 ▲ 뒤 앞 교점 끌기 [저장] = 990 m 편집 버림 · ▲ 잃음 · 최대 −0.50 m → 고친 뒤 버림 0 · 끌 때 = 저장 뒤
- 시험: test_498_2_grade_touch 새로 · helper_design_store_save 에 묶음 창구 가짜 하나(옛 길)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 21:51:29 +09:00
co-authored by Claude Opus 5.5
parent 76bb0c3fd3
commit b92fa06f34
10 changed files with 453 additions and 20 deletions
+13 -2
View File
@@ -19,7 +19,7 @@
* 초안이 있으면 측점을 냄(새로고침 · 다시 열기 = 초안 측점 · 466).
* 노선 초안을 버리면(되돌리기 · [저장 전으로]) 초안 전 상세 · 횡단 초안 칸 그대로(349-0k · 349-0o) · 가게 측점 ·
* 종단 = 그 상세 측점 · 저장 기준(466) · 그 뒤 같은 측점 목록은 가게에 다시 안 냄(상세 측점이 묶음과 달라도 · 349-0q).
* [저장](sub5)이 서버 정본 — 노선을 먼저 보내고 `pendingAlignment(DRAFT_ROUTE_ID)` 를 새 노선에 보냄.
* [저장]이 서버 정본 — 노선 바뀐 [저장]의 계획고 = 저장분 옮김 대 손댐만(`routeGradeTouch` · 서버가 자기 옮김 위에 · 498-2).
* ========================================================================== */
import { createDesignPipeline, type DesignPipeline } from "./logic/design_pipeline";
@@ -91,6 +91,7 @@ import {
createPlanlineStage,
gradeRange,
routeRange,
type GradeTouch,
} from "../../B05_Profile/logic/B05_Profile_Pipeline_Grade";
import { groundPviAt, profileAt } from "./design_calc_profile";
import {
@@ -127,6 +128,7 @@ interface PagePipeline {
detail(): SectionDetailResponse | null;
/** 노선 초안 버림 동안(그리기 멈춤 ~ 풂) — 저장 노선 낡음 다시 셈을 안 띄움(472-5 덧). */
holding(): boolean;
touch(): Promise<GradeTouch | null>;
commit(): void;
dispose(): void;
}
@@ -137,6 +139,9 @@ type Facilities = Pick<DesignStoreValues, "pipes" | "structures">;
const pages = new Map<string, PagePipeline>();
const same = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.stringify(b);
/** 노선 바뀐 [저장]의 계획고 손댐(498-2) — 묶음이 없거나 못 가르면 null(본문 = 노선 0 칸 통째 · 옛 길). */
export const routeGradeTouch = async (id: string) => (await pages.get(id)?.touch()) ?? null;
/** 편집기에 넘길 다리 — 그 프로젝트 묶음을 처음 부를 때 세움. */
export function routeDraftBridge(
projectId: string,
@@ -271,7 +276,12 @@ function createPagePipeline(
route.curves,
(patch.pipes ?? inputs.pipes ?? []) as PlanPipe[],
(patch.structures ?? inputs.structures ?? []) as PlanStructure[],
{ intervalM: options.stationIntervalM, types: enabled, ...snapOptions(inputs.route, plan) },
{
intervalM: options.stationIntervalM,
types: enabled,
...snapOptions(inputs.route, plan),
plus: patch.plusStations ?? inputs.plusStations ?? undefined, // stations 단계와 같게(498-2)
},
).map((station) => station.chainageM),
});
/** cross 서버 단계 맥락(511-B B-4) — 저장 노선 · 상세 · 시설 · 편집(`design_pipeline_cross_server`). */
@@ -678,6 +688,7 @@ function createPagePipeline(
ownsCross: Boolean(options.savedCross),
detail: () => draftDetailOf(projectId),
holding: () => holding,
touch: async () => (await pipe.settled(), followStage.touch(pageSaved.storedEdits(), pipe)),
commit: () => void (beforeDraft = null),
dispose() {
disposed = true;
@@ -82,6 +82,11 @@ export function createPageSaved(
savedSource,
canonChanged,
savedEdits,
/** 서버 저장분 계획고 편집(세션 초안 뺌) — 노선 바뀐 [저장]에서 서버가 옮기는 그 값(498-2). */
storedEdits(): AlignmentEdits {
const longitudinal = canonDetail()?.longitudinal;
return (longitudinal && readAlignment(longitudinal)?.edits) ?? emptyEdits();
},
storedPvi,
crossNow,
drafted,
+14 -8
View File
@@ -11,7 +11,7 @@
* 노선(346-3 · 6-B) — 노선 초안이 마지막 저장과 다르면 같은 본문에 `route`(계약 `RouteDraft`) · `carry`(옛 노선
* 세션 횡단 고침 원본)를 실음. 서버가 맨 앞에서 노선 저장(관 · 구조물 · 추가 측점 좌표 따라감 · 종횡단 · 계획고
* 이월) → 관 · 구조물 초안 다시 얹기 → 새 노선으로 위 차례 → 횡단 = 세션 고침을 측점 열쇠로 넘긴 patch(같은 TS ·
* Node). 이 때 조각은 기본 조각만(화면 몫 일 = 옛 노선 판이라 건너뜀) · 계획고 초안 = 노선 초안 칸 ?? 옛 노선 칸 ·
* Node). 이 때 조각은 기본 조각만(화면 몫 일 = 옛 노선 판이라 건너뜀) · 계획고 = 손댐만(서버 옮김 위에 · 498-2) ·
* 손 상단측은 안 보냄(요구 274) · [확정]도 [저장]으로(새 노선을 본 뒤 다시 [확정]) · 다 저장하면 페이지 다시 세움.
*
* ⚠ 차례를 바꾸면 결과가 갈린다 — 관 목록은 스냅샷이라 구간값(암거 옵션)보다 먼저, 상단측은
@@ -49,6 +49,8 @@ import {
} from "../../B05_Profile/B05_Profile_UI_RouteEdit_Apply";
import type { RouteSaveBody } from "../../B05_Profile/B05_Profile_Api_Replan";
import { designSignature, markDesignClean } from "./design_leave";
import { routeGradeTouch } from "./design_pipeline_page";
import type { GradeTouch } from "../../B05_Profile/logic/B05_Profile_Pipeline_Grade";
export interface DesignSaveContext {
projectId: string;
@@ -166,10 +168,12 @@ export function noticeDroppedEdits(saved: { dropped_edit_chainages?: number[] })
/** 노선 초안 동안의 계획고 초안 칸 — `design_pipeline_page` 의 `DRAFT_ROUTE_ID` 와 같은 값(sub7). */
const DRAFT_ROUTE_ID = 0;
/** 계획고 조각 — 노선이 바뀌면 노선 초안 칸(`DRAFT_ROUTE_ID`) ?? 옛 노선 칸의 초안(서버가 새 노선 번호로 고쳐
* 씀 · 측점이 없는 자리는 버리고 알림 · 213-2) · 저장 뒤 두 칸 다 비움. */
function alignmentPart(routeId: number, moved: boolean): SavePart | null {
const draft = (moved ? pendingAlignment(DRAFT_ROUTE_ID) : null) ?? pendingAlignment(routeId);
/** 계획고 조각 — 노선이 바뀌면 사용자 손댐(`touch` · 서버가 자기 옮김 위에 얹음 · 498-2) · 못 가르면 노선 초안
* 칸(`DRAFT_ROUTE_ID`) ?? 옛 노선 칸의 초안 통째(서버가 새 노선 번호로 고쳐 씀) · 측점이 없는 자리는 버리고 알림
* (213-2) · 저장 뒤 두 칸 다 비움. */
function alignmentPart(routeId: number, moved: boolean, touch: GradeTouch | null): SavePart | null {
const draft =
touch?.edits ?? (moved ? pendingAlignment(DRAFT_ROUTE_ID) : null) ?? pendingAlignment(routeId);
if (!draft) return null;
return {
body: { route_id: routeId, ...draft },
@@ -188,6 +192,7 @@ async function defaultPart(
routeId: number,
confirm: boolean,
moved: boolean,
touch: GradeTouch | null,
): Promise<SavePart | null> {
switch (key) {
case "settings": // 노선 뒤 — 노선 저장이 새로 만든 종단에도 경고 기준이 얹힘(448)
@@ -199,7 +204,7 @@ async function defaultPart(
case "plusStations": // 계획고 앞 — 추가 측점 자리의 계획고 편집이 버려지지 않게(368-2)
return plusStationsPart(projectId);
case "alignment":
return alignmentPart(routeId, moved);
return alignmentPart(routeId, moved, touch);
case "culvertOptions":
return culvertOptionsPart(stateKey("culvertopt", projectId, routeId));
case "uphill":
@@ -257,12 +262,13 @@ export async function saveDesign(
const parts: Partial<Record<SavePartKey, SavePart | null>> = {};
// 노선이 바뀌면 횡단 = 옛 노선 세션 고침 원본(서버가 새 측점에 넘김) · 화면 몫 조각은 옛 노선 판이라 안 씀
const carried = route ? routeCarryPart(ctx.projectId, routeId) : null;
const touch = route ? await routeGradeTouch(ctx.projectId) : null;
for (const key of SAVE_PART_KEYS) {
if (key === "cross") {
parts.cross = carried ? carried.cross : await ctx.crossPart(confirm);
continue;
}
const base = await defaultPart(key, ctx.projectId, routeId, confirm, !!route);
const base = await defaultPart(key, ctx.projectId, routeId, confirm, !!route, touch);
const own = route ? undefined : ctx.parts?.[key];
parts[key] = own ? await own(base, confirm) : base;
}
@@ -272,7 +278,7 @@ export async function saveDesign(
routeId,
confirm,
parts,
route ? { route, carry: carried?.carry } : {},
route ? { route, carry: carried?.carry, alignment_cleared: touch?.cleared } : {},
);
if (result.waited) showToast("앞 저장이 끝나기를 기다린 뒤 저장했습니다.", "info");
// 저장 도중 고침(가림막 동안 키 · 되돌리기 포함 · 505-2) — 조각 정리가 그 고침까지 비우므로 건너뜀(서버는 낸
+1 -1
View File
@@ -69,7 +69,7 @@ export async function postDesignSave(
routeId: number,
confirm: boolean,
parts: Partial<Record<SavePartKey, SavePart | null>>,
extra: { route?: unknown; carry?: unknown } = {},
extra: { route?: unknown; carry?: unknown; alignment_cleared?: unknown } = {},
): Promise<DesignSaveResult> {
const body: Record<string, unknown> = { route_id: routeId, confirm, ...extra };
for (const key of SAVE_PART_KEYS) {