diff --git a/B05_Profile/B05_Profile_UI_Profile_History.ts b/B05_Profile/B05_Profile_UI_Profile_History.ts index 77210a805..932521a9d 100644 --- a/B05_Profile/B05_Profile_UI_Profile_History.ts +++ b/B05_Profile/B05_Profile_UI_Profile_History.ts @@ -19,6 +19,8 @@ * `pageHistory()` 로 쌓고 되돌리며 `onPageHistory` 로 되살림 · 단추 켜짐을 받음. 고름 · 관 id 장부 · * 종류 캐시 · 구조물 넘김 · 다른 페이지(b08 · b09) 칸은 조작이 아니라 사진 밖. * 놓은 뒤 · 칸 값 바꾼 뒤(change) 잠깐 기다려 한 번 더 찍음 — 쌓는 줄이 따로 없는 조작(횡단 카드 등)도 한 걸음. + * 열 때(349) — 세운 뒤 첫 손길(누름 · 키 · 칸 바꿈) 전까지는 찍어도 첫 사진을 다시 뜨기만 함: 열 때 쓰는 + * 초안(표준 횡단면 · 첫 계산 결과 · 되살린 노선 초안)은 걸음이 아니라 열 때 모습. * ========================================================================== */ /** 조작값 스냅샷 대상 세션 키 접두어. B05·B06은 한 페이지라 함께 뜬다(CLAUDE.md 5장). */ @@ -77,6 +79,8 @@ let pointerHeld = false; const releaseWaiters = new Set<() => void>(); let pointerWatched = false; let settleTimer: ReturnType | undefined; +/** 사용자 손길 수 — 사진첩마다 세운 때 값과 견줘 「열 때」를 가름. */ +let gestures = 0; /** 페이지 사진첩 — 마지막에 세운 것(종단도 패널이 감싼 것으로 바꿔 둠). */ let active: ProfileHistory | null = null; @@ -109,7 +113,10 @@ function watchPointer(): void { if (pointerWatched || typeof window === "undefined") return; if (typeof window.addEventListener !== "function") return; pointerWatched = true; - window.addEventListener("pointerdown", () => (pointerHeld = true), true); + const touched = (): void => void (gestures += 1); + window.addEventListener("pointerdown", () => ((pointerHeld = true), touched()), true); + window.addEventListener("keydown", touched, true); + window.addEventListener("change", touched, true); const release = (): void => { if (!pointerHeld) return; // 놓을 때 부르는 쪽(끌기 마무리 · 세션 쓰기)이 먼저 끝나게 한 틱 뒤. @@ -179,6 +186,8 @@ export function createProfileHistory(onRestore: () => void, limit = 100): Profil let restoring = false; /** 연속 조작 중에는 record()를 흘려보내고 resume()에서 한 번만 쌓는다. */ let paused = false; + /** 세운 때 손길 수 — 손길을 못 듣는 환경(창 없음)은 열 때가 없음. */ + const openedAt = pointerWatched ? gestures : -1; function apply(index: number): boolean { if (index < 0 || index >= stack.length) return false; @@ -207,6 +216,10 @@ export function createProfileHistory(onRestore: () => void, limit = 100): Profil } const snapshot = takeSnapshot(stack[cursor]); if (sameSnapshot(snapshot, stack[cursor])) return; + if (gestures === openedAt) { + stack[0] = snapshot; // 열 때 — 첫 사진만 다시 뜸 + return; + } stack.splice(cursor + 1); stack.push(snapshot); if (stack.length > limit) stack.shift(); diff --git a/resources/tester/test_346_4_profile_history.py b/resources/tester/test_346_4_profile_history.py index 57569eccf..9dbc09a8f 100644 --- a/resources/tester/test_346_4_profile_history.py +++ b/resources/tester/test_346_4_profile_history.py @@ -1,5 +1,7 @@ """346-4 되돌리기 앞일 — 사진 대상 = aislo:draft: 전부 · 누름 중간은 안 찍고 놓을 때 한 번 · 크기. +349 — 열기만(손길 없음) = 걸음 0 · 열 때 쓰는 초안은 첫 사진 · 조작 한 번 = 걸음 하나. + ./venv/Scripts/python.exe -m pytest resources/tester/test_346_4_profile_history.py -q """ @@ -45,6 +47,7 @@ const K = { for (const k of Object.values(K)) sessionStorage.setItem(k, "0"); let restored = 0; const h = createProfileHistory(() => (restored += 1)); +window.dispatchEvent(new Event("keydown")); // 손길 — 열 때가 끝남 for (const k of Object.values(K)) sessionStorage.setItem(k, "1"); h.record(); out.undo = h.undo(); @@ -77,6 +80,7 @@ sessionStorage.setItem(K.pipes, big("p")); globalThis.gc(); const before = process.memoryUsage().heapUsed; const h3 = createProfileHistory(() => {}); +window.dispatchEvent(new Event("keydown")); for (let i = 0; i < 100; i += 1) { sessionStorage.setItem("aislo:draft:alignment-draft:p1:7", JSON.stringify({ i })); h3.record(); @@ -86,6 +90,33 @@ out.heapMb = (process.memoryUsage().heapUsed - before) / 1024 / 1024; let steps = 0; while (h3.undo()) steps += 1; out.steps = steps; + +// 4. 열기만 — 메뉴 누름(세우기 전) · 열 때 초안 쓰기 · 되살림 찍기 · 놓은 뒤 기다림 = 걸음 0 +data.clear(); +sessionStorage.setItem(K.cross, "saved"); +window.dispatchEvent(new Event("pointerdown")); +const h4 = createProfileHistory(() => {}); +sessionStorage.setItem("aislo:draft:std-cross:p1", "std"); +sessionStorage.setItem(K.cross, "opened"); +sessionStorage.setItem(K.route, "kept"); +h4.record(); +window.dispatchEvent(new Event("pointerup")); +await new Promise((r) => setTimeout(r, 450)); +out.openCanUndo = h4.canUndo(); +// 조작 한 번 — 누름 · 계획고 고침 · 놓음 = 걸음 하나 · 되돌리면 열 때 모습 +window.dispatchEvent(new Event("pointerdown")); +sessionStorage.setItem("aislo:draft:alignment-draft:p1:7", "1"); +window.dispatchEvent(new Event("pointerup")); +await new Promise((r) => setTimeout(r, 450)); +out.editCanUndo = h4.canUndo(); +h4.undo(); +out.openState = { + grade: sessionStorage.getItem("aislo:draft:alignment-draft:p1:7"), + cross: sessionStorage.getItem(K.cross), + route: sessionStorage.getItem(K.route), + std: sessionStorage.getItem("aislo:draft:std-cross:p1"), +}; +out.oneStep = !h4.canUndo(); writeFileSync(process.argv[2], JSON.stringify(out)); """ @@ -126,3 +157,12 @@ def test_큰_칸_100단도_바뀐_칸만_늚(result) -> None: assert result["steps"] == 99 # 한도 100(첫 사진 포함) # 같은 글을 같이 쓰지 않으면 900 KB × 100 ≈ 90 MB(UTF-8 1바이트 글 기준 · 실제는 더) assert result["heapMb"] < 10, result["heapMb"] + + +def test_열기만은_걸음_0(result) -> None: + assert result["openCanUndo"] is False + + +def test_조작_한_번은_걸음_하나_되돌리면_열_때(result) -> None: + assert result["editCanUndo"] is True and result["oneStep"] is True + assert result["openState"] == {"grade": None, "cross": "opened", "route": "kept", "std": "std"}