- 433-16: 새로고침 · 새 탭 = 세션 상세로 먼저 섬 · 뒤에서 상세 GET(ETag 304) · 판이 다르면 서버 값 위에 이 탭에서 고친 사용자 칸만 · [저장] patch 바탕도 새 판 · 노선 초안 hold 는 버릴 때 새 판 - 440-3: 숨은 종단 보기도 관 세션 id 장부 돌림(평면 마커 누름 = 초안 자리) · 그리기는 보일 때 - 384_3: pvi 단언 = 저장 꼭짓점 + 심은 측점마다 세운 꼭짓점(427) - 384_7: 세션 쓰기 = 받을 때 · 판 확인 새 판 둘 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
226 lines
8.6 KiB
JavaScript
226 lines
8.6 KiB
JavaScript
/* 451 헬퍼(433-16) — 세션 상세 판 확인: 진짜 상세 가게(`B06_Section_Section_Store`)가 세션 한 벌로 먼저 서고
|
|
* 뒤에서 서버 판을 물음 · 다르면 서버 값 위에 이 탭에서 고친 사용자 칸만 · 같으면 그대로 · 노선 초안 hold 는 버릴 때
|
|
* 새 판. 서버 창구 · 초안 얹기 · 열쇠는 가짜. 짝 = `test_451_session_canon.py`.
|
|
*/
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
|
const CATALOG = require("./helper_322_catalog_module.cjs");
|
|
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
const P = "P451";
|
|
const ROUTE = 3;
|
|
|
|
function memoryStorage() {
|
|
const map = new Map();
|
|
return {
|
|
getItem: (k) => (map.has(k) ? map.get(k) : null),
|
|
setItem: (k, v) => map.set(k, String(v)),
|
|
removeItem: (k) => map.delete(k),
|
|
key: (i) => [...map.keys()][i] ?? null,
|
|
get length() {
|
|
return map.size;
|
|
},
|
|
};
|
|
}
|
|
globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStorage() };
|
|
|
|
const anyModule = (extra = {}) =>
|
|
new Proxy(extra, { get: (t, key) => (key in t ? t[key] : () => undefined) });
|
|
|
|
/** 서버 — 부를 때마다 지금 `server` 사본 · `gate` 가 풀릴 때 답함. */
|
|
let server = null;
|
|
let gate = Promise.resolve();
|
|
let asked = 0;
|
|
const keyAt = (sections) => sections.map((s) => `at:${s.chainage_m.toFixed(2)}`);
|
|
const STUBS = {
|
|
"design_pipeline_page.ts": {
|
|
reconcilePagePipeline: async () => 0,
|
|
pipelineOwnsCross: () => false,
|
|
pagePipeline: () => null,
|
|
},
|
|
"design_pipeline_cross.ts": { forceCross: () => undefined },
|
|
"B06_Section_Section_Drafts.ts": {
|
|
forgetDraftStations: () => undefined,
|
|
withDraftShapes: async () => undefined,
|
|
withDraftStations: async () => undefined,
|
|
supersedeDraftStations: () => undefined,
|
|
},
|
|
"B05_Profile_Api_PlusStations.ts": {
|
|
loadSavedPlusStations: async () => undefined,
|
|
plusStationsOf: () => [],
|
|
},
|
|
"B06_Section_Api_Fetch.ts": anyModule({
|
|
fetchSectionDetail: async () => {
|
|
asked += 1;
|
|
await gate;
|
|
return JSON.parse(JSON.stringify(server));
|
|
},
|
|
}),
|
|
"B06_Section_Api_Culvert_Options.ts": anyModule({ STRUCTURE_ADJUST_KEYS: [] }),
|
|
"B06_Section_Swap_Bus.ts": anyModule(),
|
|
"B06_Section_Station_Keys.ts": anyModule({
|
|
KEYED_STATES: [],
|
|
registeredKeys: (_p, _r, sections) => keyAt(sections),
|
|
stationIntervalOf: () => 20,
|
|
}),
|
|
"B06_Section_UI_Page_Persist.ts": anyModule({ readCutSlopeSession: () => ({}) }),
|
|
"B06_Section_UI_Standard_Panel.ts": anyModule(),
|
|
"B06_Section_Cross_Design_Session.ts": anyModule({ crossDesignChoices: () => new Map() }),
|
|
"B06_Section_Section_Adjusts.ts": anyModule({ withPipeOptions: async () => undefined }),
|
|
"common_util_cross_design.ts": anyModule(),
|
|
"B06_Section_UI_Cross_Culvert_Wire.ts": anyModule(),
|
|
"B06_Section_UI_Cross_Ford_Geom.ts": anyModule(),
|
|
"B06_Section_UI_Cross_Box_Geom.ts": anyModule(),
|
|
"B06_Section_UI_IrregularStations.ts": anyModule({ PIPE_DISPLAY_NAME: {} }),
|
|
"B05_Profile_UI_IrregularStations.ts": anyModule({ PIPE_DISPLAY_NAME: {} }),
|
|
"config_frontend.ts": anyModule({ PIPE_TYPES: [] }),
|
|
};
|
|
|
|
const cache = new Map();
|
|
const resolve = (base) =>
|
|
[base, `${base}.ts`, path.join(base, "index.ts")].find(
|
|
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
|
|
) ?? `${base}.ts`;
|
|
function loadTs(full) {
|
|
const stub = STUBS[path.basename(full)];
|
|
if (stub) return stub;
|
|
if (cache.has(full)) return cache.get(full);
|
|
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.CommonJS,
|
|
target: ts.ScriptTarget.ES2020,
|
|
esModuleInterop: true,
|
|
},
|
|
}).outputText;
|
|
const box = { exports: {} };
|
|
cache.set(full, box.exports);
|
|
const near = (request) => {
|
|
if (CATALOG.isCatalogRequest(request)) return CATALOG;
|
|
const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" };
|
|
for (const [prefix, dir] of Object.entries(alias))
|
|
if (request.startsWith(prefix))
|
|
return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length))));
|
|
return loadTs(resolve(path.join(path.dirname(full), request)));
|
|
};
|
|
new Function("exports", "module", "require", js)(box.exports, box, near);
|
|
return box.exports;
|
|
}
|
|
|
|
const store = loadTs(path.join(ROOT, "B06_Section", "B06_Section_Section_Store.ts"));
|
|
const state = loadTs(path.join(ROOT, "A00_Common", "b_page_state.ts"));
|
|
const tick = () => new Promise((r) => setTimeout(r, 10));
|
|
|
|
/** 정본 — 측점 0 · 20 · 40 · 60 · 측점 20 집수정 조절 · 40 메모 · 계획고. */
|
|
const canon = (adjust, note, elevation) => ({
|
|
longitudinal: { design_profiles: [], stations: [] },
|
|
cross_sections: [0, 20, 40, 60].map((c) => ({
|
|
chainage_m: c,
|
|
kind: c === 0 ? "bp" : "regular",
|
|
samples: [],
|
|
design: {
|
|
design_elevation_m: elevation,
|
|
...(c === 20 ? { basin_adjust: { x: adjust } } : {}),
|
|
...(c === 40 ? { structure_note: note } : {}),
|
|
},
|
|
})),
|
|
});
|
|
const designAt = (detail, c) => detail.cross_sections.find((s) => s.chainage_m === c).design;
|
|
const patchAt = (patches, c) => patches.find((p) => p.chainage_m === c) ?? null;
|
|
const sessionNow = () => state.readState("section-detail", P, ROUTE);
|
|
const open = () => {
|
|
let release;
|
|
gate = new Promise((r) => (release = r));
|
|
return release;
|
|
};
|
|
|
|
(async () => {
|
|
const out = {};
|
|
// ① 낡은 세션(조절 1 · 메모 A · 계획고 100) + 다른 탭이 저장(조절 2 · 계획고 101) → 새 탭(새로고침)
|
|
state.writeState("section-detail", canon(1, "A", 100), P, ROUTE);
|
|
server = canon(2, "A", 101);
|
|
let release = open();
|
|
let heard = 0;
|
|
store.subscribeSectionDetail(P, ROUTE, () => (heard += 1));
|
|
const first = await store.loadSectionDetail(P, ROUTE);
|
|
out.firstFromSession = designAt(first, 20).basin_adjust.x === 1 && asked === 1;
|
|
// 서버가 답하기 전 이 탭에서 고친 칸 — 측점 40 메모
|
|
designAt(first, 40).structure_note = "mine";
|
|
release();
|
|
await tick();
|
|
const now = store.peekSectionDetail(P, ROUTE);
|
|
const patches = store.cachedCrossPatches(P, ROUTE);
|
|
out.stale = {
|
|
swapped: now !== first,
|
|
heard,
|
|
adjust: designAt(now, 20).basin_adjust.x,
|
|
elevation: designAt(now, 60).design_elevation_m,
|
|
note: designAt(now, 40).structure_note,
|
|
patchAdjust: patchAt(patches, 20)?.basin_adjust?.x ?? null,
|
|
patchNote: patchAt(patches, 40)?.structure_note ?? null,
|
|
sessionAdjust: designAt(sessionNow(), 20).basin_adjust.x,
|
|
sessionNote: designAt(sessionNow(), 40).structure_note,
|
|
};
|
|
|
|
// ② 같은 판 — 새로고침(메모리 비움 흉내) · 서버 = 세션 → 그대로(갈아끼움 · 알림 없음)
|
|
store.invalidateSectionDetail(P, ROUTE);
|
|
state.writeState("section-detail", server, P, ROUTE);
|
|
release = open();
|
|
heard = 0;
|
|
const again = await store.loadSectionDetail(P, ROUTE);
|
|
release();
|
|
await tick();
|
|
out.same = { kept: store.peekSectionDetail(P, ROUTE) === again, heard };
|
|
|
|
// ③ 서버 실패 · 엉뚱한 답 — 세션 그대로
|
|
store.invalidateSectionDetail(P, ROUTE);
|
|
state.writeState("section-detail", canon(1, "A", 100), P, ROUTE);
|
|
const saved = server;
|
|
server = null;
|
|
release = open();
|
|
const broken = await store.loadSectionDetail(P, ROUTE);
|
|
release();
|
|
await tick();
|
|
out.broken = store.peekSectionDetail(P, ROUTE) === broken;
|
|
server = saved;
|
|
|
|
// ④ 노선 초안 hold(옛 상세) · 갈아끼움 뒤 서버가 답함 → 초안을 버리면 새 판
|
|
store.invalidateSectionDetail(P, ROUTE);
|
|
state.writeState("section-detail", canon(1, "A", 100), P, ROUTE);
|
|
release = open();
|
|
const held = await store.loadSectionDetail(P, ROUTE);
|
|
const hold = store.holdSectionDetail(P, ROUTE, held);
|
|
const draftStations = [0, 25, 45, 60].map((c) => ({
|
|
key: `at:${c}`,
|
|
chainageM: c,
|
|
kind: "grid",
|
|
x: c,
|
|
y: 0,
|
|
left: [0, 1],
|
|
azimuthDeg: 0,
|
|
ground: null,
|
|
}));
|
|
await store.swapSectionStations(P, ROUTE, draftStations);
|
|
const swappedDraft = store.peekSectionDetail(P, ROUTE) !== held;
|
|
release();
|
|
await tick();
|
|
const draftKept = store.peekSectionDetail(P, ROUTE).cross_sections.map((s) => s.chainage_m);
|
|
const back = await store.restoreSectionDetail(
|
|
P,
|
|
ROUTE,
|
|
hold,
|
|
[0, 20, 40, 60].map((c) => ({ key: `at:${c}`, chainageM: c })),
|
|
);
|
|
out.hold = {
|
|
swappedDraft,
|
|
draftKept,
|
|
renewed: back && back.detail !== held,
|
|
adjust: back && designAt(back.detail, 20).basin_adjust.x,
|
|
same: back && back.same,
|
|
};
|
|
process.stdout.write(JSON.stringify(out));
|
|
})().catch((error) => {
|
|
process.stderr.write(String(error && error.stack ? error.stack : error));
|
|
process.exit(1);
|
|
});
|