diff --git a/A00_Common/b_page_state.ts b/A00_Common/b_page_state.ts index 13a72380..59904f2b 100644 --- a/A00_Common/b_page_state.ts +++ b/A00_Common/b_page_state.ts @@ -202,10 +202,55 @@ export function stateKey( return `${head}:${projectId}:${routeId}`; } -/** 세션 접근은 저장소가 막힌 브라우저(사생활 보호)에서 던진다 — 전부 조용히 넘긴다. */ +/** + * 어느 저장소에 둘지 — **화면 취향(`pref`)만 `localStorage`**, 나머지는 세션이다. + * + * 2026-09-07 확인 — 취향까지 세션에 있어 **탭을 새로 열거나 브라우저를 껐다 켤 때마다** + * 패널 높이·접힘이 초기화됐다(계획서에는 「PC 를 바꾸면」으로 적혀 있었으나 실제 불편은 + * 훨씬 잦았다). 취향은 설계 데이터가 아니므로 5장 「캐시=세션」 규칙에 걸리지 않는다 — + * **설계값·초안·계산 결과는 세션 그대로** 둔다(그 경계를 흐리지 말 것). + */ +function storageFor(key: string): Storage { + return key.startsWith("aislo:pref:") ? window.localStorage : window.sessionStorage; +} + +/** 위와 같은 판단을 **키만 들고 저장소를 직접 만지는 화면 공용 부품**에 내준다 + * (`ui_template_resizer`·`ui_template_overlay`·`ui_template_overlay_drag`). + * 그 부품들은 등록표에서 받은 키로 높이·열림·자리를 저장하는데, 여기를 안 거치면 + * 취향만 세션에 남아 탭을 새로 열 때마다 초기화된다. */ +export function storageOf(key: string): Storage { + return storageFor(key); +} + +/** + * 세션에 남아 있던 옛 취향을 **한 번에 브라우저 저장소로 옮긴다**(2026-09-07). + * + * 취향을 쓰는 화면 부품(높이 조절자·접이식 패널)은 키만 들고 저장소를 직접 만지므로 + * 읽는 자리마다 건져 올리는 방식으로는 **높이 값이 안 옮겨졌다**(실측: 접힘 4개는 옮겨지고 + * 높이 3개는 세션에 남음). 그래서 첫 로드 때 한 번 쓸어 옮긴다 — 쓰던 패널 크기가 + * 이 변경 때문에 초기화되지 않게 한다. 여러 번 불려도 안전하다. + */ +function liftPrefsToLocalStorage(): void { + try { + const stale = Object.keys(window.sessionStorage).filter((key) => key.startsWith("aislo:pref:")); + for (const key of stale) { + const value = window.sessionStorage.getItem(key); + if (value !== null && window.localStorage.getItem(key) === null) { + window.localStorage.setItem(key, value); + } + window.sessionStorage.removeItem(key); + } + } catch { + /* 저장소가 막힌 브라우저 — 취향은 이번 세션 동안 화면 메모리로만 산다. */ + } +} + +liftPrefsToLocalStorage(); + +/** 저장소 접근은 막힌 브라우저(사생활 보호)에서 던진다 — 전부 조용히 넘긴다. */ function readRaw(key: string): string | null { try { - return window.sessionStorage.getItem(key); + return storageFor(key).getItem(key); } catch { return null; } @@ -213,8 +258,9 @@ function readRaw(key: string): string | null { function writeRaw(key: string, value: string | null): void { try { - if (value === null) window.sessionStorage.removeItem(key); - else window.sessionStorage.setItem(key, value); + const store = storageFor(key); + if (value === null) store.removeItem(key); + else store.setItem(key, value); } catch { /* 무시 — 값은 화면 메모리에 남는다. */ } diff --git a/ui_template/ui_template_overlay.ts b/ui_template/ui_template_overlay.ts index 70c79471..fe396242 100644 --- a/ui_template/ui_template_overlay.ts +++ b/ui_template/ui_template_overlay.ts @@ -5,6 +5,7 @@ import { makePanelDraggable } from "./ui_template_overlay_drag"; // 워크플로 상태는 공용 창구 하나로 받는다 — 화면마다 따로 부르면 진입에서 같은 답을 // 두 번 받는다(2026-09-06 실측). 그 창구가 짧은 시간 동안 캐시한다. import { fetchWorkflowState } from "../A00_Common/b_workflow_nav"; +import { storageOf } from "../A00_Common/b_page_state"; const TITLE_OVERLAY_STATE_KEY = "frd_workflow_title_overlay_open"; const PROGRESS_OVERLAY_STATE_KEY = "frd_workflow_progress_overlay_open"; @@ -84,7 +85,7 @@ export function createWorkflowPanelHandle( } function readOpenState(key: string): boolean { - return sessionStorage.getItem(key) !== "false"; + return storageOf(key).getItem(key) !== "false"; } /** @@ -191,7 +192,7 @@ function createPanel( toggle.setAttribute("aria-label", toggle.title); toggle.setAttribute("aria-expanded", String(isOpen)); } - sessionStorage.setItem(storageKey, String(isOpen)); + storageOf(storageKey).setItem(storageKey, String(isOpen)); // 펼친 뒤에는 아래 공간이 모자랄 수 있다 — 열리는 방향을 다시 잡는다. if (dragHandle) requestAnimationFrame(() => dragHandle?.refresh()); onOpenChange?.(isOpen); diff --git a/ui_template/ui_template_overlay_drag.ts b/ui_template/ui_template_overlay_drag.ts index adb456bd..3dc6cdbb 100644 --- a/ui_template/ui_template_overlay_drag.ts +++ b/ui_template/ui_template_overlay_drag.ts @@ -11,6 +11,8 @@ * 자리는 세션에만 남긴다(5장 데이터 3층 — 화면 조작값은 캐시 몫). */ +import { storageOf } from "../A00_Common/b_page_state"; + const DRAG_THRESHOLD_PX = 4; /** 상단 공용 헤더 높이(--spacing-64) — 그 위로는 못 올라간다. */ const MIN_TOP_PX = 64; @@ -28,7 +30,7 @@ interface PanelPosition { } function readPosition(key: string): PanelPosition | null { - const raw = sessionStorage.getItem(key); + const raw = storageOf(key).getItem(key); if (!raw) return null; try { const parsed = JSON.parse(raw) as Partial; @@ -92,7 +94,7 @@ export function makePanelDraggable( root.style.bottom = "auto"; root.style.top = `${next.top}px`; } - sessionStorage.setItem(storageKey, JSON.stringify(next)); + storageOf(storageKey).setItem(storageKey, JSON.stringify(next)); } function swallowNextClick(): void { diff --git a/ui_template/ui_template_resizer.ts b/ui_template/ui_template_resizer.ts index cd788b09..717aec37 100644 --- a/ui_template/ui_template_resizer.ts +++ b/ui_template/ui_template_resizer.ts @@ -1,10 +1,11 @@ import "./ui_template_resizer.css"; +import { storageOf } from "../A00_Common/b_page_state"; /* ============================================================================= * ui_template_resizer.ts * 공통 스플리터 — 패널 경계를 끌어 크기를 조절한다. * - * 조절값은 `sessionStorage`에만 남긴다. 같은 브라우저 세션에서는 페이지를 오가도 + * 조절값은 등록표가 정한 저장소에 남긴다(취향은 브라우저, 나머지는 세션 — 2026-09-07). 같은 브라우저 세션에서는 페이지를 오가도 * 유지되고, 브라우저를 다시 열면 기본값으로 돌아간다(2026-08-01 사용자 지시). * 사용자별 영구 저장은 나중에 따로 검토한다. * ========================================================================== */ @@ -61,20 +62,20 @@ export function createPanelResizer(options: PanelResizerOptions): PanelResizer { function apply(size: number, persist: boolean): void { const next = clamp(size); target.style.setProperty(cssVar, `${Math.round(next)}px`); - if (persist && storageKey) sessionStorage.setItem(storageKey, String(Math.round(next))); + if (persist && storageKey) storageOf(storageKey).setItem(storageKey, String(Math.round(next))); onResize?.(next); } function restore(): void { if (!storageKey) return; - const saved = Number(sessionStorage.getItem(storageKey)); + const saved = Number(storageOf(storageKey).getItem(storageKey)); // 저장한 뒤 창 크기가 바뀌었을 수 있으니 복원할 때도 상·하한을 다시 씌운다. if (Number.isFinite(saved) && saved > 0) apply(saved, false); } function reset(): void { target.style.removeProperty(cssVar); - if (storageKey) sessionStorage.removeItem(storageKey); + if (storageKey) storageOf(storageKey).removeItem(storageKey); onResize?.(axis === "vertical" ? target.clientHeight : target.clientWidth); }