Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -214,6 +214,8 @@ export const STATE_REGISTRY = {
|
||||
/** 노선 초안(344-4) — 노드 · 곡선 · 반경 · 스냅 · 지형 · rev(`RouteDraft` · `design_pipeline_types`).
|
||||
* 노선 번호는 [저장]마다 새로 나므로 project 범위 · 가게 키 `routeDraft`. */
|
||||
"route-draft": { bucket: "draft", scope: "project" },
|
||||
/** 노선 초안 전 상태(440-4) — 측점 열쇠 · 횡단 초안 칸 · 관 · 구조물 · 초안 둔 채 새로고침해도 버리면 이것으로. */
|
||||
"route-draft-hold": { bucket: "draft", scope: "project" },
|
||||
|
||||
/* ── ④ 계산 결과 ─────────────────────────────────────────────────────── */
|
||||
/** 노선·종단 최신 응답. 페이지를 오갈 때 이 값으로 먼저 그린다. */
|
||||
|
||||
@@ -426,11 +426,12 @@ function createPagePipeline(
|
||||
Object.assign(record, { restored: why.startsWith("saved"), restoreWhy: why });
|
||||
return why;
|
||||
};
|
||||
/** 노선 초안 시작 — 지금 저장 상세 · 되돌리는 함수를 맡김(횡단 초안 칸 · 측점 열쇠는 맡은 쪽이 떠 둠). */
|
||||
/** 노선 초안 시작 — 지금 저장 상세 · 되돌리는 함수를 맡김(횡단 초안 칸 · 측점 열쇠 · 관 · 구조물은 맡은 쪽이 떠 둠). */
|
||||
const holdNow = (): typeof held => {
|
||||
const { detail = null, routeId } = savedSource();
|
||||
if (typeof routeId !== "number") return null;
|
||||
return { detail, restore: options.holdDraft?.(routeId, detail) ?? null };
|
||||
const restore = options.holdDraft?.(routeId, detail, (f) => (beforeDraft ??= f)); // 440-4
|
||||
return { detail, restore: restore ?? null };
|
||||
};
|
||||
// 사용자 노선 조작 뒤 follow 가 다시 매긴 관 · 구조물 = 가게(목록 · 알약 · 횡단 보기가 바로 새 측점) ·
|
||||
// 초안을 버리면 초안 전 값 · 열 때(start 차례)는 안 씀 — 알림 밖(틱 뒤)에서 씀(쓰기 고리 막음과 안 부딪힘)
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
} from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
|
||||
import type { BermAt } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
|
||||
import type { SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
|
||||
import type { DesignStoreValues } from "./design_store";
|
||||
|
||||
/** 종단도(`design_profile`)가 묶음에 내주는 것. */
|
||||
export interface ProfileLink {
|
||||
@@ -50,6 +51,8 @@ export interface PagePipelineOptions {
|
||||
holdDraft?: (
|
||||
routeId: number,
|
||||
saved: SectionDetailResponse | null,
|
||||
/** 초안 전 가게 관 · 구조물(초안 둔 채 새로고침도 · 440-4) — 묶음이 초안을 버릴 때 되씀. */
|
||||
seed?: (facilities: Pick<DesignStoreValues, "pipes" | "structures">) => void,
|
||||
) => (stations: PipelineStation[]) => Promise<string>;
|
||||
/** 고리가 `RECONCILE_ROUNDS` 바퀴 안에 안 맞음 — 「계산 불안정」 한 줄(365-2). */
|
||||
onUnstable?: () => void;
|
||||
|
||||
@@ -253,8 +253,9 @@ function startPagePipeline(shared: DesignPageShared, profile: DesignProfile): vo
|
||||
applyStructureAreaRows(rows, structureAreaRows(rows));
|
||||
},
|
||||
// 초안 시작 — 초안 전 상태를 떠 둠 · 버리면 그대로(측점이 달라도 · 349-0o) · 상세가 없으면 묶음 측점으로 갈아끼움
|
||||
holdDraft: (routeId, saved) => {
|
||||
holdDraft: (routeId, saved, seed) => {
|
||||
const hold = saved && holdSectionDetail(projectId, routeId, saved);
|
||||
if (hold) seed?.(hold.facilities); // 초안 전 관 · 구조물(440-4)
|
||||
return async (stations) => {
|
||||
if (!hold) {
|
||||
await swapSectionStations(projectId, routeId, stations);
|
||||
|
||||
@@ -159,7 +159,8 @@ export async function mountProfileView(
|
||||
const followed = designStore.get("pipes");
|
||||
if (followed) profilePanel.drainage.followPipes(followed);
|
||||
else if (profilePanel.drainage.pipeChainages().length) profilePanel.drainage.reload();
|
||||
if (!host.visible()) return;
|
||||
// 숨어도 관 세션 id 장부는 따라감 — 평면 마커 누름이 초안 자리를 고름(440-3) · 그리기는 보일 때
|
||||
if (!host.visible()) return void (lastPipes && shownPipes(lastPipes));
|
||||
if (lastPipes) applyPipes(lastPipes, false);
|
||||
bridge.takeStructures(designStore.get("structures"));
|
||||
};
|
||||
@@ -214,6 +215,7 @@ export async function mountProfileView(
|
||||
onPipesChanged: (pipes) => {
|
||||
lastPipes = pipes;
|
||||
if (host.visible()) applyPipes(pipes);
|
||||
else shownPipes(pipes); // 숨어도 관 id 장부만(440-3)
|
||||
},
|
||||
onStructureMove: (from, to, station) => {
|
||||
if (isPipeStation(station)) profilePanel.drainage.movePipe(from, to);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* 쓰는 조작(재생성·계획선 편집 저장·확정)은 그 응답/재조회로 `replace`·`invalidate`한다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { clearState, readState, readStateRaw, writeState } from "../A00_Common/b_page_state";
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
import { setGeneralStations, setPlusStations } from "../common_util/common_util_structure_station";
|
||||
import { loadSavedPlusStations, plusStationsOf } from "../B05_Profile/B05_Profile_Api_PlusStations";
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { applyStructureAreaRows, structureAreaRows } from "./B06_Section_Structure_Layouts";
|
||||
import { withDraftAdjusts, withPipeOptions } from "./B06_Section_Section_Adjusts";
|
||||
import type {
|
||||
CrossSection,
|
||||
CrossSectionPatch,
|
||||
SectionDetailResponse,
|
||||
StandardCrossSection,
|
||||
@@ -39,6 +40,7 @@ import { fetchSectionDetail, saveSections } from "./B06_Section_Api_Fetch";
|
||||
import { staleKeyAgainstScreen, USER_TOUCHED_KEYS } from "./B06_Section_Cross_Refresh";
|
||||
import { kindChangedChainages, STRUCTURE_ADJUST_KEYS } from "./B06_Section_Api_Culvert_Options";
|
||||
import type { PipelineStation } from "../A00_Common/design/logic/design_pipeline_types";
|
||||
import type { DesignStoreValues } from "../A00_Common/design/design_store";
|
||||
import {
|
||||
carryStationKeys,
|
||||
KEYED_STATES,
|
||||
@@ -126,6 +128,7 @@ export async function loadSectionDetail(
|
||||
const stored = sessionDetail(projectId, routeId); // 세션 글 = 한 번만 풂(387-7)
|
||||
if (stored) {
|
||||
cache.set(key, stored);
|
||||
void checkSessionCanon(projectId, routeId, stored).catch(() => undefined);
|
||||
return withDraftWalls(stored, projectId, routeId);
|
||||
}
|
||||
}
|
||||
@@ -143,6 +146,62 @@ export async function loadSectionDetail(
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션 상세 판 확인(433-16) — 새로고침 · 새 탭 첫 화면은 세션 한 벌로 서고, 뒤에서 서버에 한 번 물음(ETag ·
|
||||
* 같으면 304). 다른 탭 · 다른 PC 가 저장해 판이 다르면 서버 값 위에 **이 탭에서 고친 사용자 칸만**(세션 정본
|
||||
* 사본과 다른 칸) 얹음 — 낡은 캐시가 [저장] patch 바탕(445)으로 서버 값을 되쓰지 않게. 새 상세로 갈아끼워 듣는
|
||||
* 쪽이 다시 그림 · 노선 초안이 옛 상세를 쥐고 있으면(hold) 버릴 때 새 판으로(`renewed`).
|
||||
*/
|
||||
async function checkSessionCanon(
|
||||
projectId: string,
|
||||
routeId: number,
|
||||
stored: SectionDetailResponse,
|
||||
): Promise<void> {
|
||||
const fresh = await fetchSectionDetail(projectId, routeId);
|
||||
if (!Array.isArray(fresh?.cross_sections)) return;
|
||||
const raw = readStateRaw("section-detail", projectId, routeId);
|
||||
if (raw === null || raw === JSON.stringify(fresh)) return; // 같은 판 · 그 사이 버림
|
||||
const key = keyOf(projectId, routeId);
|
||||
const current = cache.get(key);
|
||||
const edits = userEdits(current ?? stored, savedDesigns(projectId, routeId) ?? []);
|
||||
writeState("section-detail", fresh, projectId, routeId);
|
||||
keepCanon(projectId, routeId, fresh);
|
||||
for (const section of fresh.cross_sections) {
|
||||
const edit = edits.find((entry) => Math.abs(entry.chainage_m - section.chainage_m) < 0.01);
|
||||
if (edit && section.design) section.design = { ...section.design, ...edit.cells };
|
||||
}
|
||||
renewed.set(stored, fresh);
|
||||
if (current !== stored) return; // 노선 초안 갈아끼움 중 · 그 사이 새로 받음 — 초안을 버리면 새 판
|
||||
cache.set(key, fresh);
|
||||
await withDraftWalls(fresh, projectId, routeId);
|
||||
if (cache.get(key) !== fresh) return;
|
||||
emitSwap(fresh, new Map());
|
||||
listeners.get(key)?.forEach((fn) => fn(fresh, new Map()));
|
||||
}
|
||||
|
||||
/** 판이 바뀐 세션 상세 → 새 판 상세(433-16) — 노선 초안 hold 가 쥔 옛 객체를 되돌릴 때. */
|
||||
const renewed = new WeakMap<SectionDetailResponse, SectionDetailResponse>();
|
||||
|
||||
/** 측점마다 정본 사본과 다른 사용자 칸(`USER_TOUCHED_KEYS`) — 이 탭에서 고친 것. */
|
||||
function userEdits(
|
||||
detail: SectionDetailResponse,
|
||||
canonRows: ReadonlyArray<{ chainage_m: number; design: CrossSection["design"] }>,
|
||||
): Array<{ chainage_m: number; cells: Record<string, unknown> }> {
|
||||
const out: Array<{ chainage_m: number; cells: Record<string, unknown> }> = [];
|
||||
for (const section of detail.cross_sections) {
|
||||
if (!section.design) continue;
|
||||
const now = section.design as unknown as Record<string, unknown>;
|
||||
const row = canonRows.find((r) => Math.abs(r.chainage_m - section.chainage_m) < 0.01);
|
||||
const was = (row?.design ?? {}) as unknown as Record<string, unknown>;
|
||||
const cells: Record<string, unknown> = {};
|
||||
for (const name of USER_TOUCHED_KEYS)
|
||||
if (now[name] !== undefined && JSON.stringify(now[name]) !== JSON.stringify(was[name]))
|
||||
cells[name] = now[name];
|
||||
if (Object.keys(cells).length) out.push({ chainage_m: section.chainage_m, cells });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const listeners = new Map<string, Set<DetailListener>>();
|
||||
|
||||
/** 상세 객체가 갈아끼워짐(측점 수가 바뀜 · 346-2 4파) — 쥔 옛 객체를 버리고 새것으로 다시 그릴 것. */
|
||||
@@ -230,26 +289,44 @@ function recrossSaved(
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
/** 노선 초안 전 상태(349-0o) — 상세(초안 전 객체 그대로 · 되돌리면 캐시) · 측점 열쇠 · 측점 열쇠 칸 다섯(횡단 초안). */
|
||||
/** 노선 초안 전 상태(349-0o) — 상세(초안 전 객체 그대로 · 되돌리면 캐시) · 측점 열쇠 · 측점 열쇠 칸 다섯(횡단 초안) ·
|
||||
* 가게 관 · 구조물(440-4 · 묶음이 버릴 때 되씀). */
|
||||
export interface SectionHold {
|
||||
detail: SectionDetailResponse;
|
||||
keys: Keyed[];
|
||||
cells: Array<[KeyedState, unknown]>;
|
||||
facilities: Pick<DesignStoreValues, "pipes" | "structures">;
|
||||
}
|
||||
type HeldDraft = Omit<SectionHold, "detail"> & { routeId: number };
|
||||
|
||||
/** 노선 초안 시작 — 초안 전 상세(`detail` · 객체 그대로 — 갈아끼움은 설계를 사본으로 씀) · 지금 측점 열쇠 · 횡단 초안 칸을 떠 둠. */
|
||||
/**
|
||||
* 노선 초안 시작 — 초안 전 상세(`detail` · 객체 그대로 — 갈아끼움은 설계를 사본으로 씀) · 지금 측점 열쇠 · 횡단 초안 칸 ·
|
||||
* 관 · 구조물을 떠 둠 · 세션에도 한 벌(440-4). 초안 둔 채 새로고침이면 그 한 벌 — 세션 칸 · 열쇠 · 관은 이미 초안
|
||||
* 자리로 옮겨 쓴 값이라 처음 값으로 안 뜸.
|
||||
*/
|
||||
export function holdSectionDetail(
|
||||
projectId: string,
|
||||
routeId: number,
|
||||
detail: SectionDetailResponse,
|
||||
): SectionHold {
|
||||
const kept = readState<HeldDraft>("route-draft-hold", projectId);
|
||||
if (kept?.routeId === routeId && readState("route-draft", projectId)) {
|
||||
const { keys, cells, facilities } = kept;
|
||||
return { detail, keys, cells, facilities };
|
||||
}
|
||||
const sections = detail.cross_sections;
|
||||
const keys = registeredKeys(projectId, routeId, sections, stationIntervalOf(detail));
|
||||
return {
|
||||
detail,
|
||||
const held: HeldDraft = {
|
||||
routeId,
|
||||
keys: sections.map((section, i) => ({ key: keys[i], chainageM: section.chainage_m })),
|
||||
cells: KEYED_STATES.map((name) => [name, readState(name, projectId, routeId)]),
|
||||
facilities: {
|
||||
pipes: readState<DesignStoreValues["pipes"]>("pipes", projectId),
|
||||
structures: readState<DesignStoreValues["structures"]>("structures", projectId),
|
||||
},
|
||||
};
|
||||
writeState("route-draft-hold", held, projectId);
|
||||
return { detail, keys: held.keys, cells: held.cells, facilities: held.facilities };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -267,11 +344,7 @@ export async function restoreSectionDetail(
|
||||
const key = keyOf(projectId, routeId);
|
||||
const current = cache.get(key);
|
||||
if (!current) return null;
|
||||
const saved = hold.detail;
|
||||
const sections = saved.cross_sections;
|
||||
const same =
|
||||
sections.length === stations.length &&
|
||||
stations.every((station, i) => Math.abs(station.chainageM - sections[i].chainage_m) < 1e-3);
|
||||
const saved = renewed.get(hold.detail) ?? hold.detail; // 그 사이 서버 판이 바뀌었으면 새 판(433-16)
|
||||
const oldKeys = registeredKeys(
|
||||
projectId,
|
||||
routeId,
|
||||
@@ -286,11 +359,18 @@ export async function restoreSectionDetail(
|
||||
moves.set(text(section.chainage_m), text(to.chainageM));
|
||||
});
|
||||
for (const [name, value] of hold.cells) writeState(name, value ?? null, projectId, routeId);
|
||||
useStationKeyList(projectId, routeId, hold.keys);
|
||||
clearState("route-draft-hold", projectId);
|
||||
cache.set(key, saved); // 세션 상세는 정본 그대로(387-7)
|
||||
// 초안 둔 채 새로고침 = 초안 전 상세에 초안 자리 관이 얹혀 있었음 — 되쓴 관 · 구조물로 다시 얹음(440-4)
|
||||
await withDraftStations(saved, projectId, routeId);
|
||||
useStationKeyList(projectId, routeId, hold.keys);
|
||||
await withDraftShapes(saved, projectId);
|
||||
withStructureAreas(saved);
|
||||
if (cache.get(key) !== saved) return null; // 그 사이 다른 갈아끼움 · 새로 받음
|
||||
const sections = saved.cross_sections; // 다시 얹은 뒤 측점(440-4)
|
||||
const same =
|
||||
sections.length === stations.length &&
|
||||
stations.every((station, i) => Math.abs(station.chainageM - sections[i].chainage_m) < 1e-3);
|
||||
emitSwap(saved, moves);
|
||||
listeners.get(key)?.forEach((fn) => fn(saved, moves));
|
||||
return { detail: saved, same };
|
||||
|
||||
@@ -119,6 +119,7 @@ process.stdout.write(
|
||||
both: ids(both),
|
||||
noDetail: noDetail === viewBase,
|
||||
pvi: both.basePvi.length,
|
||||
pviAt: both.basePvi.map((node) => node.chainage_m),
|
||||
kinds: [on.type_id, off.type_id],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
/* 451 헬퍼(440-4) — 노선 초안 둔 채 새로고침: 진짜 상세 가게 · 진짜 측점 열쇠 · 세션(모듈을 다시 세워도 남음).
|
||||
* 초안 시작 hold = 세션 한 벌(열쇠 · 횡단 초안 칸 · 관 · 구조물) · 새로고침 뒤 hold = 그 한 벌(초안으로 옮겨 쓴 세션 아님) ·
|
||||
* 버림 = 초안 전 칸 · 초안 얹기 다시(되쓴 관으로) 뒤 초안 전 열쇠. 서버 · 초안 얹기는 가짜. 짝 = `test_451_refresh_hold.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 (detail) => {
|
||||
// 초안 얹기 흉내 — 가게 관 자리(세션 pipes)마다 그 측점 관 표시 · 부른 차례를 적음
|
||||
const pipes = state.readState("pipes", P) ?? [];
|
||||
calls.push(`stations:${pipes.map((p) => p.chainage_m).join(",")}`);
|
||||
for (const s of detail.cross_sections)
|
||||
s.pipeAt = pipes.some((p) => p.chainage_m === s.chainage_m);
|
||||
},
|
||||
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(),
|
||||
"ui_template_elements.ts": anyModule(),
|
||||
"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: [] }),
|
||||
};
|
||||
|
||||
let cache = new Map();
|
||||
const calls = [];
|
||||
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;
|
||||
}
|
||||
|
||||
let store = loadTs(path.join(ROOT, "B06_Section", "B06_Section_Section_Store.ts"));
|
||||
const state = loadTs(path.join(ROOT, "A00_Common", "b_page_state.ts"));
|
||||
let keys = loadTs(path.join(ROOT, "B06_Section", "B06_Section_Station_Keys.ts"));
|
||||
/** 새로고침 흉내 — 모듈 메모리를 비우고 다시 세움(세션 · 서버는 그대로). */
|
||||
function refresh() {
|
||||
const keep = cache.get(path.join(ROOT, "A00_Common", "b_page_state.ts"));
|
||||
cache = new Map([[path.join(ROOT, "A00_Common", "b_page_state.ts"), keep]]);
|
||||
store = loadTs(path.join(ROOT, "B06_Section", "B06_Section_Section_Store.ts"));
|
||||
keys = loadTs(path.join(ROOT, "B06_Section", "B06_Section_Station_Keys.ts"));
|
||||
}
|
||||
|
||||
const sec = (c, kind) => ({
|
||||
chainage_m: c,
|
||||
kind,
|
||||
samples: [],
|
||||
design: { design_elevation_m: 100 },
|
||||
});
|
||||
// 저장 상세: 340 · 관 351 · 360
|
||||
const saved = {
|
||||
longitudinal: { design_profiles: [], stations: [] },
|
||||
cross_sections: [sec(340, "regular"), sec(351, "irregular"), sec(360, "regular")],
|
||||
};
|
||||
const at = (c) => ({ key: `at:${c.toFixed(2)}`, chainageM: c });
|
||||
const savedPipes = [{ id: "p1", chainage_m: 351 }];
|
||||
const draftPipes = [{ id: "p1", chainage_m: 361 }];
|
||||
const cell = () => state.readState("crossdesign", P, ROUTE);
|
||||
const keysNow = () =>
|
||||
keys.registeredKeys(P, ROUTE, store.peekSectionDetail(P, ROUTE).cross_sections, 20);
|
||||
|
||||
(async () => {
|
||||
const out = {};
|
||||
server = saved;
|
||||
state.writeState("section-detail", saved, P, ROUTE);
|
||||
state.writeState("pipes", savedPipes, P);
|
||||
state.writeState("crossdesign", { before: true }, P, ROUTE);
|
||||
// 탭 1 — 열기 · 노선 초안 시작(hold) · 초안이 세션 칸 · 관을 초안 자리로 옮겨 씀
|
||||
const opened = await store.loadSectionDetail(P, ROUTE);
|
||||
const first = store.holdSectionDetail(P, ROUTE, opened);
|
||||
const kept = state.readState("route-draft-hold", P);
|
||||
out.start = {
|
||||
kept: !!kept && kept.routeId === ROUTE,
|
||||
cells: JSON.stringify(first.cells.find(([n]) => n === "crossdesign")[1]),
|
||||
pipes: first.facilities.pipes.map((p) => p.chainage_m),
|
||||
keys: first.keys.map((k) => k.chainageM),
|
||||
};
|
||||
state.writeState("route-draft", { nodes: [], rev: 1 }, P);
|
||||
state.writeState("crossdesign", { draft: true }, P, ROUTE);
|
||||
state.writeState("pipes", draftPipes, P);
|
||||
|
||||
// 새로고침(초안 둔 채) — 새 모듈 · 세션 그대로 · 상세 = 세션(초안 관 자리로 얹힘)
|
||||
refresh();
|
||||
calls.length = 0;
|
||||
const reopened = await store.loadSectionDetail(P, ROUTE);
|
||||
const hold = store.holdSectionDetail(P, ROUTE, reopened);
|
||||
out.refresh = {
|
||||
cells: JSON.stringify(hold.cells.find(([n]) => n === "crossdesign")[1]),
|
||||
pipes: hold.facilities.pipes.map((p) => p.chainage_m),
|
||||
keys: JSON.stringify(hold.keys) === JSON.stringify(first.keys),
|
||||
pipeShownAt: reopened.cross_sections.filter((s) => s.pipeAt).map((s) => s.chainage_m),
|
||||
};
|
||||
// 묶음이 초안을 버림 — 가게 관 = hold 관(묶음 몫 흉내) → 되돌림
|
||||
state.writeState("route-draft", null, P);
|
||||
state.writeState("pipes", hold.facilities.pipes, P);
|
||||
calls.length = 0;
|
||||
const back = await store.restoreSectionDetail(
|
||||
P,
|
||||
ROUTE,
|
||||
hold,
|
||||
saved.cross_sections.map((s) => at(s.chainage_m)),
|
||||
);
|
||||
out.drop = {
|
||||
same: back && back.same,
|
||||
cells: JSON.stringify(cell()),
|
||||
kept: state.readState("route-draft-hold", P),
|
||||
calls: [...calls],
|
||||
pipeShownAt: back.detail.cross_sections.filter((s) => s.pipeAt).map((s) => s.chainage_m),
|
||||
keys: JSON.stringify(keysNow()) === JSON.stringify(first.keys.map((k) => k.key)),
|
||||
};
|
||||
|
||||
// 새 초안(route-draft 없음) = 지금 값을 다시 뜸 · 다른 노선 hold = 안 씀
|
||||
state.writeState("crossdesign", { later: true }, P, ROUTE);
|
||||
const fresh = store.holdSectionDetail(P, ROUTE, back.detail);
|
||||
state.writeState("route-draft", { nodes: [], rev: 2 }, P);
|
||||
const other = store.holdSectionDetail(P, ROUTE + 1, back.detail);
|
||||
out.next = {
|
||||
cells: JSON.stringify(fresh.cells.find(([n]) => n === "crossdesign")[1]),
|
||||
otherRoute: state.readState("route-draft-hold", P).routeId,
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})().catch((error) => {
|
||||
process.stderr.write(String(error && error.stack ? error.stack : error));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,225 @@
|
||||
/* 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);
|
||||
});
|
||||
@@ -71,7 +71,10 @@ def test_저장_기준_입력이_심는_측점(planted):
|
||||
"both"
|
||||
] # 꺼진 종류도
|
||||
assert planted["noDetail"] is True # 상세 없음 = 종단도 기준
|
||||
assert planted["pvi"] == 2
|
||||
# 저장 꼭짓점(양 끝) + 심은 측점마다 세운 꼭짓점(427 · seatBornNodes)
|
||||
born = [c for k, c in planted["both"] if k == "irregular" and 0 < c < 60]
|
||||
assert planted["pviAt"] == sorted([0, 60, *born])
|
||||
assert planted["pvi"] == 2 + len(born)
|
||||
|
||||
|
||||
def test_늦게_심어도_안_돎(r):
|
||||
|
||||
@@ -96,7 +96,8 @@ def test_열_때_세션_상세_한_번만_풂() -> None:
|
||||
def test_세션_상세_쓰는_곳_받을_때_하나() -> None:
|
||||
store = (ROOT / "B06_Section" / "B06_Section_Section_Store.ts").read_text(encoding="utf-8")
|
||||
code = re.sub(r"/\*.*?\*/|//[^\n]*", "", store, flags=re.S)
|
||||
assert code.count('writeState("section-detail"') == 1 # 서버에서 받은 정본만
|
||||
# 서버에서 받은 정본만 — 받을 때 · 세션 판 확인(451 · 433-16)이 받은 새 판
|
||||
assert re.findall(r'writeState\("section-detail", (\w+),', code) == ["detail", "fresh"]
|
||||
cross = (ROOT / "A00_Common" / "design" / "design_pipeline_cross.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""451 노선 초안 둔 채 새로고침 hold(440-4).
|
||||
|
||||
① 초안 시작 hold = 세션 한 벌(측점 열쇠 · 횡단 초안 칸 · 가게 관 · 구조물)
|
||||
② 새로고침(모듈 다시 세움 · 세션 그대로) 뒤 hold = 그 한 벌 — 초안이 옮겨 쓴 세션 칸 · 초안 자리 관이 아님
|
||||
③ 초안 버림 = 초안 전 칸 · 초안 얹기 다시(되쓴 관 · 저장 자리 351) 뒤 초안 전 열쇠 · 세션 한 벌 지움
|
||||
④ 새 초안(노선 초안 없음) = 지금 값을 다시 뜸 · 다른 노선 한 벌은 안 씀
|
||||
⑤ 묶음 = hold 가 돌려준 관 · 구조물로 초안 전 가게(`beforeDraft`)를 채움(버릴 때 되씀)
|
||||
TS 셈은 `helper_451_refresh_hold.cjs`.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_451_refresh_hold.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).with_name("helper_451_refresh_hold.cjs")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def got() -> dict:
|
||||
run = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert run.returncode == 0, run.stderr
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
def test_초안_시작_hold_는_세션에도(got) -> None:
|
||||
start = got["start"]
|
||||
assert start["kept"] is True
|
||||
assert start["cells"] == '{"before":true}'
|
||||
assert start["pipes"] == [351] and start["keys"] == [340, 351, 360]
|
||||
|
||||
|
||||
def test_새로고침_hold_는_초안_전_한_벌(got) -> None:
|
||||
refresh = got["refresh"]
|
||||
assert refresh["cells"] == '{"before":true}' # 초안이 옮겨 쓴 {"draft":true} 아님
|
||||
assert refresh["pipes"] == [351] # 초안 자리 361 아님
|
||||
assert refresh["keys"] is True
|
||||
|
||||
|
||||
def test_버리면_초안_전_칸_관_열쇠(got) -> None:
|
||||
drop = got["drop"]
|
||||
assert drop["same"] is True
|
||||
assert drop["cells"] == '{"before":true}'
|
||||
assert drop["kept"] is None
|
||||
assert drop["calls"] == ["stations:351"] # 되쓴 관으로 초안 얹기 다시
|
||||
assert drop["pipeShownAt"] == [351] # 관 제원 = 제 카드
|
||||
assert drop["keys"] is True
|
||||
|
||||
|
||||
def test_새_초안은_다시_뜸(got) -> None:
|
||||
assert got["next"]["cells"] == '{"later":true}'
|
||||
assert got["next"]["otherRoute"] == 4
|
||||
|
||||
|
||||
def test_묶음이_초안_전_가게를_hold_로() -> None:
|
||||
page = (ROOT / "A00_Common/design/design_pipeline_page.ts").read_text(encoding="utf-8")
|
||||
hold = page[page.index("const holdNow") :]
|
||||
hold = hold[: hold.index("};")]
|
||||
assert "options.holdDraft?.(routeId, detail, (f) => (beforeDraft ??= f))" in hold
|
||||
shared = (ROOT / "A00_Common/design/design_shared.ts").read_text(encoding="utf-8")
|
||||
draft = shared[shared.index("holdDraft: (routeId, saved, seed) => {") :]
|
||||
assert "if (hold) seed?.(hold.facilities);" in draft[: draft.index("return async")]
|
||||
@@ -0,0 +1,71 @@
|
||||
"""451 B06 세션 캐시 판 · 숨은 종단 보기 관 id 장부(433-16 · 440-3).
|
||||
|
||||
① 새로고침 · 새 탭 = 세션 상세로 먼저 섬 · 뒤에서 서버 판을 물음 — 다른 탭이 저장해 다르면 서버 값 위에
|
||||
이 탭에서 고친 사용자 칸만 · [저장] patch 바탕도 서버 값 · 세션 = 서버 정본 그대로
|
||||
② 같은 판 · 서버 실패 = 세션 그대로(갈아끼움 · 알림 없음)
|
||||
③ 노선 초안이 옛 상세를 쥔 채 서버 판이 바뀜 — 초안을 버리면 새 판
|
||||
④ 숨은 종단 보기도 관 세션 id 장부를 돌림(평면 마커 누름 = 초안 자리) · 그리기는 보일 때
|
||||
TS 셈은 `helper_451_session_canon.cjs`.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_451_session_canon.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).with_name("helper_451_session_canon.cjs")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def got() -> dict:
|
||||
run = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert run.returncode == 0, run.stderr
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
def test_첫_화면은_세션_뒤에서_판_확인(got) -> None:
|
||||
assert got["firstFromSession"] is True
|
||||
|
||||
|
||||
def test_판이_다르면_서버_값_위에_내_칸만(got) -> None:
|
||||
stale = got["stale"]
|
||||
assert stale["swapped"] is True and stale["heard"] == 1
|
||||
assert stale["adjust"] == 2 and stale["elevation"] == 101 # 다른 탭이 저장한 값
|
||||
assert stale["note"] == "mine" # 이 탭에서 고친 칸
|
||||
assert stale["patchAdjust"] == 2 and stale["patchNote"] == "mine" # [저장] 바탕
|
||||
assert stale["sessionAdjust"] == 2 and stale["sessionNote"] == "A" # 세션 = 서버 정본
|
||||
|
||||
|
||||
def test_같은_판_실패는_그대로(got) -> None:
|
||||
assert got["same"] == {"kept": True, "heard": 0}
|
||||
assert got["broken"] is True
|
||||
|
||||
|
||||
def test_초안_hold_는_버리면_새_판(got) -> None:
|
||||
hold = got["hold"]
|
||||
assert hold["swappedDraft"] is True and hold["draftKept"] == [0, 25, 45, 60]
|
||||
assert hold["renewed"] is True and hold["adjust"] == 2 and hold["same"] is True
|
||||
|
||||
|
||||
def test_숨은_종단_보기도_관_id_장부() -> None:
|
||||
view = (ROOT / "A00_Common/design/design_view_profile.ts").read_text(encoding="utf-8")
|
||||
follow = view[view.index("const followFacilities") : view.index("const selectGraphAt")]
|
||||
assert "if (!host.visible()) return void (lastPipes && shownPipes(lastPipes));" in follow
|
||||
changed = view[view.index("onPipesChanged: (pipes) => {") :]
|
||||
changed = changed[: changed.index("},")]
|
||||
assert "else shownPipes(pipes);" in changed
|
||||
Reference in New Issue
Block a user