Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -37,7 +37,10 @@ import { pipelineDetail, reconcilePagePipeline } from "./design_pipeline_page";
|
||||
import { forgetPageData, onPageBusy } from "./design_page_data";
|
||||
import { pageRouteId } from "./design_page_route";
|
||||
import { setPipeSectionsSource } from "./design_structure_pipe_calc";
|
||||
import { peekSectionDetail } from "../../B06_Section/B06_Section_Section_Store";
|
||||
import {
|
||||
forgetSectionMemory,
|
||||
peekSectionDetail,
|
||||
} from "../../B06_Section/B06_Section_Section_Store";
|
||||
import "./design_page.css";
|
||||
|
||||
export type { DesignPageShared } from "./design_shared";
|
||||
@@ -288,6 +291,7 @@ export function renderDesignPage(root: HTMLElement): MountedPage {
|
||||
shared.lane.dispose();
|
||||
stopBusy();
|
||||
forgetPageData(projectId, true); // 다시 열면 저장분 · 노선 id 새로(386-2)
|
||||
forgetSectionMemory(projectId); // 다시 열면 상세 = 새로고침 길(세션 정본 + 초안 · 466)
|
||||
delete (window as unknown as { __designPage?: unknown }).__designPage;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,13 +15,15 @@
|
||||
* 고리(365-2) — 열 때 · 들어올 때 입력 대 묶음 입력 · 다르면 한 바퀴 · 최대 3 · 넘으면 `onUnstable`.
|
||||
* 출력 — 가게 stations · profile · footprint · generation · 노선 초안이 있으면 종단도가 새 첫 계획선 ·
|
||||
* 옮긴 계획고 · 새 노선 상세(`pipelineDetail` — 지반 · 측점 · 횡단 면적 → 유토곡선)를 그림.
|
||||
* 첫 계산 = 열 때 한 번(저장 노선 · 발자국 · 저장 안 한 노선 초안이 있으면 그 노선).
|
||||
* 노선 초안을 버리면(되돌리기 · [저장 전으로]) 열 때처럼 — 측점 · 종단을 안 내고 초안 전 상세 · 횡단 초안 칸
|
||||
* 그대로(349-0k · 349-0o) · 그 뒤 같은 측점 목록은 가게에 다시 안 냄(상세 측점이 묶음과 달라도 · 349-0q).
|
||||
* 첫 계산 = 열 때 한 번(저장 노선 · 발자국 · 저장 안 한 노선 초안이 있으면 그 노선) · 관 · 구조물 · 추가 측점
|
||||
* 초안이 있으면 측점을 냄(새로고침 · 다시 열기 = 초안 측점 · 466).
|
||||
* 노선 초안을 버리면(되돌리기 · [저장 전으로]) 초안 전 상세 · 횡단 초안 칸 그대로(349-0k · 349-0o) · 가게 측점 ·
|
||||
* 종단 = 그 상세 측점 · 저장 기준(466) · 그 뒤 같은 측점 목록은 가게에 다시 안 냄(상세 측점이 묶음과 달라도 · 349-0q).
|
||||
* [저장](sub5)이 서버 정본 — 노선을 먼저 보내고 `pendingAlignment(DRAFT_ROUTE_ID)` 를 새 노선에 보냄.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createDesignPipeline, publishToStore, type DesignPipeline } from "./logic/design_pipeline";
|
||||
import { createDesignPipeline, type DesignPipeline } from "./logic/design_pipeline";
|
||||
import { publishPagePipeline } from "./design_pipeline_page_publish";
|
||||
import { SOURCE_INPUT } from "./logic/design_pipeline_types";
|
||||
export { pipelineWork } from "./logic/design_pipeline";
|
||||
import { pipelineCrossSource, type PipelineCrossSource } from "./design_pipeline_cross";
|
||||
@@ -30,7 +32,6 @@ import type {
|
||||
ChangeSource,
|
||||
FollowPatch,
|
||||
PipelineInputs,
|
||||
PipelineOutputs,
|
||||
PipelineRunRecord,
|
||||
PipelineStation,
|
||||
RangePart,
|
||||
@@ -56,7 +57,11 @@ import {
|
||||
readAlignmentDraft,
|
||||
} from "../../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||
import { readAlignment } from "../../B05_Profile/logic/B05_Profile_UI_Profile_Data";
|
||||
import { followPlusDraft, plusStationsOf } from "../../B05_Profile/B05_Profile_Api_PlusStations";
|
||||
import {
|
||||
followPlusDraft,
|
||||
plusStationsOf,
|
||||
readPlusDraft,
|
||||
} from "../../B05_Profile/B05_Profile_Api_PlusStations";
|
||||
import { pageReplaying } from "../../B05_Profile/B05_Profile_UI_Profile_History";
|
||||
import {
|
||||
analysisTerrain,
|
||||
@@ -380,27 +385,7 @@ function createPagePipeline(
|
||||
},
|
||||
},
|
||||
});
|
||||
const publish = publishToStore(store);
|
||||
/** 상세가 맞춘 측점 목록(열쇠 · 자리) — 열 때 · 되돌린 차례 측점 · 마지막으로 낸 측점. 같은 목록이면 가게에
|
||||
* 다시 안 냄(되돌린 상세 측점이 묶음 측점과 달라도 · 관 · 구조물 메아리가 상세를 갈아끼우지 않게 · 349-0q). */
|
||||
let stationsShown: string | null = null;
|
||||
pipe.subscribe((event) => {
|
||||
const saved = event.kind === "stage" && (event.stage === "ground" || event.stage === "profile");
|
||||
const quiet = saved && event.generation === quietGeneration;
|
||||
if (event.kind === "stage" && event.stage === "ground") {
|
||||
const list = JSON.stringify(
|
||||
(event.value as PipelineOutputs["ground"] | null)?.stations.map((station) => [
|
||||
station.key,
|
||||
station.chainageM,
|
||||
station.x,
|
||||
station.y,
|
||||
]) ?? null,
|
||||
);
|
||||
if (!quiet && list === stationsShown) return;
|
||||
stationsShown = list;
|
||||
}
|
||||
if (!quiet) publish(event);
|
||||
}, "가게");
|
||||
const publisher = publishPagePipeline(pipe, store, () => quietGeneration);
|
||||
/** 노선 초안을 버린 차례 끝 — 초안 전 상세로 되돌림(열 때와 같은 상태 · 측점이 달라도 · 다른 자리는
|
||||
* 기록에) · 그 상세가 저장 정본(다시 하기도 같은 기준). 차례 끝에서 판정 — ground 를 건너뛰어도 · 사진첩
|
||||
* 되돌리기가 가게 초안을 먼저 비워도(349-0m). */
|
||||
@@ -429,6 +414,9 @@ function createPagePipeline(
|
||||
if (diff) record.restoreDiff = diff;
|
||||
}
|
||||
why = await restore(stations).catch(() => "error");
|
||||
// 가게 측점 · 종단 = 되돌린 상세 · 저장 기준(초안 측점이 남지 않게 · 466)
|
||||
if (why.startsWith("saved") || why === "no-saved")
|
||||
await publisher.shown(projectId, routeId as number).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
Object.assign(record, { restored: why.startsWith("saved"), restoreWhy: why });
|
||||
@@ -548,8 +536,12 @@ function createPagePipeline(
|
||||
const run = (lastRun = pipe.start(inputs));
|
||||
trackPageBusy(projectId, run); // 「계산 중」 점(386-2)
|
||||
openGeneration = pipe.generation;
|
||||
quietGeneration = inputs.route ? -1 : pipe.generation;
|
||||
void run.then(() => reconcile()); // 열 때 셈하는 사이 바뀐 입력(세션 · 가게)도 맞춤
|
||||
// 관 · 구조물 · 추가 측점 초안이 있으면 측점을 냄 — 새로고침 · 다시 열기 = 초안 측점(갈아끼움 · 466)
|
||||
const drafts = store.get("pipes") ?? store.get("structures") ?? readPlusDraft(projectId);
|
||||
quietGeneration = inputs.route || drafts ? -1 : pipe.generation;
|
||||
void run
|
||||
.then(() => reconcile()) // 열 때 셈하는 사이 바뀐 입력(세션 · 가게)도 맞춤
|
||||
.then(() => publisher.opened(projectId, openGeneration, canon?.routeId)); // 466
|
||||
};
|
||||
|
||||
/** 묶음에 넘긴 입력(follow 가 고치기 전) · 마지막으로 띄운 차례 — 고리가 견줌 · 기다림. */
|
||||
@@ -677,8 +669,10 @@ function createPagePipeline(
|
||||
},
|
||||
restart() {
|
||||
if (!started) return start();
|
||||
// 갈아끼움 · 같은 상세 다시 그림은 안 셈 · 보기가 심은 측점은 묶음이 입력으로 셈(386-3)
|
||||
// 갈아끼움 · 같은 상세 다시 그림은 안 셈 · 보기가 심은 측점은 묶음이 입력으로 셈(386-3) · 화면 선만
|
||||
// 바뀜(비정규 측점 도착) = 상세가 그 선 대비 낡았으면 cross 한 번 더(466)
|
||||
if (canonChanged()) void loadPlan().then(start);
|
||||
else if (typeof canon?.routeId === "number") publisher.recheck(projectId, canon.routeId);
|
||||
},
|
||||
earthwork() {
|
||||
const cross = crossNow()?.input ?? null;
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/design_pipeline_page_publish.ts
|
||||
* 페이지 계산 묶음 → 가게(stations · profile · footprint · generation) 잇기(`design_pipeline_page` 700줄 쪼갬 · 466).
|
||||
*
|
||||
* 조용한 차례(열 때 · 노선 초안을 버린 차례)는 측점 · 종단을 바로 안 냄(상세 = 저장 정본 · 초안 전 상세 그대로) ·
|
||||
* 그 목록을 「보인 측점」으로 적어 뒤 같은 목록 차례도 안 냄(관 · 구조물 메아리가 상세를 갈아끼우지 않게 · 349-0q).
|
||||
* 조용한 차례 끝(열 때 · 버린 차례의 되돌림 뒤) = 가게 측점 · 종단 = 지금 상세가 쥔 측점 · 그 차례 저장 기준 종단
|
||||
* (466 · 요구 272) — 상세에서 지은 목록이라 갈아끼움은 「같은 목록」(349-0o 그대로) · 가게에 초안 측점이 안 남음.
|
||||
* 화면 선만 바뀜(정본 그대로) = 상세가 그 선 대비 낡았으면 cross 한 번 더(`recrossIfStale` · 466).
|
||||
* 상세 가게는 늦게 불러옴(가게 ↔ 묶음 순환 없음 · Section_Store `recrossSaved` 와 같은 꼴).
|
||||
* ========================================================================== */
|
||||
|
||||
import { publishToStore, type DesignPipeline } from "./logic/design_pipeline";
|
||||
import type { PipelineOutputs, PipelineStation, StationKind } from "./logic/design_pipeline_types";
|
||||
import type { DesignStore } from "./design_store";
|
||||
import type { CrossSection } from "../../B06_Section/B06_Section_Api_Fetch";
|
||||
|
||||
export interface PagePublisher {
|
||||
/** 조용한 차례 끝(열 때 · 노선 초안을 버린 되돌림 뒤) — 가게 측점 = 지금 상세 측점 · 종단 = 그 차례 종단.
|
||||
* 상세 없음(떠남 · 아직 안 받음) · 뒤 차례가 떴으면 안 냄(그 차례 몫). */
|
||||
shown(projectId: string, routeId: number): Promise<void>;
|
||||
/** 열고 맞춤까지 끝 — 그 뒤로 낸 측점이 없으면(조용히 엶) `shown`(지난 페이지 초안 측점이 남지 않게). */
|
||||
opened(projectId: string, since: number, routeId: unknown): Promise<void>;
|
||||
/** 화면 선만 바뀜 — 상세가 그 선 대비 낡았으면 다 돈 뒤 cross 한 번 더. */
|
||||
recheck(projectId: string, routeId: number): void;
|
||||
}
|
||||
|
||||
/** 열쇠 머리 → 측점 종류(`grid:` · `pipe:` · `structure:` · `end:` · `curve:`). */
|
||||
const kindOfKey = (key: string): StationKind => {
|
||||
const head = key.slice(0, key.indexOf(":"));
|
||||
return (["grid", "pipe", "structure", "end", "curve"] as const).find((k) => k === head) ?? "grid";
|
||||
};
|
||||
|
||||
/** 상세 횡단 → 가게 측점 꼴(같은 자리 · 열쇠 묶음 측점이 없을 때 — 자리 · 축은 상세 것 · 지반은 안 실음). */
|
||||
function fromSection(section: CrossSection, key: string): PipelineStation {
|
||||
const kind = kindOfKey(key);
|
||||
const raw = section as unknown as {
|
||||
center_x?: number;
|
||||
center_y?: number;
|
||||
azimuth_deg?: number;
|
||||
frame?: { left_xy?: [number, number] };
|
||||
};
|
||||
return {
|
||||
key,
|
||||
chainageM: section.chainage_m,
|
||||
x: raw.center_x ?? 0,
|
||||
y: raw.center_y ?? 0,
|
||||
azimuthDeg: raw.azimuth_deg ?? 0,
|
||||
left: raw.frame?.left_xy ?? [0, 0],
|
||||
kind,
|
||||
id: kind === "pipe" || kind === "structure" ? key.slice(key.indexOf(":") + 1) : null,
|
||||
ground: null,
|
||||
planRadiusM: null,
|
||||
curveOuterSide: null,
|
||||
curveWideningM: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 묶음 출력을 가게에 냄 — `quietGeneration` = 측점 · 종단을 안 내는 차례 번호(부를 때마다 읽음). */
|
||||
export function publishPagePipeline(
|
||||
pipe: DesignPipeline,
|
||||
store: DesignStore,
|
||||
quietGeneration: () => number,
|
||||
): PagePublisher {
|
||||
const publish = publishToStore(store);
|
||||
/** 상세가 맞춘 측점 목록(열쇠 · 자리) — 열 때 · 되돌린 차례 측점 · 마지막으로 낸 측점. 같은 목록이면 가게에
|
||||
* 다시 안 냄(되돌린 상세 측점이 묶음 측점과 달라도 · 관 · 구조물 메아리가 상세를 갈아끼우지 않게 · 349-0q). */
|
||||
let stationsShown: string | null = null;
|
||||
/** 마지막으로 가게에 측점을 낸 차례 번호. */
|
||||
let published = -1;
|
||||
pipe.subscribe((event) => {
|
||||
const saved = event.kind === "stage" && (event.stage === "ground" || event.stage === "profile");
|
||||
const quiet = saved && event.generation === quietGeneration();
|
||||
if (event.kind === "stage" && event.stage === "ground") {
|
||||
const list = JSON.stringify(
|
||||
(event.value as PipelineOutputs["ground"] | null)?.stations.map((station) => [
|
||||
station.key,
|
||||
station.chainageM,
|
||||
station.x,
|
||||
station.y,
|
||||
]) ?? null,
|
||||
);
|
||||
if (!quiet && list === stationsShown) return;
|
||||
stationsShown = list;
|
||||
}
|
||||
if (!quiet) publish(event);
|
||||
if (!quiet && event.kind === "stage" && event.stage === "ground") published = event.generation;
|
||||
}, "가게");
|
||||
|
||||
const publisher: PagePublisher = {
|
||||
async opened(projectId, since, routeId) {
|
||||
if (typeof routeId !== "number" || published >= since || pipe.input("route")) return;
|
||||
await publisher.shown(projectId, routeId).catch(() => undefined);
|
||||
},
|
||||
async shown(projectId, routeId) {
|
||||
const generation = pipe.generation;
|
||||
const [{ peekSectionDetail }, { registeredKeys, stationIntervalOf }] = await Promise.all([
|
||||
import("../../B06_Section/B06_Section_Section_Store"),
|
||||
import("../../B06_Section/B06_Section_Station_Keys"),
|
||||
]);
|
||||
if (pipe.generation !== generation || pipe.input("route")) return;
|
||||
const detail = peekSectionDetail(projectId, routeId);
|
||||
if (!detail) return;
|
||||
const sections = detail.cross_sections;
|
||||
const keys = registeredKeys(projectId, routeId, sections, stationIntervalOf(detail));
|
||||
const ground = pipe.output("ground")?.stations ?? [];
|
||||
const stations = sections.map((section, i): PipelineStation => {
|
||||
const hit = ground.find(
|
||||
(station) =>
|
||||
station.key === keys[i] && Math.abs(station.chainageM - section.chainage_m) < 1e-3,
|
||||
);
|
||||
return hit ? { ...hit, chainageM: section.chainage_m } : fromSection(section, keys[i]);
|
||||
});
|
||||
store.set("stations", stations);
|
||||
store.set("profile", pipe.output("profile"));
|
||||
},
|
||||
recheck(projectId, routeId) {
|
||||
void import("../../B06_Section/B06_Section_Section_Store")
|
||||
.then((section) => section.recrossIfStale(projectId, routeId))
|
||||
.catch(() => undefined);
|
||||
},
|
||||
};
|
||||
return publisher;
|
||||
}
|
||||
@@ -293,6 +293,14 @@ function recrossSaved(
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
/** 화면 계획선이 바뀜(비정규 측점 도착 · 다시 그림 — 정본은 그대로 · 466) — 지금 상세가 그 선 대비 낡았으면
|
||||
* 다 돈 뒤 cross 한 번 더(열 때 시설 줄이 측점을 늦게 내 그 사이 차례가 옛 선으로 셈한 측점). */
|
||||
export function recrossIfStale(projectId: string, routeId: number): void {
|
||||
const key = keyOf(projectId, routeId);
|
||||
const detail = cache.get(key);
|
||||
if (detail) recrossSaved(projectId, key, detail, routeId);
|
||||
}
|
||||
|
||||
/** 노선 초안 전 상태(349-0o) — 상세(초안 전 객체 그대로 · 되돌리면 캐시) · 측점 열쇠 · 측점 열쇠 칸 다섯(횡단 초안) ·
|
||||
* 가게 관 · 구조물(440-4 · 묶음이 버릴 때 되씀). */
|
||||
export interface SectionHold {
|
||||
@@ -401,6 +409,16 @@ export function invalidateSectionDetail(projectId: string, routeId?: number): vo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지를 떠남(466 · 요구 272) — 메모리 상세 · 받는 중 약속만 비움(세션 정본 · 정본 사본 그대로). 다시 열면
|
||||
* 새로고침 길(세션 정본 + `withDraftWalls`) — 초안 때 갈아끼운 측점 목록이 다음 페이지의 저장 정본으로 남지 않게.
|
||||
*/
|
||||
export function forgetSectionMemory(projectId: string): void {
|
||||
const prefix = `${projectId}:`;
|
||||
for (const map of [cache, pending])
|
||||
for (const key of [...map.keys()]) if (key.startsWith(prefix)) map.delete(key);
|
||||
}
|
||||
|
||||
/** 받아 둔 그 노선 상세(받지 않고 · 없으면 null) — 노선 = 페이지 노선 id(B05 평면 횡단 판 · 관 길이 · 416). */
|
||||
export function peekSectionDetail(
|
||||
projectId: string,
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
/* 466(요구 272) 헬퍼 — 진짜 상세 가게 · 가게 잇기(vite ssr 묶음 · helper_272_detail_memory_entry.ts).
|
||||
* node helper_272_detail_memory.mjs <묶음.mjs>
|
||||
* ① 관 초안 갈아끼움 → [폐기] → 페이지 떠남(forgetSectionMemory) → 다시 읽기 = 세션 정본(새로고침 길)
|
||||
* ② 묶음 출력 → 가게 — 조용한 차례 · 버린 차례 끝(shown) · 열고 맞춤 끝(opened) · 메아리
|
||||
* 표준출력 JSON 한 벌. 짝 = test_272_detail_memory.py · test_272_drop_store_stations.py. */
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const storage = () => {
|
||||
const m = new Map();
|
||||
return {
|
||||
getItem: (k) => (m.has(k) ? m.get(k) : null),
|
||||
setItem: (k, v) => m.set(k, String(v)),
|
||||
removeItem: (k) => m.delete(k),
|
||||
key: (i) => [...m.keys()][i] ?? null,
|
||||
get length() {
|
||||
return m.size;
|
||||
},
|
||||
clear: () => m.clear(),
|
||||
};
|
||||
};
|
||||
globalThis.window = globalThis;
|
||||
globalThis.sessionStorage = storage();
|
||||
globalThis.localStorage = storage();
|
||||
const any = new Proxy(function () {}, {
|
||||
get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any),
|
||||
apply: () => any,
|
||||
construct: () => any,
|
||||
});
|
||||
globalThis.document = any;
|
||||
globalThis.requestAnimationFrame = (run) => setTimeout(run, 0);
|
||||
let fetches = 0; // 상세 GET 만 셈
|
||||
globalThis.fetch = (url) => {
|
||||
if (/\/sections\/\d+\/detail/.test(String(url))) fetches += 1;
|
||||
return Promise.reject(new Error("서버 없음"));
|
||||
};
|
||||
|
||||
const m = await import(pathToFileURL(process.argv[2]).href);
|
||||
const P = "p272";
|
||||
const R = 7;
|
||||
localStorage.setItem("frd_current_project_id", P);
|
||||
|
||||
const section = (chainage_m, kind) => ({
|
||||
station_id: `station_${String(Math.round(chainage_m * 1000)).padStart(12, "0")}`,
|
||||
chainage_m,
|
||||
label: "",
|
||||
kind,
|
||||
center_z: 100,
|
||||
azimuth_deg: 90,
|
||||
center_x: chainage_m,
|
||||
center_y: 0,
|
||||
frame: { left_xy: [0, 1] },
|
||||
samples: [{ offset_m: 0, elevation_m: 100, valid: true }],
|
||||
design: { design_elevation_m: 101 },
|
||||
});
|
||||
const saved = {
|
||||
longitudinal: {
|
||||
length_m: 60,
|
||||
stations: [],
|
||||
profile_alignment: { policy: { station_interval_m: 20 } },
|
||||
},
|
||||
cross_sections: [
|
||||
section(0, "bp"),
|
||||
section(20, "regular"),
|
||||
section(40, "regular"),
|
||||
section(60, "ep"),
|
||||
],
|
||||
};
|
||||
const station = (key, chainageM, kind, x = chainageM) => ({
|
||||
key,
|
||||
chainageM,
|
||||
x,
|
||||
y: 0,
|
||||
azimuthDeg: 90,
|
||||
left: [0, 1],
|
||||
kind,
|
||||
id: kind === "pipe" ? key.slice(5) : null,
|
||||
ground: { centerZ: 99, left: [{ offsetM: 0.5, z: 99.1 }], right: [{ offsetM: 0.5, z: 98.9 }] },
|
||||
planRadiusM: null,
|
||||
curveOuterSide: null,
|
||||
curveWideningM: null,
|
||||
});
|
||||
const chainages = (detail) => detail?.cross_sections.map((s) => s.chainage_m) ?? null;
|
||||
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
const shop = m.useDesignStore(P, null);
|
||||
const out = {};
|
||||
|
||||
// ── ① 상세 메모리 ─────────────────────────────────────────────────────────
|
||||
m.writeState("section-detail", saved, P, R);
|
||||
const first = await m.loadSectionDetail(P, R); // 세션 한 벌 → 캐시(새로고침 길)
|
||||
const keys = m.registeredKeys(P, R, first.cross_sections, m.stationIntervalOf(first));
|
||||
const saveList = () => first.cross_sections.map((s, i) => station(keys[i], s.chainage_m, "grid"));
|
||||
// 관 초안(30 m) — 묶음 측점이 늘어 갈아끼움(메모리만)
|
||||
shop.set("pipes", [{ id: "p1", chainage_m: 30, facility: "pipe" }]);
|
||||
const drafted = [...saveList()];
|
||||
drafted.splice(2, 0, station("pipe:p1", 30, "pipe"));
|
||||
const swap = await m.swapSectionStations(P, R, drafted);
|
||||
const during = await m.loadSectionDetail(P, R);
|
||||
out.memory = {
|
||||
keys,
|
||||
swapped: swap !== null,
|
||||
during: chainages(during),
|
||||
session: chainages(m.readState("section-detail", P, R)),
|
||||
};
|
||||
// [폐기] = 초안 칸 지움 — 메모리는 그대로(페이지가 떠나며 비움)
|
||||
m.discardDesignEdits(P);
|
||||
out.memory.afterDiscard = chainages(await m.loadSectionDetail(P, R));
|
||||
out.memory.pipesAfterDiscard = shop.get("pipes");
|
||||
m.forgetSectionMemory(P);
|
||||
out.memory.peekAfterForget = m.peekSectionDetail(P, R);
|
||||
const fetchesBefore = fetches;
|
||||
const again = await m.loadSectionDetail(P, R);
|
||||
out.memory.reopened = chainages(again);
|
||||
out.memory.reopenedSame = again === during;
|
||||
out.memory.canonAsked = fetches - fetchesBefore; // 새로고침과 같은 뒤 판 확인(433-16)
|
||||
// 다른 프로젝트 · 다른 노선 메모리는 그대로
|
||||
m.writeState("section-detail", saved, "other", R);
|
||||
await m.loadSectionDetail("other", R);
|
||||
m.forgetSectionMemory(P);
|
||||
out.memory.otherKept = m.peekSectionDetail("other", R) !== null;
|
||||
|
||||
// ── ② 가게 잇기 ───────────────────────────────────────────────────────────
|
||||
const fakePipe = () => {
|
||||
const fns = [];
|
||||
const outputs = {};
|
||||
const inputs = {};
|
||||
return {
|
||||
generation: 0,
|
||||
outputs,
|
||||
inputs,
|
||||
subscribe(fn) {
|
||||
fns.push(fn);
|
||||
return () => undefined;
|
||||
},
|
||||
output: (name) => outputs[name] ?? null,
|
||||
input: (name) => inputs[name] ?? null,
|
||||
emit(event) {
|
||||
fns.forEach((fn) => fn(event));
|
||||
},
|
||||
};
|
||||
};
|
||||
await m.loadSectionDetail(P, R); // 다시 연 페이지 — 세션 정본
|
||||
const pipe = fakePipe();
|
||||
let quiet = -1;
|
||||
const publisher = m.publishPagePipeline(pipe, shop, () => quiet);
|
||||
const sets = [];
|
||||
const stopSets = shop.subscribe("stations", (value) =>
|
||||
sets.push(
|
||||
value && chainages({ cross_sections: value.map((s) => ({ chainage_m: s.chainageM })) }),
|
||||
),
|
||||
);
|
||||
const ground = (gen, list) => {
|
||||
pipe.outputs.ground = { stations: list, longitudinal: { chainageM: [], z: [] } };
|
||||
pipe.emit({ kind: "stage", stage: "ground", generation: gen, value: pipe.outputs.ground });
|
||||
};
|
||||
const profile = { tag: "saved-profile" };
|
||||
// 열 때(조용한 차례) — 묶음 측점 ≠ 상세 측점(저장 상세에 없는 덧 측점 50) · 가게에 안 냄
|
||||
const pipeline = [...saveList()];
|
||||
pipeline.splice(3, 0, station("grid:x50", 50, "grid"));
|
||||
pipe.generation = 1;
|
||||
quiet = 1;
|
||||
shop.set("stations", drafted); // 지난 페이지 초안 측점이 가게에 남음
|
||||
sets.length = 0;
|
||||
ground(1, pipeline);
|
||||
out.store = { quietSets: sets.length };
|
||||
pipe.outputs.profile = profile;
|
||||
await publisher.opened(P, 1, R);
|
||||
await tick();
|
||||
out.store.opened = chainages(m.peekSectionDetail(P, R));
|
||||
out.store.openedStore = shop.get("stations").map((s) => s.chainageM);
|
||||
out.store.openedProfile = shop.get("profile")?.tag ?? null;
|
||||
out.store.openedSwap = await m.swapSectionStations(P, R, shop.get("stations"));
|
||||
// 메아리(같은 묶음 목록 · 조용하지 않은 차례) — 가게에 안 냄(349-0q)
|
||||
pipe.generation = 2;
|
||||
sets.length = 0;
|
||||
ground(2, pipeline);
|
||||
out.store.echoSets = sets.length;
|
||||
// 새 목록(관 옮김) = 냄 · 그 뒤 opened 는 안 냄(낸 차례가 더 새로움)
|
||||
pipe.generation = 3;
|
||||
sets.length = 0;
|
||||
ground(3, drafted);
|
||||
out.store.newSets = sets.length;
|
||||
await publisher.opened(P, 1, R);
|
||||
out.store.openedAfterPublish = shop.get("stations").map((s) => s.chainageM);
|
||||
// 버린 차례 끝 — 묶음 측점(초안 목록)과 상세가 달라도 가게 = 상세 측점 · 갈아끼움 없음(349-0o)
|
||||
pipe.generation = 4;
|
||||
quiet = 4;
|
||||
ground(4, drafted);
|
||||
await publisher.shown(P, R);
|
||||
out.store.dropped = shop.get("stations").map((s) => s.chainageM);
|
||||
out.store.droppedKeys = shop.get("stations").map((s) => s.key);
|
||||
out.store.droppedSwap = await m.swapSectionStations(P, R, shop.get("stations"));
|
||||
out.store.droppedDetail = chainages(m.peekSectionDetail(P, R));
|
||||
// 노선 초안이 다시 섰거나 뒤 차례가 뜸 = 안 냄 · 상세 없음(떠남) = 안 냄
|
||||
pipe.inputs.route = { rev: 1 };
|
||||
shop.set("stations", drafted);
|
||||
await publisher.shown(P, R);
|
||||
out.store.routeSkip = shop.get("stations").length;
|
||||
pipe.inputs.route = null;
|
||||
m.forgetSectionMemory(P);
|
||||
await publisher.shown(P, R);
|
||||
out.store.goneSkip = shop.get("stations").length;
|
||||
stopSets();
|
||||
await tick();
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,12 @@
|
||||
/* 466(요구 272) 시험 묶음 진입점 — 상세 가게 메모리 = 페이지 수명 · 버린 초안 차례 가게 측점(진짜 모듈 · vite ssr). */
|
||||
export {
|
||||
forgetSectionMemory,
|
||||
loadSectionDetail,
|
||||
peekSectionDetail,
|
||||
swapSectionStations,
|
||||
} from "../../B06_Section/B06_Section_Section_Store";
|
||||
export { registeredKeys, stationIntervalOf } from "../../B06_Section/B06_Section_Station_Keys";
|
||||
export { publishPagePipeline } from "../../A00_Common/design/design_pipeline_page_publish";
|
||||
export { discardDesignEdits } from "../../A00_Common/design/design_leave";
|
||||
export { useDesignStore } from "../../A00_Common/design/design_store";
|
||||
export { readState, writeState } from "../../A00_Common/b_page_state";
|
||||
@@ -0,0 +1,109 @@
|
||||
"""PLAN 466 (요구 272 · 분석 4장 (A)) — 상세 가게 메모리 = 페이지 수명.
|
||||
|
||||
못박는 것
|
||||
- 관 초안으로 갈아끼운 상세(메모리) → [폐기] → 페이지를 떠남 = 메모리 비움 → 다시 열기(B05 · B06 · B07) =
|
||||
세션 정본(새로고침 길 · 뒤에서 판 확인 한 번) · 초안 측점이 남지 않음. 다른 프로젝트 메모리는 그대로.
|
||||
- 페이지 dispose 가 비움(`design_page`) · 묶음 열 때 관 · 구조물 · 추가 측점 초안이 있으면 측점을 냄
|
||||
(초안 남긴 채 떠남 · 새로고침 = 초안 측점 · 갈아끼움).
|
||||
- 화면 선만 바뀜(비정규 측점 도착) = 상세가 그 선 대비 낡았으면 cross 한 번 더(`recrossIfStale`).
|
||||
짝: `helper_272_detail_memory_entry.ts` + `helper_272_detail_memory.mjs`(진짜 상세 가게 · vite ssr).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_272_detail_memory.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "vite").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def run_helper(work: Path) -> dict:
|
||||
"""vite ssr 로 진짜 모듈을 묶어 헬퍼를 돌림 — 466 두 시험이 같이 씀."""
|
||||
build = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
[
|
||||
"node",
|
||||
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
"../resources/tester/helper_272_detail_memory_entry.ts",
|
||||
"--outDir",
|
||||
str(work),
|
||||
"--logLevel",
|
||||
"error",
|
||||
],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=300,
|
||||
)
|
||||
assert build.returncode == 0, build.stderr
|
||||
bundle = work / "bundle.mjs"
|
||||
shutil.copy(work / "helper_272_detail_memory_entry.js", bundle)
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_272_detail_memory.mjs"), str(bundle)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def got(tmp_path_factory) -> dict:
|
||||
return run_helper(tmp_path_factory.mktemp("b272"))
|
||||
|
||||
|
||||
def test_초안_동안은_메모리만_갈아끼움(got) -> None:
|
||||
m = got["memory"]
|
||||
assert m["swapped"] and m["during"] == [0, 20, 30, 40, 60]
|
||||
assert m["session"] == [0, 20, 40, 60] # 세션 = 저장 정본
|
||||
|
||||
|
||||
def test_폐기만으론_메모리_그대로_떠나면_정본(got) -> None:
|
||||
m = got["memory"]
|
||||
assert m["pipesAfterDiscard"] is None # 관 초안 지움
|
||||
assert m["afterDiscard"] == [0, 20, 30, 40, 60] # 같은 페이지 안 = 메모리(떠날 때 비움)
|
||||
assert m["peekAfterForget"] is None
|
||||
assert m["reopened"] == [0, 20, 40, 60] and m["reopenedSame"] is False # 새로고침 길
|
||||
assert m["canonAsked"] == 1 # 세션으로 서고 뒤에서 판 확인(새로고침과 같음)
|
||||
assert m["otherKept"] is True
|
||||
|
||||
|
||||
def test_페이지가_떠날_때_비움() -> None:
|
||||
page = (ROOT / "A00_Common/design/design_page.ts").read_text(encoding="utf-8")
|
||||
dispose = page[page.index(" covers: (route)") :]
|
||||
assert "forgetSectionMemory(projectId);" in dispose[: dispose.index("\n };\n")]
|
||||
|
||||
|
||||
def test_초안_있으면_열_때_측점_냄() -> None:
|
||||
src = (ROOT / "A00_Common/design/design_pipeline_page.ts").read_text(encoding="utf-8")
|
||||
start = src[src.index(" const start = (): void => {") :]
|
||||
body = start[: start.index("\n };\n")]
|
||||
assert 'store.get("pipes") ?? store.get("structures") ?? readPlusDraft(projectId)' in body
|
||||
assert "quietGeneration = inputs.route || drafts ? -1 : pipe.generation;" in body
|
||||
|
||||
|
||||
def test_화면_선만_바뀌면_낡음_다시_셈() -> None:
|
||||
src = (ROOT / "A00_Common/design/design_pipeline_page.ts").read_text(encoding="utf-8")
|
||||
restart = src[src.index(" restart() {") :]
|
||||
assert "publisher.recheck(projectId, canon.routeId)" in restart[: restart.index(" },")]
|
||||
store = (ROOT / "B06_Section/B06_Section_Section_Store.ts").read_text(encoding="utf-8")
|
||||
fn = store[store.index("export function recrossIfStale(") :]
|
||||
assert "recrossSaved(projectId, key, detail, routeId)" in fn[: fn.index("\n}\n")]
|
||||
@@ -0,0 +1,56 @@
|
||||
"""PLAN 466 (요구 272 · 분석 4장 (A2)) — 조용한 차례(열 때 · 버린 노선 초안)도 가게 측점 = 상세 측점.
|
||||
|
||||
못박는 것
|
||||
- 조용한 차례 = 묶음 측점을 바로 안 냄 · 열고 맞춤 끝(`opened`) = 가게 측점 · 종단 = 지금 상세 측점 · 그 차례
|
||||
종단(지난 페이지 초안 측점이 안 남음) · 그 목록으로 갈아끼움 = 없음(같은 목록).
|
||||
- 메아리(같은 묶음 목록) = 안 냄(349-0q) · 새 목록 = 냄 · 그 뒤 opened 는 안 덮음.
|
||||
- 버린 차례 끝(`shown`) = 묶음 측점(초안)과 상세가 달라도 상세 측점 · 열쇠 · 갈아끼움 없음(349-0o) ·
|
||||
노선 초안이 다시 섬 · 상세 없음(떠남) = 안 냄.
|
||||
짝: `test_272_detail_memory.run_helper`(같은 헬퍼).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_272_drop_store_stations.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
if str(HERE) not in sys.path:
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
import test_272_detail_memory as base # noqa: E402
|
||||
|
||||
pytestmark = base.pytestmark
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def got(tmp_path_factory) -> dict:
|
||||
return base.run_helper(tmp_path_factory.mktemp("b272d"))["store"]
|
||||
|
||||
|
||||
def test_조용히_연_차례_가게_상세_측점(got) -> None:
|
||||
assert got["quietSets"] == 0
|
||||
assert got["opened"] == [0, 20, 40, 60]
|
||||
assert got["openedStore"] == [0, 20, 40, 60] # 지난 페이지 초안(30) 안 남음
|
||||
assert got["openedProfile"] == "saved-profile"
|
||||
assert got["openedSwap"] is None # 같은 목록 = 갈아끼움 없음
|
||||
|
||||
|
||||
def test_메아리는_안_내고_새_목록은_냄(got) -> None:
|
||||
assert got["echoSets"] == 0
|
||||
assert got["newSets"] == 1
|
||||
assert got["openedAfterPublish"] == [0, 20, 30, 40, 60] # 낸 차례가 더 새로움 — 안 덮음
|
||||
|
||||
|
||||
def test_버린_차례_끝_가게_상세_측점(got) -> None:
|
||||
assert got["dropped"] == [0, 20, 40, 60] and got["droppedDetail"] == [0, 20, 40, 60]
|
||||
assert "pipe:p1" not in got["droppedKeys"]
|
||||
assert got["droppedSwap"] is None
|
||||
|
||||
|
||||
def test_초안_다시_섬_떠남은_안_냄(got) -> None:
|
||||
assert got["routeSkip"] == 5 and got["goneSkip"] == 5
|
||||
Reference in New Issue
Block a user