diff --git a/A00_Common/design/design_pipeline_page.ts b/A00_Common/design/design_pipeline_page.ts index f5324ecec..114d9fe8c 100644 --- a/A00_Common/design/design_pipeline_page.ts +++ b/A00_Common/design/design_pipeline_page.ts @@ -26,7 +26,6 @@ import { createDesignPipeline, type DesignPipeline } from "./logic/design_pipeli 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"; import { createStationsStage, snapOptions, stationDiff } from "./logic/design_pipeline_stations"; import type { ChangeSource, @@ -48,7 +47,6 @@ import type { SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fe import { createCrossStage } from "../../B06_Section/logic/B06_Section_Cross_Stage"; import { emptyEdits, - toAlignmentBase, type AlignmentEdits, type ProfileAlignment, } from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment"; @@ -56,7 +54,6 @@ import { markAlignmentSaved, readAlignmentDraft, } from "../../B05_Profile/B05_Profile_UI_Profile_Edit"; -import { readAlignment } from "../../B05_Profile/logic/B05_Profile_UI_Profile_Data"; import { followPlusDraft, plusStationsOf, @@ -71,6 +68,7 @@ import { import { fetchStructureTypes } from "../../B05_Profile/B05_Profile_Api_Structures"; import { forgetPageData, pagePipes, pageStructures, trackPageBusy } from "./design_page_data"; import type { PagePipelineOptions, ProfileLink } from "./design_pipeline_page_types"; +import { createPageSaved } from "./design_pipeline_page_saved"; import { feedFacilities } from "./design_pipeline_facilities"; export type { PagePipelineOptions, ProfileLink } from "./design_pipeline_page_types"; import { @@ -192,24 +190,12 @@ function createPagePipeline( let enabled: PlanType[] = []; let savedStructures: PlanStructure[] = []; let savedPipes: PlanPipe[] = []; - /** 저장 노선 측점(열쇠) · 마지막으로 모은 횡단 입력(시설 제원 — 새 노선 상세가 씀). */ + /** 저장 노선 측점(열쇠) — 저장분을 받을 때 셈. */ let savedStations: PipelineStation[] = []; - let crossSource: PipelineCrossSource | null = null; - /** 저장 정본(서버가 준 상세 · 노선 번호) — 측점 갈아끼움(swap)이 만든 새 상세는 지반 표본 배열을 - * 그대로 둠 → 정본 아님(묶음 출력 → 상세 → 다시 묶음 고리를 끊음). */ - let canon: { routeId: unknown; samples: unknown; detail: SectionDetailResponse | null } | null = - null; + /** 저장 기준(정본 지문 · 종단도 저장 기준 · 계획고 · PVI · 횡단 입력) — `design_pipeline_page_saved`. */ + const pageSaved = createPageSaved(projectId, link, options, () => savedStations); + const { savedSource, canonChanged, savedEdits, storedPvi, crossNow, drafted } = pageSaved; let shown: SectionDetailResponse | null = null; - /** 이 묶음이 지은 새 노선 상세들 — 횡단 보기 카드가 그것을 종단도에 그려도 저장 정본이 아님. */ - const drafted = new WeakSet(); - let lastSaved: ReturnType | null = null; - /** 종단도가 내준 저장 기준 — 종단도가 묶음 새 노선 상세를 그리는 동안은 그 앞 저장 기준(고리 끊음 · - * 새 노선 상세 → 정본으로 오인 → 처음부터 다시 → 새 상세 … 349-0g). */ - const savedSource = (): ReturnType => { - const now = link.pipelineSource(); - if (now.detail && drafted.has(now.detail)) return lastSaved ?? { ...now, detail: null }; - return (lastSaved = now); - }; let ready = false; let started = false; /** 앞 종단이 노선 초안으로 셈한 것인가 — 초안을 버리면 저장 기준으로 다시 그림. */ @@ -240,41 +226,6 @@ function createPagePipeline( plan ? plan.planned.map(([x, y]): Vertex => [x, y]) : null; const savedCurves = () => (plan?.curves ?? []).map(({ start, apex, end, radius_m }) => ({ start, apex, end, radius_m })); - /** 저장 정본이 바뀌었나(새로 받은 상세 · 새 노선) — 바뀌었으면 잡아 둠. */ - const canonChanged = (): boolean => { - const { detail = null, routeId } = savedSource(); - const samples = detail?.longitudinal.samples; - if (canon && canon.routeId === routeId && canon.samples === samples) return false; - canon = { routeId, samples, detail }; - return true; - }; - /** 저장 노선 계획고 편집(세션 초안 → 저장분) — 노선 초안을 버리면 이것으로 돌아옴. */ - const savedEdits = (): AlignmentEdits => { - const routeId = canon?.routeId; - const longitudinal = canon?.detail?.longitudinal; - return ( - (typeof routeId === "number" ? readAlignmentDraft(routeId) : null) ?? - (longitudinal && readAlignment(longitudinal)?.edits) ?? - emptyEdits() - ); - }; - const storedPvi = () => { - const stored = canon?.detail && readAlignment(canon.detail.longitudinal); - return stored ? toAlignmentBase(stored).basePvi : null; - }; - /** 횡단 입력 — 부를 때마다 저장 상세 + 세션 고침 · 표준을 모음(상세 객체마다 기억하면 고침이 안 감 · 365-2). */ - const crossNow = (): PipelineCrossSource | null => { - const routeId = canon?.routeId; - const source = pipelineCrossSource( - projectId, - canon?.detail ?? null, - savedStations, - typeof routeId === "number" ? routeId : null, - options.stationKeys, // 초안 자리 측점 짝(457) - ); - if (source) crossSource = source; - return source; - }; const statusOf = (pipes: unknown, structures: unknown) => shapeStatus(pipes ?? savedPipes, structures ?? savedStructures, types); // 새 측점 그림 · 맥락(457) const groundDeps = groundStageDeps(projectId); @@ -395,7 +346,7 @@ function createPagePipeline( if (pushed && dropPending && !pipe.input("route")) return (lastRun as Promise).then(finishDrop); dropPending = false; - const routeId = canon?.routeId; + const routeId = pageSaved.canonRouteId(); const stations = pipe.output("ground")?.stations ?? []; let why: string = record.outcome; // 새 조작에 밀림 · 단계 실패 — 초안 전 상세는 그대로 둠 if (record.outcome === "done") { @@ -409,7 +360,7 @@ function createPagePipeline( held = null; const detail = hold?.detail; if (detail) { - canon = { routeId, samples: detail.longitudinal.samples, detail }; + pageSaved.adoptCanon(routeId, detail); const diff = stationDiff(detail, stations); if (diff) record.restoreDiff = diff; } @@ -474,7 +425,7 @@ function createPagePipeline( ground, profile, pipe.output("cross"), - crossSource?.specs ?? new Map(), + pageSaved.crossSpecs(), options.stationIntervalM ?? profile.policy.station_interval_m, statusOf(pipe.input("pipes"), pipe.input("structures")), ) @@ -541,7 +492,7 @@ function createPagePipeline( quietGeneration = inputs.route || drafts ? -1 : pipe.generation; void run .then(() => reconcile()) // 열 때 셈하는 사이 바뀐 입력(세션 · 가게)도 맞춤 - .then(() => publisher.opened(projectId, openGeneration, canon?.routeId)); // 466 + .then(() => publisher.opened(projectId, openGeneration, pageSaved.canonRouteId())); // 466 }; /** 묶음에 넘긴 입력(follow 가 고치기 전) · 마지막으로 띄운 차례 — 고리가 견줌 · 기다림. */ @@ -563,7 +514,7 @@ function createPagePipeline( let seen: Promise | null = null; while (seen !== lastRun) await (seen = lastRun); // 기다리는 사이 뜬 차례까지 await pipe.settled(); - if (disposed || canon?.routeId !== savedSource().routeId) return round; // 정본 바뀜 = restart 몫 + if (disposed || pageSaved.canonRouteId() !== savedSource().routeId) return round; // 정본 바뀜 = restart 몫 if (facilities.saving()) return (facilities.afterSave(), round); // 저장 도중 = 옛 저장분 · 끝나면 다시 const now = inputsNow(); const stale = (Object.entries(SOURCE_INPUT) as Array<[ChangeSource, keyof PipelineInputs]>) @@ -601,7 +552,7 @@ function createPagePipeline( stationSnap: routePlan.station_snap ?? false, stationSnapStep: routePlan.station_snap_step, }); - crossSource = null; + pageSaved.forgetCross(); }) .catch(() => undefined); @@ -672,7 +623,8 @@ function createPagePipeline( // 갈아끼움 · 같은 상세 다시 그림은 안 셈 · 보기가 심은 측점은 묶음이 입력으로 셈(386-3) · 화면 선만 // 바뀜(비정규 측점 도착) = 상세가 그 선 대비 낡았으면 cross 한 번 더(466) if (canonChanged()) void loadPlan().then(start); - else if (typeof canon?.routeId === "number") publisher.recheck(projectId, canon.routeId); + else if (typeof pageSaved.canonRouteId() === "number") + publisher.recheck(projectId, pageSaved.canonRouteId() as number); }, earthwork() { const cross = crossNow()?.input ?? null; diff --git a/A00_Common/design/design_pipeline_page_saved.ts b/A00_Common/design/design_pipeline_page_saved.ts new file mode 100644 index 000000000..506cfa72d --- /dev/null +++ b/A00_Common/design/design_pipeline_page_saved.ts @@ -0,0 +1,97 @@ +/* ============================================================================= + * A00_Common/design/design_pipeline_page_saved.ts + * 페이지 묶음의 **저장 기준** — 저장 정본 지문(노선 번호 · 지반 표본) · 종단도가 내준 저장 기준 · 저장 노선 + * 계획고 편집 · 저장 PVI · 횡단 입력(cross). `design_pipeline_page.ts` 에서 갈라냄(700줄 한계 · 472-5). + * ========================================================================== */ + +import type { SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch"; +import { + emptyEdits, + toAlignmentBase, + type AlignmentEdits, +} from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment"; +import { readAlignmentDraft } from "../../B05_Profile/B05_Profile_UI_Profile_Edit"; +import { readAlignment } from "../../B05_Profile/logic/B05_Profile_UI_Profile_Data"; +import { pipelineCrossSource, type PipelineCrossSource } from "./design_pipeline_cross"; +import type { PipelineStation } from "./logic/design_pipeline_types"; +import type { PagePipelineOptions, ProfileLink } from "./design_pipeline_page_types"; + +export function createPageSaved( + projectId: string, + link: ProfileLink, + options: PagePipelineOptions, + /** 저장 노선 측점(열쇠) — 페이지가 저장분을 받을 때 셈. */ + savedStations: () => PipelineStation[], +) { + /** 저장 정본(서버가 준 상세 · 노선 번호) — 측점 갈아끼움(swap)이 만든 새 상세는 지반 표본 배열을 + * 그대로 둠 → 정본 아님(묶음 출력 → 상세 → 다시 묶음 고리를 끊음). */ + let canon: { routeId: unknown; samples: unknown; detail: SectionDetailResponse | null } | null = + null; + /** 마지막으로 모은 횡단 입력(시설 제원 — 새 노선 상세가 씀). */ + let crossSource: PipelineCrossSource | null = null; + /** 이 묶음이 지은 새 노선 상세들 — 횡단 보기 카드가 그것을 종단도에 그려도 저장 정본이 아님. */ + const drafted = new WeakSet(); + let lastSaved: ReturnType | null = null; + /** 종단도가 내준 저장 기준 — 종단도가 묶음 새 노선 상세를 그리는 동안은 그 앞 저장 기준(고리 끊음 · + * 새 노선 상세 → 정본으로 오인 → 처음부터 다시 → 새 상세 … 349-0g). */ + const savedSource = (): ReturnType => { + const now = link.pipelineSource(); + if (now.detail && drafted.has(now.detail)) return lastSaved ?? { ...now, detail: null }; + return (lastSaved = now); + }; + /** 저장 정본이 바뀌었나(새로 받은 상세 · 새 노선) — 바뀌었으면 잡아 둠. */ + const canonChanged = (): boolean => { + const { detail = null, routeId } = savedSource(); + const samples = detail?.longitudinal.samples; + if (canon && canon.routeId === routeId && canon.samples === samples) return false; + canon = { routeId, samples, detail }; + return true; + }; + /** 저장 노선 계획고 편집(세션 초안 → 저장분) — 노선 초안을 버리면 이것으로 돌아옴. */ + const savedEdits = (): AlignmentEdits => { + const routeId = canon?.routeId; + const longitudinal = canon?.detail?.longitudinal; + return ( + (typeof routeId === "number" ? readAlignmentDraft(routeId) : null) ?? + (longitudinal && readAlignment(longitudinal)?.edits) ?? + emptyEdits() + ); + }; + const storedPvi = () => { + const stored = canon?.detail && readAlignment(canon.detail.longitudinal); + return stored ? toAlignmentBase(stored).basePvi : null; + }; + /** 횡단 입력 — 부를 때마다 저장 상세 + 세션 고침 · 표준을 모음(상세 객체마다 기억하면 고침이 안 감 · 365-2). */ + const crossNow = (): PipelineCrossSource | null => { + const routeId = canon?.routeId; + const source = pipelineCrossSource( + projectId, + canon?.detail ?? null, + savedStations(), + typeof routeId === "number" ? routeId : null, + options.stationKeys, // 초안 자리 측점 짝(457) + ); + if (source) crossSource = source; + return source; + }; + return { + savedSource, + canonChanged, + savedEdits, + storedPvi, + crossNow, + drafted, + /** 저장 정본 노선 번호(정본을 아직 안 잡았으면 undefined). */ + canonRouteId: (): unknown => canon?.routeId, + /** 초안을 버려 되돌린 상세 = 저장 정본(다시 하기도 같은 기준). */ + adoptCanon(routeId: unknown, detail: SectionDetailResponse): void { + canon = { routeId, samples: detail.longitudinal.samples, detail }; + }, + /** 마지막 횡단 입력의 시설 제원(새 노선 상세가 씀). */ + crossSpecs: () => crossSource?.specs ?? new Map(), + /** 저장분을 다시 받음 — 횡단 입력도 다시 모음. */ + forgetCross(): void { + crossSource = null; + }, + }; +} diff --git a/resources/tester/test_272_detail_memory.py b/resources/tester/test_272_detail_memory.py index 1d5fdbc6d..e79e5e2e9 100644 --- a/resources/tester/test_272_detail_memory.py +++ b/resources/tester/test_272_detail_memory.py @@ -103,7 +103,11 @@ def test_초안_있으면_열_때_측점_냄() -> None: 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(" },")] + # 정본 지문은 `design_pipeline_page_saved`(472-5 나눔) — 노선 번호를 거기서 읽음 + assert ( + "publisher.recheck(projectId, pageSaved.canonRouteId()" + 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")]