fix(498-1 메모리): 평면 편집 키 듣개 둘을 캔버스 수명에 묶음 — 요구 298-3

- B05_Profile_UI_RouteEdit_StructureMarks · B05_Profile_UI_RouteEdit_Keys 의 창 keydown · keyup · blur 듣개를 listenWhileAlive 로 · 다음 키 누름 때만 떨어지던 detach 걷음
- 시험: test_298_3_page_dispose 두 칸 더함 · test_383_2 · helper_399 가 @ui 듣기 틀을 읽음
- 확인: 키 누름 없이 [저장] 다섯 번 GC 뒤 힙 39 → 123 → 205 → 205 → 206 → 206 MB(둘째부터 평탄)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 23:32:28 +09:00
co-authored by Claude Opus 5.5
parent fcb3c994b3
commit 5555be5e42
5 changed files with 21 additions and 22 deletions
+7 -11
View File
@@ -9,6 +9,7 @@
* 일은 끌기(`_Pointer`)와 같음. 걸음 · 결과 한 줄 셈은 `logic/B05_Profile_RouteEdit_KeyNudge`.
* ========================================================================== */
import { listenWhileAlive } from "@ui/ui_template_window_watch";
import { lastPointerWithin } from "../B06_Section/B06_Section_UI_Cross_Panel_Base";
import { HOLD_DELAY_MS, HOLD_INTERVAL_MS } from "./B05_Profile_UI_Profile_Edit";
import type { RouteEditPointerParams } from "./B05_Profile_UI_RouteEdit_Pointer";
@@ -109,14 +110,8 @@ export function bindRouteEditKeys(params: RouteEditPointerParams, dragging: () =
params.draw();
}
const detach = (): void => {
window.removeEventListener("keydown", onKeyDown, true);
window.removeEventListener("keyup", onKeyUp, true);
window.removeEventListener("blur", release);
};
function onKeyDown(event: KeyboardEvent): void {
if (!canvas.isConnected) return detach();
if (!canvas.isConnected) return;
if (!isArrowKey(event.key) || !lastPointerWithin(".b05-routeedit")) return; // B06 겹침 평면도 같음(436)
const target = event.target as HTMLElement | null;
if (target?.closest?.("input, select, textarea, [contenteditable='true']")) return;
@@ -147,14 +142,15 @@ export function bindRouteEditKeys(params: RouteEditPointerParams, dragging: () =
}
function onKeyUp(event: KeyboardEvent): void {
if (!canvas.isConnected) return detach();
if (!canvas.isConnected) return;
if (!hold || event.key !== hold.key) return;
event.preventDefault();
event.stopPropagation();
release();
}
window.addEventListener("keydown", onKeyDown, true);
window.addEventListener("keyup", onKeyUp, true);
window.addEventListener("blur", release);
// 창 듣기는 캔버스 수명에 묶음 — 다시 세운 옛 페이지를 안 붙듦(298-3)
listenWhileAlive(window, "keydown", canvas, onKeyDown, true);
listenWhileAlive(window, "keyup", canvas, onKeyUp, true);
listenWhileAlive(window, "blur", canvas, release);
}
@@ -15,6 +15,7 @@
* ========================================================================== */
import { pickPx } from "../A00_Common/touch_bridge";
import { listenWhileAlive } from "@ui/ui_template_window_watch";
import { draftProjectId } from "../A00_Common/design/design_store";
import {
NO_SELECTION,
@@ -365,13 +366,8 @@ export function bindStructureMarks(
holdKey = null;
finish();
};
const detach = (): void => {
window.removeEventListener("keydown", onKeyDown, true);
window.removeEventListener("keyup", onKeyUp, true);
window.removeEventListener("blur", release);
};
function onKeyDown(event: KeyboardEvent): void {
if (!canvas.isConnected) return detach();
if (!canvas.isConnected) return;
const escape = event.key === "Escape";
if ((!escape && !isArrowKey(event.key)) || !lastPointerWithin(".b05-routeedit")) return;
const target = event.target as HTMLElement | null;
@@ -392,12 +388,13 @@ export function bindStructureMarks(
}, HOLD_DELAY_MS);
}
function onKeyUp(event: KeyboardEvent): void {
if (!canvas.isConnected) return detach();
if (!canvas.isConnected) return;
if (event.key === holdKey) release();
}
window.addEventListener("keydown", onKeyDown, true);
window.addEventListener("keyup", onKeyUp, true);
window.addEventListener("blur", release);
// 창 듣기는 캔버스 수명에 묶음 — 다시 세운 옛 페이지를 안 붙듦(298-3)
listenWhileAlive(window, "keydown", canvas, onKeyDown, true);
listenWhileAlive(window, "keyup", canvas, onKeyUp, true);
listenWhileAlive(window, "blur", canvas, release);
return {
press(px, py) {
@@ -104,6 +104,8 @@ function loadTs(full) {
cache.set(full, box.exports);
const localRequire = (request) => {
if (STUBS[request]) return STUBS[request];
if (request.startsWith("@ui/"))
return loadTs(path.join(B05, "..", "ui_template", `${request.slice(4)}.ts`));
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
return require(request);
};
@@ -53,6 +53,8 @@ def test_옛_페이지_풀림_지금_페이지는_들음(out) -> None:
"scroll|resize",
), # 끌기 동안 pointer 듣기는 끝에 뗌
("B05_Profile/B05_Profile_UI_Panel_Settings.ts", "pointerup|keyup"),
("B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks.ts", "keydown|keyup|blur"),
("B05_Profile/B05_Profile_UI_RouteEdit_Keys.ts", "keydown|keyup|blur"),
],
)
def test_전역_듣기는_노드_수명으로만(rel: str, types: str) -> None:
+3 -1
View File
@@ -63,7 +63,9 @@ def test_이음_끌기_걸기_한_줄_창_가로채기() -> None:
pointer = _read("B05_Profile_UI_RouteEdit_Pointer.ts")
assert pointer.count("bindRouteEditKeys(") == 1
keys = _read("B05_Profile_UI_RouteEdit_Keys.ts")
assert 'window.addEventListener("keydown", onKeyDown, true)' in keys # 앞서 받음(capture)
assert (
'listenWhileAlive(window, "keydown", canvas, onKeyDown, true)' in keys
) # 앞서 받음(capture)
# 마지막 누름이 평면 안 — B06 겹침 평면도 같음(436)
assert 'lastPointerWithin(".b05-routeedit")' in keys
assert "event.preventDefault();\n event.stopPropagation();" in keys