From 6b227c4422c929e6a269417256acd2b035152ec5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 16:17:20 +0900 Subject: [PATCH] =?UTF-8?q?fix(=EA=B3=84=EC=82=B0=EB=AC=B6=EC=9D=8C):=2034?= =?UTF-8?q?9-0m=20=ED=99=94=EB=A9=B4=20=EB=90=98=EB=8F=8C=EB=A6=AC?= =?UTF-8?q?=EA=B8=B0=EC=97=90=EC=84=9C=EB=8F=84=20=EB=90=98=EB=8F=8C?= =?UTF-8?q?=EB=A6=BC=20=EA=B8=B8=20=E2=80=94=20=EC=B4=88=EC=95=88=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=3D=20=EB=AC=B6=EC=9D=8C=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=C2=B7=20=EB=90=98=EB=8F=8C=EB=A6=BC=20=3D=20?= =?UTF-8?q?=EC=B0=A8=EB=A1=80=20=EB=81=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 까닭: 화면 Ctrl+Z 는 페이지 사진첩이 세션 노선 초안 칸을 먼저 비우고 다리에 알림 → 묶음이 가게 칸만 보고 「초안 아니었음」으로 판정해 되돌림을 건너뜀 - 초안이었나 = 묶음이 쥔 노선 입력 · 같은 알림이 두 번 와도 한 차례(값이 같으면 차례 없음) - 되돌림 판정 = 그 차례가 끝난 뒤(ground 알림에 안 기댐) · 밀린 차례면 초안 전 상세를 그대로 둠 - 시험 test_349_0l 되돌리기를 화면 꼴로(칸 먼저 비움 · 알림 두 번) — 옛 코드는 restored 없음 · 다시 셈 1 로 재현 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ --- A00_Common/design/design_pipeline_page.ts | 56 +++++++++----------- resources/tester/helper_349_0l_undo_draw.cjs | 7 ++- resources/tester/test_349_0l_undo_draw.py | 1 + 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/A00_Common/design/design_pipeline_page.ts b/A00_Common/design/design_pipeline_page.ts index 770df3f6c..b341c573f 100644 --- a/A00_Common/design/design_pipeline_page.ts +++ b/A00_Common/design/design_pipeline_page.ts @@ -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 | null = null; + /** 초안을 버린 차례가 끝나 되돌림까지 마치는 약속(종단도는 그 뒤 저장 기준으로 — 349-0l). */ let dropping: Promise | 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 => { 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((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); + } }, }; diff --git a/resources/tester/helper_349_0l_undo_draw.cjs b/resources/tester/helper_349_0l_undo_draw.cjs index 7195522ff..b16e95478 100644 --- a/resources/tester/helper_349_0l_undo_draw.cjs +++ b/resources/tester/helper_349_0l_undo_draw.cjs @@ -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); diff --git a/resources/tester/test_349_0l_undo_draw.py b/resources/tester/test_349_0l_undo_draw.py index 37000bc56..88ded36ee 100644 --- a/resources/tester/test_349_0l_undo_draw.py +++ b/resources/tester/test_349_0l_undo_draw.py @@ -3,6 +3,7 @@ · 열기 → 노선 꼭짓점 끌기 → 되돌리기 → 횡단 보기 → 종단 보기 — 누가토량 · 측점 · 횡단 = 열기 값 · 진짜 종단도 낡음 판정 · crossPreview 예약을 거침 — 되돌린 뒤 다시 셈 0 · 낡음 0 · 묶음 기록에 되돌림 길(restored · 까닭) · 되돌리기 = 묶음 한 번 +· 되돌리기 = 화면 Ctrl+Z 꼴(사진첩이 세션 노선 초안 칸을 먼저 비움 · 알림 두 번 · 349-0m) 짝: `helper_349_0l_undo_draw.cjs`(실제 TS 를 Node 로 · 화면 부품 · 서버 창구만 가짜). """