fix(계산묶음): 349-0m 화면 되돌리기에서도 되돌림 길 — 초안 여부 = 묶음 입력 · 되돌림 = 차례 끝

- 까닭: 화면 Ctrl+Z 는 페이지 사진첩이 세션 노선 초안 칸을 먼저 비우고 다리에 알림 → 묶음이 가게 칸만 보고 「초안 아니었음」으로 판정해 되돌림을 건너뜀
- 초안이었나 = 묶음이 쥔 노선 입력 · 같은 알림이 두 번 와도 한 차례(값이 같으면 차례 없음)
- 되돌림 판정 = 그 차례가 끝난 뒤(ground 알림에 안 기댐) · 밀린 차례면 초안 전 상세를 그대로 둠
- 시험 test_349_0l 되돌리기를 화면 꼴로(칸 먼저 비움 · 알림 두 번) — 옛 코드는 restored 없음 · 다시 셈 1 로 재현

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
This commit is contained in:
2026-10-16 16:17:20 +09:00
co-authored by Claude Opus 5.5
parent f38f6d7d70
commit 6b227c4422
3 changed files with 32 additions and 32 deletions
+25 -31
View File
@@ -26,7 +26,6 @@ import { createStationsStage } from "./logic/design_pipeline_stations";
import type {
FollowPatch,
PipelineInputs,
PipelineOutputs,
PipelineRunRecord,
PipelineStation,
RouteDraft,
@@ -229,9 +228,7 @@ function createPagePipeline(
let quietGeneration = -1;
/** 노선 초안 전 상세(종단도가 그리던 것) — 초안을 버리면 이것으로(다시 셈 없음 · 349-0k). */
let restoreTo: SectionDetailResponse | null = null;
let restoring = false;
/** 초안을 버린 차례 기록 · 되돌림이 끝나는 약속(종단도는 그 뒤 저장 기준으로 — 349-0l). */
let dropRun: Promise<PipelineRunRecord> | null = null;
/** 초안을 버린 차례가 끝나 되돌림까지 마치는 약속(종단도는 그 뒤 저장 기준으로 — 349-0l). */
let dropping: Promise<string> | null = null;
/** 노선 초안 전 가게 관 · 구조물 — 초안 동안 follow 가 새 측점을 가게에 씀 · 초안을 버리면 이것으로. */
let beforeDraft: Facilities | null = null;
@@ -319,29 +316,23 @@ function createPagePipeline(
const saved = event.kind === "stage" && (event.stage === "ground" || event.stage === "profile");
if (!(saved && event.generation === quietGeneration)) publish(event);
});
// 노선 초안을 버린 차례 — 저장 노선 측점이 나오면 초안 전 상세로 되돌림(열 때와 같은 길)
pipe.subscribe((event) => {
if (event.kind !== "stage" || event.stage !== "ground") return;
if (!restoring || event.generation !== quietGeneration) return;
restoring = false;
/** 노선 초안을 버린 차례 끝 — 저장 노선 측점으로 초안 전 상세에 되돌림(열 때와 같은 길) · 기록에 길 남김.
* 차례 끝에서 판정 — ground 를 건너뛰어도 · 사진첩 되돌리기가 가게 초안을 먼저 비워도(349-0m). */
const finishDrop = async (record: PipelineRunRecord): Promise<string> => {
const routeId = canon?.routeId;
const stations = (event.value as PipelineOutputs["ground"] | null)?.stations ?? [];
const saved = restoreTo;
restoreTo = null;
if (typeof routeId !== "number" || !stations.length || !options.onDraftDropped) return;
const drop = options.onDraftDropped;
const record = dropRun;
dropping = new Promise<string>((resolve) =>
queueMicrotask(
() => void drop(routeId, saved, stations).then(resolve, () => resolve("error")),
),
);
void dropping.then((why) =>
record?.then((run) =>
Object.assign(run, { restored: why === "saved" || why === "unchanged", restoreWhy: why }),
),
);
});
const stations = pipe.output("ground")?.stations ?? [];
let why: string = record.outcome; // 새 조작에 밀림 · 단계 실패 — 초안 전 상세는 그대로 둠
if (record.outcome === "done") {
why = "none";
if (typeof routeId === "number" && stations.length && options.onDraftDropped) {
const saved = restoreTo;
restoreTo = null;
why = await options.onDraftDropped(routeId, saved, stations).catch(() => "error");
}
}
Object.assign(record, { restored: why === "saved" || why === "unchanged", restoreWhy: why });
return why;
};
// 사용자 노선 조작 뒤 follow 가 다시 매긴 관 · 구조물 = 가게(목록 · 알약 · 횡단 보기가 바로 새 측점) ·
// 초안을 버리면 초안 전 값 · 열 때(start 차례)는 안 씀 — 알림 밖(틱 뒤)에서 씀(쓰기 고리 막음과 안 부딪힘)
pipe.subscribe((event) => {
@@ -439,7 +430,7 @@ function createPagePipeline(
canonChanged();
started = true;
restoreTo = null;
restoring = false;
dropping = null;
void pipe.start(inputsNow());
openGeneration = quietGeneration = pipe.generation;
};
@@ -491,20 +482,23 @@ function createPagePipeline(
saved: () => store.get("routeDraft"),
edited(edit: RouteDraftEdit | null) {
const stored = store.get("routeDraft");
// 묶음이 쥔 노선 초안 — 가게 칸은 사진첩 되돌리기가 먼저 비울 수 있음(349-0m)
const wasDraft = pipe.input("route") !== null;
let draft: RouteDraft | null = null;
if (edit) {
draft = { ...edit, rev: (stored?.rev ?? 0) + 1, terrain: analysisTerrain(projectId) };
if (stored && same({ ...stored, rev: 0 }, { ...draft, rev: 0 })) draft = stored;
}
if (draft && !stored && !restoring) restoreTo = savedSource().detail ?? null;
if (draft && !wasDraft) restoreTo ??= savedSource().detail ?? null;
store.set("routeDraft", draft);
if (!started) return start();
const before = pipe.output("route")?.line ?? null;
const after = draft ? draftLine(draft, plan?.min_radius_m ?? 0).line : savedLine();
const generation = pipe.generation;
const run = pipe.update("route", draft, routeRange(before, after));
restoring = !draft && !!stored && pipe.generation !== generation;
if (restoring) [quietGeneration, dropRun, dropping] = [pipe.generation, run, null];
if (run && !draft && wasDraft) {
quietGeneration = pipe.generation;
dropping = run.then(finishDrop);
}
},
};
+6 -1
View File
@@ -3,6 +3,7 @@
* 상세 가게(`B06_Section_Section_Store`). 화면 부품(DOM · SVG · 오버레이 · 도구줄)과 서버 창구만 가짜 ·
* 그리기는 진짜 `renderProfile` 의 낡음 판정 줄만 · 다시 셈 창구(`refreshCrossDesigns`)는 부른 수를 세고 면적을
* 바꿈(서버와 다른 값 흉내). 검증_sub7 노선(관 11 · 구조물 1 은 시험이 관 자리에 놓음).
* 되돌리기 = 화면 Ctrl+Z 꼴 — 사진첩이 세션 노선 초안 칸을 먼저 비운 뒤 다리에 알림(349-0m).
* 입력 argv[2] JSON · argv[3] 격자 이진. 결과 = 표준출력 JSON. 짝 = `test_349_0l_undo_draw.py`. */
const fs = require("fs");
const path = require("path");
@@ -208,6 +209,7 @@ const layouts = load("B06_Section", "B06_Section_Structure_Layouts.ts");
const profileData = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Data.ts");
const alignmentMod = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Alignment.ts");
const baseMod = load("A00_Common", "design", "design_profile_base.ts");
const pageState = load("A00_Common", "b_page_state.ts");
const massHaul = load("common_util", "common_util_mass_haul.ts");
const profileMod = load("A00_Common", "design", "design_profile.ts");
@@ -340,9 +342,12 @@ async function run() {
doubled(await live());
const drag = await measure(mark, refreshMark);
// 되돌리기 한 번(초안 버림)
// 되돌리기 한 번(초안 버림) — 화면 Ctrl+Z 꼴(349-0m): 페이지 사진첩이 세션 route-draft 칸을 먼저 되쓰고
// 편집기 되살림(fromPage → tell)이 다리에 null 을 알림 · 같은 알림이 한 번 더 와도 한 차례
mark = runs().length;
refreshMark = refreshCalls;
pageState.writeState("route-draft", null, P);
bridge.edited(null);
bridge.edited(null);
await quiet();
const undo = await measure(mark, refreshMark);
@@ -3,6 +3,7 @@
· 열기 → 노선 꼭짓점 끌기 → 되돌리기 → 횡단 보기 → 종단 보기 — 누가토량 · 측점 · 횡단 = 열기 값
· 진짜 종단도 낡음 판정 · crossPreview 예약을 거침 — 되돌린 뒤 다시 셈 0 · 낡음 0
· 묶음 기록에 되돌림 길(restored · 까닭) · 되돌리기 = 묶음 한 번
· 되돌리기 = 화면 Ctrl+Z 꼴(사진첩이 세션 노선 초안 칸을 먼저 비움 · 알림 두 번 · 349-0m)
짝: `helper_349_0l_undo_draw.cjs`(실제 TS 를 Node 로 · 화면 부품 · 서버 창구만 가짜).
"""