Files
Aislo/A00_Common/design/design_leave.ts
T
eomsangdonandClaude Opus 5.5 a8ad867468 feat(B05·B06 떠남): 346-4 떠남 확인 — 저장 안 한 조작이면 탭 닫기 · 다른 페이지 때 공용 물음
- A00_Common/design/design_leave.ts — 고침 칸(초안 통 − 고름 · 관 id 장부 · 종류 캐시 · 구조물 넘김 · 표준 횡단면 · b08 · b09) 지문 · 모든 노선
- 횡단 초안은 [저장] 뒤에도 남음 — 저장 · 확정 끝 깨끗한 표(markDesignClean)와 다를 때만 물음 · [초기화]는 초안을 다 지움
- B05 ↔ B06 이동은 안 묻고 다시 걺 · [버리고 넘어감] = 고침 칸만 지움 · 저장 실패 = 머묾
- design_page 한 줄(걺) · design_shared 한 줄(저장 끝 표) · 문구 B05_Design_Leave · b07-extent 묶음 다시 지음
- 시험 test_346_4_design_leave.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
2026-10-16 11:43:19 +09:00

124 lines
4.9 KiB
TypeScript

/* =============================================================================
* A00_Common/design/design_leave.ts
* B05 · B06 떠남 확인(346-4) — 저장 안 한 조작(초안 통)이 있으면 탭 닫기 · 다른 페이지로 갈 때
* 공용 물음(`ui_template_leave_dialog` · [취소] [버리고 넘어감] [저장 후 넘어감]).
*
* 고침 = 초안 통 칸 중 고침이 아닌 칸(고름 · 관 id 장부 · 종류 캐시 · 고른 구조물 넘김) ·
* 다른 페이지 칸(b08 · b09) · 저장 뒤에도 남기는 표준 횡단면(std-cross)을 뺀 것.
* 횡단 초안은 [저장] 뒤에도 세션에 남음 — 그래서 「있음」이 아니라 **마지막 깨끗한 때와 다름**으로 봄:
* [저장] · [확정]이 끝나면 `markDesignClean` 이 그때 지문을 적음 · [초기화]는 초안을 다 지우고 새로 읽음.
* B05 ↔ B06 는 한 페이지 — 그 사이 이동은 묻지 않음.
* 프로젝트 바꿈은 이 페이지를 떠난 뒤라 이 물음이 먼저 · 들어갈 때 물음은 `b_project_switch`.
* ========================================================================== */
import { ROUTES } from "@config/config_frontend";
import { leaveOrStay } from "@ui/ui_template_leave_dialog";
import { t as L } from "@ui/ui_template_locale";
import {
clearState,
namesInBucket,
readByKey,
routeStateKeys,
stateKey,
type StateName,
} from "../b_page_state";
import { currentRoute, setLeaveGuard } from "../router";
/** 초안 통이지만 고침으로 안 보는 칸 — 묻지 않고 [버리고 넘어감]에도 안 지움. */
const NOT_EDITS = new Set<StateName>([
"selection",
"pipe-ids",
"structure-pick",
"structure-type-cache",
"std-cross",
]);
const OTHER_PAGE = /^b0[89]-/;
export function editDraftNames(): StateName[] {
return namesInBucket("draft").filter((name) => !NOT_EDITS.has(name) && !OTHER_PAGE.test(name));
}
const filled = (raw: string | null): raw is string =>
raw !== null && raw !== "" && raw !== "{}" && raw !== "[]";
/** 고침 칸 지문 — 모든 노선 것 · 빈 칸 뺌 · 고침이 없으면 "". */
export function editSignature(projectId: string): string {
const parts: string[] = [];
for (const name of editDraftNames()) {
const own = stateKey(name, projectId);
const keys = own ? [own] : routeStateKeys(name, projectId);
for (const key of keys.sort()) {
const raw = readByKey(key);
if (filled(raw)) parts.push(`${key}=${raw}`);
}
}
return parts.join("\n");
}
/** 긴 지문을 들고 있지 않게 — FNV-1a 32비트 + 길이. */
function digest(text: string): string {
let hash = 0x811c9dc5;
for (let index = 0; index < text.length; index += 1) {
hash ^= text.charCodeAt(index);
hash = Math.imul(hash, 0x01000193);
}
return `${text.length}:${hash >>> 0}`;
}
// ponytail: 깨끗한 표는 메모리만 — [저장] 뒤 새로고침하면 남은 횡단 초안 때문에 한 번 더 물음.
const cleanMarks = new Map<string, string>();
/** [저장] · [확정]이 끝난 때 — 지금 초안을 깨끗한 것으로 적음. */
export function markDesignClean(projectId: string): void {
if (projectId) cleanMarks.set(projectId, digest(editSignature(projectId)));
}
/** 저장 안 한 조작이 있나. */
export function designDirty(projectId: string): boolean {
if (!projectId) return false;
const signature = editSignature(projectId);
if (!signature) return false;
return cleanMarks.get(projectId) !== digest(signature);
}
/** [버리고 넘어감] — 고침 칸만 지움(모든 노선). */
export function discardDesignEdits(projectId: string): void {
editDraftNames().forEach((name) => clearState(name, projectId));
}
const SAME_PAGE = new Set<string>([ROUTES.B05_PROFILE, ROUTES.B06_SECTION]);
/**
* 페이지를 세울 때 한 번 — `root` 가 화면에서 빠지면 스스로 풂.
* `save` = [저장] 한 줄기(`saveSharedDesign(shared, false)`).
*/
export function guardDesignLeave(
projectId: string,
root: HTMLElement,
save: () => Promise<unknown>,
): void {
const unload = (event: BeforeUnloadEvent): void => {
if (!root.isConnected) return window.removeEventListener("beforeunload", unload);
if (!designDirty(projectId)) return;
event.preventDefault();
event.returnValue = "";
};
window.addEventListener("beforeunload", unload);
const guard = async (): Promise<boolean> => {
if (!root.isConnected) return true;
if (SAME_PAGE.has(currentRoute())) {
// 라우터가 「넘어감」 뒤 물음을 내림 — 같은 페이지 안 이동이면 한 틱 뒤 다시 걺.
window.setTimeout(() => root.isConnected && setLeaveGuard(guard), 0);
return true;
}
if (!designDirty(projectId)) return true;
const got = await leaveOrStay(L("B05_Design_Leave"), {
save,
stillDirty: () => designDirty(projectId),
discard: () => discardDesignEdits(projectId),
});
return got !== "stay";
};
setLeaveGuard(guard);
}