지움
- flushDesignStore(design_store · 부름 0) — 시험 344-4 의 flush 대조 한 줄도
- b_page_state 옛 세션 열쇠 옮김(legacy · migrate) · b_project_switch 옛 키 머리
— 옛 열쇠는 세션 저장소(탭 닫으면 사라짐) · 지금 코드가 안 씀 · storage · DB 에 없음
· 시험 290-1 · 401 · 384-1 의 옛 키 줄 뺌
- 시험만 부르던 TS — planFootprint(346-2 거울은 footprintRecord + slopePieces 로 그대로)
· shiftSegment · shiftMovingPoints(쉬프트 단추는 09-02 지움 · min_cover_guard 쉬프트 시험 뺌)
· dropStationsNear(test_b05_station_merge.mjs 지움) · createIrregularStationsSection(부름 0)
· generalStations(234-1 은 기본 맞춤 값으로 같은 것 잼) · sameSummaryItem
- test_b05_b06_save_roundtrip + helper_b05_save_payload 둘 — 프로젝트 936be972 storage 없어 늘 skip
· 같은 저장 왕복은 test_472_6_design_save(창구 원자성) · test_449_save_pipes(관 PUT)
· test_238_1_save_keeps_structure_stations(구조물 측점)가 잼
남김
- migrateLegacyStations — 부르는 곳 있음(design_view_profile_sections) · storage 에 비정규 측점 남음
- common_util_excavation 셋 — 지금은 운영이 부름(design_calc_node)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
215 lines
8.0 KiB
JavaScript
215 lines
8.0 KiB
JavaScript
/* 401 프로젝트마다 따로 동작 — 진짜 TS(b_page_state · b_project_switch · b_asset_cache)를 가짜 저장소 ·
|
|
* 가짜 IndexedDB 위에서 돌려 JSON 으로 냄. 탭 둘 = 세션 둘 · localStorage 하나 · 모듈 한 벌씩. */
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
|
const { makeIdb } = require("./helper_343_3_asset_cache.cjs");
|
|
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
|
|
function fakeStorage() {
|
|
const map = new Map();
|
|
return {
|
|
get length() {
|
|
return map.size;
|
|
},
|
|
key: (i) => [...map.keys()][i] ?? null,
|
|
getItem: (k) => (map.has(k) ? map.get(k) : null),
|
|
setItem: (k, v) => map.set(k, String(v)),
|
|
removeItem: (k) => map.delete(k),
|
|
dump: () => Object.fromEntries(map),
|
|
};
|
|
}
|
|
|
|
const shared = { localStorage: fakeStorage(), idb: makeIdb() };
|
|
const router = { route: "/b07-design-detail", guard: null, sets: 0 };
|
|
let choice = "save";
|
|
const asked = [];
|
|
|
|
/** 탭 하나 = 창 하나 · 모듈 한 벌(모듈 메모리 따로). */
|
|
function openTab() {
|
|
const win = new EventTarget();
|
|
win.sessionStorage = fakeStorage();
|
|
win.localStorage = shared.localStorage;
|
|
win.indexedDB = shared.idb.indexedDB;
|
|
win.setTimeout = setTimeout;
|
|
win.reloads = 0;
|
|
win.location = { reload: () => (win.reloads += 1) };
|
|
const cache = new Map();
|
|
const load = (rel) => {
|
|
if (cache.has(rel)) return cache.get(rel);
|
|
const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, rel), "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const exports = {};
|
|
cache.set(rel, exports);
|
|
new Function("exports", "require", "window", js)(exports, need, win);
|
|
return exports;
|
|
};
|
|
const need = (id) => {
|
|
if (id.includes("config_frontend"))
|
|
return { CURRENT_PROJECT_ID_KEY: "frd_current_project_id", API_BASE_URL: "http://x/api" };
|
|
if (id.endsWith("b_page_state")) return load("A00_Common/b_page_state.ts");
|
|
if (id.endsWith("b_asset_cache")) return load("A00_Common/b_asset_cache.ts");
|
|
if (id.endsWith("ui_template_locale")) return { t: (key) => key };
|
|
if (id.endsWith("ui_template_leave_dialog"))
|
|
return {
|
|
leaveOrStay: async (message, hooks) => {
|
|
asked.push(message);
|
|
if (choice === "cancel") return "stay";
|
|
if (choice === "discard") return (await hooks.discard(), "discarded");
|
|
try {
|
|
await hooks.save();
|
|
} catch {
|
|
return "stay";
|
|
}
|
|
return hooks.stillDirty() ? "stay" : "saved";
|
|
},
|
|
};
|
|
if (id.endsWith("ui_template_elements")) return { showConfirmDialog: async () => true };
|
|
return {}; // B04 · B05 받기(보관함이 부르지 않음)
|
|
};
|
|
const sw = load("A00_Common/b_project_switch.ts");
|
|
sw.linkRouter({
|
|
currentRoute: () => router.route,
|
|
setLeaveGuard: (fn) => ((router.guard = fn), (router.sets += 1)),
|
|
});
|
|
return { win, sw, ps: load("A00_Common/b_page_state.ts") };
|
|
}
|
|
|
|
const X = "11111111-aaaa-4aaa-8aaa-000000000001";
|
|
const Y = "22222222-bbbb-4bbb-8bbb-000000000002";
|
|
/** 브라우저 BeforeUnloadEvent 처럼 returnValue 를 쓸 수 있게. */
|
|
class BeforeUnload extends Event {
|
|
returnValue = true;
|
|
}
|
|
const tick = () => new Promise((resolve) => setTimeout(resolve, 5));
|
|
const keys = (win) => Object.keys(win.sessionStorage.dump()).sort();
|
|
|
|
(async () => {
|
|
const out = {};
|
|
|
|
// 1. 탭 둘 — B 가 Y 로 바꿔도 A 는 X · 새 탭은 마지막 값(Y)
|
|
shared.localStorage.setItem("frd_current_project_id", X);
|
|
const A = openTab();
|
|
out.newTabStartsWithLast = A.sw.currentProjectId();
|
|
await A.sw.enterProject(X, async (id) => id);
|
|
const B = openTab();
|
|
await B.sw.enterProject(Y, async (id) => id);
|
|
out.tabs = {
|
|
a: A.sw.currentProjectId(),
|
|
b: B.sw.currentProjectId(),
|
|
fresh: openTab().sw.currentProjectId(),
|
|
last: shared.localStorage.getItem("frd_current_project_id"),
|
|
};
|
|
|
|
// 2. 다른 프로젝트 화면을 그리려 하면 새로 엶 — 같은 프로젝트는 그대로
|
|
out.reload = { first: A.sw.reloadIfOtherProject(), same: A.sw.reloadIfOtherProject() };
|
|
A.sw.setCurrentProject(Y);
|
|
out.reload.other = A.sw.reloadIfOtherProject();
|
|
out.reload.count = A.win.reloads;
|
|
A.sw.setCurrentProject(X);
|
|
|
|
// 3. 떠남 — 그 프로젝트 칸 · 준비 표식 · 보관함 몫만 · 밖 칸 · 취향 · CAD 복구본 그대로
|
|
const seedX = [
|
|
`aislo:draft:structures:${X}`,
|
|
`aislo:draft:crossw:${X}:12`,
|
|
`aislo:result:section-detail:${X}:12`,
|
|
`aislo:draft:d03-plan:${X}`,
|
|
`aislo:draft:d04-assignees:${X}`,
|
|
`aislo:draft:e05-report:${X}`,
|
|
];
|
|
const keep = [
|
|
`aislo:draft:structures:${Y}`,
|
|
"a06:otp-pending",
|
|
"aislo.adminCompany",
|
|
"aislo.provenance.tint",
|
|
"m01_master_draft",
|
|
"m02-hopyo-check:3",
|
|
"aislo:entered-project",
|
|
];
|
|
seedX.forEach((k) => A.win.sessionStorage.setItem(k, '{"v":1}'));
|
|
keep.slice(0, -1).forEach((k) => A.win.sessionStorage.setItem(k, "1"));
|
|
A.win.sessionStorage.setItem("frd_preloaded_signature", `${X}|sig`);
|
|
const localKeep = [`aislo:pref:routeedit-view:${X}`, `OPEN_WEB_CAD__RECOVERY__b07:${X}:1`];
|
|
localKeep.forEach((k) => shared.localStorage.setItem(k, "1"));
|
|
for (const [pid, url] of [
|
|
[X, "u1"],
|
|
[X, "u2"],
|
|
[Y, "u1"],
|
|
]) {
|
|
shared.idb.data.set(`${pid}|${url}`, { key: `${pid}|${url}`, projectId: pid, url });
|
|
}
|
|
const removed = [];
|
|
const stop = A.ps.onStateRemoved((key) => removed.push(key)); // 설계 가게가 미룬 쓰기를 거두는 알림
|
|
await A.sw.leaveProject(X);
|
|
stop();
|
|
out.leave = {
|
|
notified: removed.length,
|
|
keys: keys(A.win),
|
|
expected: [...keep].sort(),
|
|
entered: A.win.sessionStorage.getItem("aislo:entered-project"),
|
|
local: localKeep.map((k) => shared.localStorage.getItem(k)),
|
|
idb: [...shared.idb.data.keys()].sort(),
|
|
};
|
|
// 다른 탭(B) 세션은 안 건드림
|
|
B.win.sessionStorage.setItem(`aislo:draft:structures:${X}`, "1");
|
|
await A.sw.leaveProject(X);
|
|
out.leave.otherTabKept = B.win.sessionStorage.getItem(`aislo:draft:structures:${X}`);
|
|
|
|
// 4. 떠남 물음 — 고친 가드만 모아 한 번 · 저장 실패 = 머묾 · 폐기 · 같은 페이지 · 빠진 페이지
|
|
const root1 = { isConnected: true };
|
|
const root2 = { isConnected: true };
|
|
const state = { d1: false, d2: false, saves: [], discards: [], saveOk: true };
|
|
A.sw.guardProjectLeave(root1, {
|
|
dirty: () => state.d1,
|
|
save: async () => (state.saves.push(1), state.saveOk && (state.d1 = false), state.saveOk),
|
|
discard: () => state.discards.push(1),
|
|
});
|
|
A.sw.guardProjectLeave(root2, {
|
|
dirty: () => state.d2,
|
|
save: async () => (state.saves.push(2), (state.d2 = false), true),
|
|
message: "둘째 글",
|
|
});
|
|
await tick();
|
|
const ask = router.guard;
|
|
const unloadBlocked = () => {
|
|
const event = new BeforeUnload("beforeunload", { cancelable: true });
|
|
A.win.dispatchEvent(event);
|
|
return event.defaultPrevented;
|
|
};
|
|
out.guard = { armed: ask === A.sw.askProjectLeave, cleanLeave: await ask() };
|
|
out.guard.cleanUnload = unloadBlocked();
|
|
out.guard.asked0 = asked.length;
|
|
state.d2 = true;
|
|
out.guard.dirtyUnload = unloadBlocked();
|
|
choice = "cancel";
|
|
out.guard.cancel = await ask();
|
|
choice = "save";
|
|
out.guard.save = await ask();
|
|
out.guard.messages = [...asked];
|
|
state.d1 = true;
|
|
state.saveOk = false;
|
|
out.guard.saveFail = await ask();
|
|
choice = "discard";
|
|
out.guard.discard = await ask();
|
|
out.guard.saves = state.saves;
|
|
out.guard.discards = state.discards.length;
|
|
router.route = "/b06-section";
|
|
A.sw.guardProjectLeave(
|
|
{ isConnected: true },
|
|
{ dirty: () => true, save: async () => true, samePage: ["/b05-profile", "/b06-section"] },
|
|
);
|
|
await tick();
|
|
const sets = router.sets;
|
|
const askedBefore = asked.length;
|
|
out.guard.samePage = await ask();
|
|
await tick();
|
|
out.guard.reArmed = router.sets === sets + 1;
|
|
out.guard.samePageAsked = asked.length - askedBefore;
|
|
process.stdout.write(JSON.stringify(out));
|
|
})().catch((error) => {
|
|
process.stderr.write(String(error && error.stack ? error.stack : error));
|
|
process.exit(1);
|
|
});
|