/* ============================================================================= * 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([ "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(); /** [저장] · [확정]이 끝난 때 — 지금 초안을 깨끗한 것으로 적음. */ 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([ROUTES.B05_PROFILE, ROUTES.B06_SECTION]); /** * 페이지를 세울 때 한 번 — `root` 가 화면에서 빠지면 스스로 풂. * `save` = [저장] 한 줄기(`saveSharedDesign(shared, false)`). */ export function guardDesignLeave( projectId: string, root: HTMLElement, save: () => Promise, ): 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 => { 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); }