From f7d0f077f72859aef84f3278fa3d04bd2ad62576 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 15:21:01 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05=20=EB=B0=B0=EC=88=98=EC=9C=A0=EC=97=AD)?= =?UTF-8?q?:=20349-0i=20=EB=90=98=EB=8F=8C=EB=A6=AC=EA=B8=B0=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=85=B8=EC=84=A0=EC=97=90=20=EB=8F=8C?= =?UTF-8?q?=EC=95=84=EC=98=A4=EB=A9=B4=20=EC=A0=80=EC=9E=A5=EB=B6=84=20?= =?UTF-8?q?=EB=8B=A4=EC=8B=9C=20=EB=B0=9B=EA=B8=B0=20=E2=80=94=20=EB=82=A1?= =?UTF-8?q?=EC=9D=8C=20=EA=B7=B8=EB=8C=80=EB=A1=9C=20=EB=A8=B8=EB=AC=BE=20?= =?UTF-8?q?=EC=97=86=EC=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 노선 초안이 없어지면(되돌리기 · 버림) 저장 배수유역을 열 때와 같은 길로 조용히 다시 받음(계산 아님) · 그 답의 낡음대로 흐림 풂 또는 그대로 · [적용] 따라감 - 떠 있던 초안용 live 요청 끊음 · 자동도 3 s 셈 대신 저장분 · 답 올 때까지 흐림 유지 - 패널 받기 줄 — loadSaved 를 요청 함수로(열 때 · reload · 되돌리기가 같은 길) · 700줄 맞춤 - test_349_drainage_live_flow 되돌리기 장면(자동 · 수동 요청 중 · 저장분 낡음) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte --- B05_Profile/B05_Profile_UI_Drainage_Live.ts | 28 ++++++++++++- B05_Profile/B05_Profile_UI_Drainage_Panel.ts | 26 +++++++----- .../tester/helper_349_drainage_live_flow.cjs | 40 ++++++++++++++++++- .../tester/test_349_drainage_live_flow.py | 18 +++++++++ 4 files changed, 100 insertions(+), 12 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Drainage_Live.ts b/B05_Profile/B05_Profile_UI_Drainage_Live.ts index 1b581f947..9bbff9a3e 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Live.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Live.ts @@ -9,6 +9,8 @@ * 흐림 + 가운데 [업데이트]. 낡은 동안 제안 [적용]은 끔(두 모드 · 새 답이 오면 다시 켬). * - 다시 조작하면 처음으로 — 떠 있는 요청을 끊고(AbortController) 다시 흐림 또는 다시 3 s. * 묶음이 같은 노선 · 관으로 다시 돌면(입력 지문 같음) 그대로 둠 — 떠 있는 답을 받음. + * - 초안이 없어지면(되돌리기 · 버림) 저장 배수유역을 다시 받아(열 때와 같은 길 · 계산 아님) 그 답의 + * 낡음대로 — 떠 있던 초안용 답은 버림 · 자동도 3 s 셈 대신 저장분. * - 서버가 「낡음」으로 답하면(노선 [저장] 뒤 등) 같은 흐림 + [업데이트] · 자동이면 3 s 뒤. * - 답은 패널 요청 감싸기(`DetailRunner`, quiet)로 올림 — 옛 차례 · 서버가 그만둔 답은 버림. * - 덮개 · 잠금 없음 — 다른 칸 조작은 그대로. @@ -47,6 +49,8 @@ export interface DrainageLiveDeps { runner: DetailRunner; /** [적용] 답이 화면에 오른 뒤 — 세션 초안에 담음. */ rememberPipes: () => void; + /** 저장 배수유역 다시 받기(열 때와 같은 길 · 조용히) — 답을 화면에 올렸으면 true. */ + loadSaved: () => Promise; } export interface DrainageLive { @@ -98,6 +102,10 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive { let mode = readDrainageMode(); let route: unknown = null; let stale = false; + /** 마지막 단계에 노선 초안이 있었나 — 없어지면 저장분으로 돌아옴. */ + let drafted = false; + /** 저장분 다시 받기 답이 「낡음」이었나(`markStale` 이 세움). */ + let savedStale = false; let generation = 0; /** 마지막 단계 입력 지문 — 같으면 묶음이 다시 돈 것뿐. */ let key = ""; @@ -172,6 +180,20 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive { paint(); } + /** 초안이 없어짐 — 저장분을 다시 받아 그 답의 낡음대로(흐림은 답이 올 때까지 그대로). */ + function backToSaved(): void { + cancel(); + const sent = ++generation; + savedStale = false; // 답이 「낡음」이면 apply 가 markStale 로 세움 + paint(); + void deps.loadSaved().then((applied) => { + if (sent !== generation) return; + stale = !applied || savedStale; + if (stale && mode === "auto") timer = window.setTimeout(refresh, QUIET_MS); + paint(); + }); + } + function openSettings(): void { openModal({ title: "배수유역 계산", @@ -219,12 +241,16 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive { const next = inputKey(route, ctx.inputs.pipes); if (next === key) return null; key = next; - if (draftMakesStale(route) || stale) invalidate(); + const wasDraft = drafted; + drafted = draftMakesStale(route); + if (drafted || (stale && !wasDraft)) invalidate(); + else if (wasDraft) backToSaved(); return null; }, refresh, suggest, markStale() { + savedStale = true; if (stale) return; stale = true; if (mode === "auto" && !inflight) timer = window.setTimeout(refresh, QUIET_MS); diff --git a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts index d6ec5155a..1dbc5ccf5 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts @@ -409,13 +409,12 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra /** 저장된 관 지점(없으면 자동 배치)을 불러온다. 화면에 들어올 때 1회. 저장 전 조작분(세션 초안)이 * 있으면 그 관 목록으로 세운다 — B06 을 다녀와도 지정 · 옵션이 옛값으로 안 돌아감(21-10 흠 3). */ - const loadSaved = (): Promise => - run(() => { - const pending = readPendingPipes(projectId as string); - return pending !== null // 빈 목록 = 다 지움(관 0개 · 312) — 저장분으로 안 넘어감 - ? computeDetailBasins(projectId as string, pending) - : fetchDetailPipePoints(projectId as string); - }); + const loadSaved = () => { + const pending = readPendingPipes(projectId as string); + return pending !== null // 빈 목록 = 다 지움(관 0개 · 312) — 저장분으로 안 넘어감 + ? computeDetailBasins(projectId as string, pending) + : fetchDetailPipePoints(projectId as string); + }; /** 저장분까지 버리고 자동 배치로 되돌린다("초기화"). 화면만 되돌리면 다시 들어왔을 때 * 옛 관이 살아난다(2026-08-02 사용자 보고). */ @@ -426,7 +425,14 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra const analyze = batched(() => run(() => computeDetailBasins(projectId as string, pipeRequest()))); // 자동/수동 다시 셈(347-2) · 제안 목록(347-3 · [적용] = 관 초안으로 세부 유역 다시 나눔). - const live = createDrainageLive({ root, basinList, pipeRequest, runner, rememberPipes }); + const live = createDrainageLive({ + root, + basinList, + pipeRequest, + runner, + rememberPipes, + loadSaved: () => run(loadSaved, true), // 초안 버림 = 저장분 조용히(349-0i) + }); analyzeButton.addEventListener("click", () => void analyze()); deleteButton.addEventListener("click", () => void pipeEditor.deleteSelected()); autoButton.addEventListener("click", () => void resetPipes()); @@ -493,7 +499,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra showProgress(2 / 3, L("B05_Drainage_Status_Analyzing")); // B04가 확정해 둔 관 지점을 그대로 불러온다 — 저장분이 없으면 백엔드가 자동 배치를 준다. // 두 화면이 같은 파일을 보므로 B04에서 옮긴 관이 여기서도 같은 자리에 있다. - void loadSaved(); + void run(loadSaved); } catch (error) { if (sequence !== loadSequence) return; status.hidden = false; @@ -565,7 +571,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra void loadLayers(); }, reload() { - if (projectId && meta) void loadSaved(); + if (projectId && meta) void run(loadSaved); }, pipeChainages: () => pipeEditor.chainages(), movePipe(fromChainage, toChainage) { diff --git a/resources/tester/helper_349_drainage_live_flow.cjs b/resources/tester/helper_349_drainage_live_flow.cjs index afbc2db29..519020dd4 100644 --- a/resources/tester/helper_349_drainage_live_flow.cjs +++ b/resources/tester/helper_349_drainage_live_flow.cjs @@ -86,6 +86,7 @@ const flush = async () => { /* ---- 모듈 읽기(그 자리 트랜스파일 · 바깥 모듈은 가짜) ---- */ const state = new Map(); const detailCalls = []; +let modeRow = 1; // ⚙ 창에서 고를 줄 — 0 수동 · 1 자동 const stubs = { "@config/config_frontend": { API_BASE_URL: "/api" }, "@ui/ui_template_progress": { createProgressCircle: () => ({ root: el("progress") }) }, @@ -94,7 +95,7 @@ const stubs = { openModal: (options) => { const body = el("modal"); options.mount(body, () => undefined); - body.children[1].children[0].fire("change"); + body.children[modeRow].children[0].fire("change"); }, }, "../A00_Common/b_page_state": { @@ -144,6 +145,8 @@ async function main() { { chainage_m: 100, id: "a1", source: "stream", options: { pipe_diameter_mm: 1000 } }, ]; let applied = 0; + let savedLoads = 0; + let savedAnswer = null; let remembered = 0; let live; const status = el("status"); @@ -167,6 +170,11 @@ async function main() { pipeRequest: () => pipes, runner, rememberPipes: () => (remembered += 1), + // 저장분 다시 받기(349-0i) — 답은 시험이 정할 때 옴 + loadSaved: () => { + savedLoads += 1; + return runner.run(() => new Promise((resolve) => (savedAnswer = resolve)), true); + }, }); const [veil, busy] = viewport.children; const update = veil.children[0]; @@ -270,6 +278,36 @@ async function main() { answer(calls[5]); await flush(); out.autoSavedDone = look(); + + // 349-0i 되돌리기 — 자동: 끌기 → 낡음 → Ctrl+Z(초안 없어짐) = 3 s 셈 대신 저장분 + const savedOk = { basins: [], pipe_points: pipes, stale: null }; + stage(draft(20, 300)); + advance(1000); + stage(null); + out.autoUndoWaiting = { ...look(), savedLoads }; + advance(5000); + out.autoUndoNoFetch = look(); + savedAnswer(savedOk); + await flush(); + out.autoUndo = { ...look(), savedLoads }; + + // 수동: 끌기 → [업데이트] 요청 중 → Ctrl+Z = 초안용 답 끊고 저장분 · 흐림은 답 올 때까지 + modeRow = 0; + gear.fire("click"); + stage(draft(21, 310)); + update.fire("click"); + const draftCall = calls[calls.length - 1]; + stage(null); + out.undoWaiting = { ...look(), aborted: draftCall.aborted, savedLoads }; + savedAnswer(savedOk); + await flush(); + out.undo = { ...look(), savedLoads }; + // 저장분 자체가 낡음 = 흐림 · [업데이트] 그대로 + stage(draft(22, 320)); + stage(null); + savedAnswer({ basins: [], pipe_points: pipes, stale: "배수유역 낡음" }); + await flush(); + out.undoSavedStale = { ...look(), savedLoads, mode: state.get("drainage-mode") }; // 입력 지문 — 판 번호 · 관 차례는 안 봄 · 꼭짓점 · 관 자리(cm) · id 는 봄 const { inputKey } = load(path.join(B05, "logic", "B05_Profile_Drainage_Live.ts")); const a = { id: "a1", chainage_m: 100 }; diff --git a/resources/tester/test_349_drainage_live_flow.py b/resources/tester/test_349_drainage_live_flow.py index 06fa912b3..c3583ee28 100644 --- a/resources/tester/test_349_drainage_live_flow.py +++ b/resources/tester/test_349_drainage_live_flow.py @@ -7,6 +7,7 @@ - 노선 [저장] 뒤 서버 답이 「낡음」 = 수동은 흐림 + [업데이트] · 자동은 3 s 뒤 조용히. - ⚙ 자동: 흐림 없음 · 3 s 조용한 뒤 셈 · 그 사이 또 옮기면 처음부터 · [업데이트] 막 없음(349-0e). - 낡은 동안 제안 [적용] 끔(두 모드) · 새 답이 오면 다시 켬(349-0e). +- 되돌리기로 초안이 없어지면 저장분 다시 받기 · 그 답의 낡음대로 · 초안용 답 끊음 · 자동도 3 s 셈 대신(349-0i). """ import json @@ -103,6 +104,23 @@ def test_auto_mode(r): assert look(r["autoSavedDone"]) == state(False, False, False, 6) +def test_undo_back_to_saved(r): + # 자동 — 끌기 뒤 1 s 에 되돌리기: 3 s 셈 없음 · 저장분 하나 · 답 오면 낡음 풂 + assert look(r["autoUndoWaiting"]) == {**state(True, False, False, 6, True), "savedLoads": 2} + assert r["autoUndoNoFetch"]["fetches"] == 6 + assert look(r["autoUndo"]) == {**state(False, False, False, 6), "savedLoads": 2} + # 수동 — [업데이트] 요청 중 되돌리기: 초안용 요청 끊음 · 저장분 답 올 때까지 흐림 그대로 + waiting = r["undoWaiting"] + assert waiting["aborted"] and waiting["stale"] and waiting["blur"] and not waiting["busy"] + assert look(r["undo"]) == {**state(False, False, False, 7), "savedLoads": 3} + # 저장분 자체가 낡음 = 흐림 · [업데이트] · [적용] 꺼짐 그대로 + assert look(r["undoSavedStale"]) == { + **state(True, True, False, 7), + "savedLoads": 4, + "mode": "manual", + } + + def test_input_key(r): assert look(r["keys"]) == { "rev": True,