Merge remote-tracking branch 'origin/dev' into sub_laptop_8

This commit is contained in:
2026-10-16 11:51:48 +09:00
17 changed files with 1040 additions and 166 deletions
+54 -8
View File
@@ -10,10 +10,15 @@
*
* 저장하지 않는 것: 패널 접힘·높이·표시 토글 같은 **화면 배치 값**. 조작 이력이 아니라
* 보기 설정이라 되돌릴 대상이 아니다(`isLayoutKey`).
*
* 대상 = 초안 통(`aislo:draft:` 전부 — 노선 초안 · 계획고 · 관 · 구조물 · 횡단) + 옛 b05 · b06 키(346-4).
* 끌기 · 누르고 있기 중간은 안 찍음 — 누름이 남아 있는 동안 record() 는 미루고 놓을 때 한 번.
* 사진끼리 안 바뀐 값은 앞 사진 글을 같이 씀 — 칸마다 수백 KB × 100단이어도 바뀐 칸만 늘어남.
* ========================================================================== */
/** 조작값 스냅샷 대상 세션 키 접두어. B05·B06은 한 페이지라 함께 뜬다(CLAUDE.md 5장). */
const DATA_KEY_PREFIXES = ["b05:", "b05-", "b06:", "b06-", "aislo:draft:alignment-draft:"];
const DRAFT_PREFIX = "aislo:draft:";
const LEGACY_KEY_PREFIXES = ["b05:", "b05-", "b06:", "b06-"];
/** 조작이 아니라 보기 설정인 키 — 스냅샷에서 뺀다. */
function isLayoutKey(key: string): boolean {
@@ -27,20 +32,22 @@ function isLayoutKey(key: string): boolean {
}
function isDataKey(key: string): boolean {
return DATA_KEY_PREFIXES.some((prefix) => key.startsWith(prefix)) && !isLayoutKey(key);
if (key.startsWith(DRAFT_PREFIX)) return true;
return LEGACY_KEY_PREFIXES.some((prefix) => key.startsWith(prefix)) && !isLayoutKey(key);
}
/** 한 시점의 조작값 — 키 → 값(JSON 문자열). */
export type HistorySnapshot = Record<string, string>;
function takeSnapshot(): HistorySnapshot {
/** `previous` 와 같은 값은 그 글을 그대로 담음(메모리 한 벌). */
function takeSnapshot(previous?: HistorySnapshot): HistorySnapshot {
const snapshot: HistorySnapshot = {};
try {
for (let index = 0; index < sessionStorage.length; index += 1) {
const key = sessionStorage.key(index);
if (!key || !isDataKey(key)) continue;
const value = sessionStorage.getItem(key);
if (value !== null) snapshot[key] = value;
if (value !== null) snapshot[key] = previous?.[key] === value ? previous[key] : value;
}
} catch {
// 세션 저장소를 못 쓰는 환경에서는 되돌리기만 비활성이 된다.
@@ -55,6 +62,39 @@ function sameSnapshot(left: HistorySnapshot, right: HistorySnapshot): boolean {
return leftKeys.every((key) => left[key] === right[key]);
}
/** 누름(마우스 · 터치 · 펜)이 남아 있나 — 창 하나에 한 번 듣고 놓으면 미룬 기록을 부름. */
let pointerHeld = false;
const releaseWaiters = new Set<() => void>();
let pointerWatched = false;
function watchPointer(): void {
if (pointerWatched || typeof window === "undefined") return;
if (typeof window.addEventListener !== "function") return;
pointerWatched = true;
window.addEventListener("pointerdown", () => (pointerHeld = true), true);
const release = (): void => {
if (!pointerHeld) return;
// 놓을 때 부르는 쪽(끌기 마무리 · 세션 쓰기)이 먼저 끝나게 한 틱 뒤.
window.setTimeout(() => {
pointerHeld = false;
const waiters = [...releaseWaiters];
releaseWaiters.clear();
waiters.forEach((fn) => fn());
}, 0);
};
window.addEventListener("pointerup", release, true);
window.addEventListener("pointercancel", release, true);
window.addEventListener("blur", release);
// 창 밖에서 놓아 pointerup 을 못 받은 때 — 단추 없이 움직이면 놓은 것.
window.addEventListener(
"pointermove",
(event) => {
if (pointerHeld && event.buttons === 0) release();
},
true,
);
}
/** 스냅샷을 세션에 되쓴다 — 스냅샷에 없던 조작 키는 지운다. */
function restoreSnapshot(snapshot: HistorySnapshot): void {
try {
@@ -91,7 +131,8 @@ export interface ProfileHistory {
* `onRestore`는 세션 값이 바뀐 뒤 화면을 다시 세우는 콜백이다 — 세션이 정본이므로
* 여기서 각 패널이 자기 값을 다시 읽어 그린다.
*/
export function createProfileHistory(onRestore: () => void, limit = 50): ProfileHistory {
export function createProfileHistory(onRestore: () => void, limit = 100): ProfileHistory {
watchPointer();
const stack: HistorySnapshot[] = [takeSnapshot()];
let cursor = 0;
/** 복원 중에 들어오는 record()를 무시한다 — 복원이 새 이력을 만들면 redo가 사라진다. */
@@ -112,10 +153,14 @@ export function createProfileHistory(onRestore: () => void, limit = 50): Profile
return true;
}
return {
const history: ProfileHistory = {
record() {
if (restoring || paused) return;
const snapshot = takeSnapshot();
if (pointerHeld) {
releaseWaiters.add(history.record);
return;
}
const snapshot = takeSnapshot(stack[cursor]);
if (sameSnapshot(snapshot, stack[cursor])) return;
stack.splice(cursor + 1);
stack.push(snapshot);
@@ -128,11 +173,12 @@ export function createProfileHistory(onRestore: () => void, limit = 50): Profile
resume() {
if (!paused) return;
paused = false;
this.record();
history.record();
},
undo: () => apply(cursor - 1),
redo: () => apply(cursor + 1),
canUndo: () => cursor > 0,
canRedo: () => cursor < stack.length - 1,
};
return history;
}