diff --git a/A00_Common/b_page_state.ts b/A00_Common/b_page_state.ts index 53711a03..9c05c761 100644 --- a/A00_Common/b_page_state.ts +++ b/A00_Common/b_page_state.ts @@ -107,8 +107,6 @@ export const STATE_REGISTRY = { culvertopt: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:culvertopt:${p}:${r}` }, /** 배수관 이동(측점 옮김) 예약. */ culvertmove: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:culvertmove:${p}:${r}` }, - /** 배수관 추가·삭제 예약(2026-09-12) — B06 종단·목록에서 넣고 뺀 관. */ - culvertedit: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:culvertedit:${p}:${r}` }, /** 암 경계선 오프셋(측점별). */ rockb: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:rockb:${p}:${r}` }, /** 측점별 암 절토 경사비(1:n 의 n) — 카드에서 넣은 사용자 값(2026-09-07). diff --git a/B05_Profile/B05_Profile_UI_Page_Actions.ts b/B05_Profile/B05_Profile_UI_Page_Actions.ts index 591cbe61..88a86153 100644 --- a/B05_Profile/B05_Profile_UI_Page_Actions.ts +++ b/B05_Profile/B05_Profile_UI_Page_Actions.ts @@ -171,16 +171,15 @@ export async function tempSaveAction(ctx: PageActionContext): Promise { ); // B06 조정창에서 만진 배수관 구간값도 세션에만 있다 — 함께 내보낸다 // (CLAUDE.md 5장: 영구저장은 [저장]·[확정]에서만). 실패해도 저장은 진행한다. - // ⚠ 키 **셋을 다 넘긴다**(2026-09-12 사용자: 어느 페이지에서 저장해도 결과가 같아야 - // 한다). 종전에는 구간값 키만 넘겨, B06 에서 예약한 관 이동·추가·삭제가 B05 [임시저장] - // 에서는 조용히 빠졌다. + // ⚠ 이동 키까지 넘긴다(2026-09-12 사용자: 어느 페이지에서 저장해도 결과가 같아야 + // 한다). 종전에는 구간값 키만 넘겨, 옛 세션에 남은 관 이동 예약이 B05 [임시저장]에서는 + // 조용히 빠졌다. 관 목록 자체는 이제 두 페이지가 같은 스냅샷(`pipes`)을 쓴다. if (latest?.route?.id != null) { const routeId = latest.route.id; await flushCulvertOptions( projectId, stateKey("culvertopt", projectId, routeId), stateKey("culvertmove", projectId, routeId), - stateKey("culvertedit", projectId, routeId), ).catch(() => undefined); } // B06에서 만져 **캐시에 얹힌** 횡단 수정분을 함께 남긴다 — 안 보내면 바로 아래 diff --git a/B06_Section/B06_Section_Api_Culvert_Options.ts b/B06_Section/B06_Section_Api_Culvert_Options.ts index eae5d769..011a583d 100644 --- a/B06_Section/B06_Section_Api_Culvert_Options.ts +++ b/B06_Section/B06_Section_Api_Culvert_Options.ts @@ -28,14 +28,6 @@ type PendingMap = Record; export interface CulvertOptionWriter { /** 측점 옵션을 세션에 예약한다. 같은 측점의 앞선 예약과는 합쳐진다. */ queue: (chainageM: number, patch: CulvertOptionPatch) => void; - /** 기준 측점 이동을 예약한다(2026-08-29 사용자: B06에서도 옮길 수 있어야 한다). - * 세부 배수유역 재분할은 저장 시 서버가 관 목록을 다시 받아 처리한다. */ - queueMove: (fromChainageM: number, toChainageM: number) => void; - /** 관(계곡 통과 시설) 추가를 예약한다 — B05·B06 은 한 페이지라 넣는 자리도 같아야 한다 - * (2026-09-12 사용자). 이동과 같은 규칙으로 세션에만 쌓고 [저장]·[확정]에서 나간다. */ - queueAdd: (chainageM: number, facility?: string) => void; - /** 관 삭제를 예약한다. 추가해 둔 것을 다시 지우면 예약만 걷어 낸다. */ - queueRemove: (chainageM: number) => void; /** 예약분을 즉시 내보낸다([저장]·[확정]에서만 부른다). */ flush: () => Promise; } @@ -67,7 +59,9 @@ function writePending(sessionKey: string | null, pending: PendingMap): void { * 하지 않는다. 성공하면 세션을 비우고, 실패하면 **그대로 두어** 다음 [저장]에서 * 다시 시도한다 — 조용히 잃으면 화면과 저장분이 갈린다. */ -/** 예약된 이동 — 옛 측점 키 → 새 누가거리(m). */ +/** 예약된 이동 — 옛 측점 키 → 새 누가거리(m). + * ⚠ **이제 쓰는 곳은 없다**(2026-09-12 일원화: 관 목록은 B05·B06 이 같은 스냅샷을 쓴다). + * 옛 세션에 남아 있던 예약을 저장 때 한 번 흘려보내려고 읽기만 남겨 둔다. */ type PendingMoves = Record; function readMoves(sessionKey: string | null): PendingMoves { @@ -80,59 +74,16 @@ function readMoves(sessionKey: string | null): PendingMoves { } } -/** 예약된 추가·삭제 — 이동과 같은 세션 칸에 함께 담는다(키 하나로 읽고 쓴다). */ -interface PendingEdits { - /** 넣을 관 — 누가거리(m)와 시설 종류(기본 배관). */ - adds: Array<{ chainage_m: number; facility?: string }>; - /** 지울 관 — 측점 키(누가거리 2자리). */ - removes: string[]; -} - -const EMPTY_EDITS: PendingEdits = { adds: [], removes: [] }; - -function readEdits(sessionKey: string | null): PendingEdits { - if (!sessionKey) return { ...EMPTY_EDITS, adds: [], removes: [] }; - try { - const raw = window.sessionStorage.getItem(sessionKey); - const parsed = raw ? (JSON.parse(raw) as Partial) : null; - return { - adds: Array.isArray(parsed?.adds) ? parsed.adds : [], - removes: Array.isArray(parsed?.removes) ? parsed.removes : [], - }; - } catch { - return { adds: [], removes: [] }; - } -} - -function writeEdits(sessionKey: string | null, edits: PendingEdits): void { - if (!sessionKey) return; - try { - if (!edits.adds.length && !edits.removes.length) window.sessionStorage.removeItem(sessionKey); - else window.sessionStorage.setItem(sessionKey, JSON.stringify(edits)); - } catch { - /* 세션 저장 실패는 무시 — 값은 화면 캐시에 남아 있다. */ - } -} - export async function flushCulvertOptions( projectId: string | null, sessionKey: string | null, moveKey: string | null = null, - editKey: string | null = null, ): Promise { const pending = readPending(sessionKey); const moves = readMoves(moveKey); - const edits = readEdits(editKey); - if ( - !projectId || - (Object.keys(pending).length === 0 && - Object.keys(moves).length === 0 && - !edits.adds.length && - !edits.removes.length) - ) - return; + if (!projectId || (Object.keys(pending).length === 0 && Object.keys(moves).length === 0)) return; const current = await fetchDetailPipePoints(projectId); - let touched = edits.adds.length > 0 || edits.removes.length > 0; + let touched = false; const points: DetailPipeInput[] = current.pipe_points.map((point) => { // 좌표는 서버가 다시 계산하므로 싣지 않는다(DetailPipeInput 규약). const { lonlat: _lonlat, ...input } = point; @@ -148,23 +99,9 @@ export async function flushCulvertOptions( if (movedTo !== undefined) next.chainage_m = movedTo; return next; }); - // 지울 것을 걷어 내고 넣을 것을 붙인다 — 서버는 이 목록을 그대로 정본으로 삼는다. - const removed = new Set(edits.removes); - const next = points.filter((point) => !removed.has(keyOf(point.chainage_m))); - for (const add of edits.adds) { - if (next.some((point) => Math.abs(point.chainage_m - add.chainage_m) < 0.005)) continue; - next.push({ - chainage_m: add.chainage_m, - // 사람이 넣은 자리 — 세류 교차·간격 규칙이 만든 것과 구분한다. - source: "user", - ...(add.facility ? { facility: add.facility as DetailPipeInput["facility"] } : {}), - }); - } - next.sort((left, right) => left.chainage_m - right.chainage_m); - if (touched) await saveDetailPipePoints(projectId, next); + if (touched) await saveDetailPipePoints(projectId, points); writePending(sessionKey, {}); writePending(moveKey, {}); - writeEdits(editKey, { adds: [], removes: [] }); } /** @@ -177,8 +114,6 @@ export function createCulvertOptionWriter( onError?: (message: string) => void, /** 이동 예약 세션 키 — 없으면 이동은 메모리에도 남지 않는다. */ moveKey: () => string | null = () => null, - /** 추가·삭제 예약 세션 키 — 없으면 추가·삭제를 받지 않는다. */ - editKey: () => string | null = () => null, ): CulvertOptionWriter { return { queue(chainageM, patch) { @@ -187,43 +122,9 @@ export function createCulvertOptionWriter( pending[keyOf(chainageM)] = { ...(pending[keyOf(chainageM)] ?? {}), ...patch }; writePending(key, pending); }, - queueMove(fromChainageM, toChainageM) { - const key = moveKey(); - if (!key) return; - const moves = readMoves(key); - // 이미 옮긴 관을 또 옮기면 **원래 자리** 기준으로 최종 위치만 남긴다. - const origin = - Object.keys(moves).find((entry) => Math.abs(moves[entry] - fromChainageM) < 0.005) ?? - keyOf(fromChainageM); - moves[origin] = toChainageM; - window.sessionStorage.setItem(key, JSON.stringify(moves)); - }, - queueAdd(chainageM, facility) { - const key = editKey(); - if (!key) return; - const edits = readEdits(key); - const at = Number(chainageM.toFixed(2)); - // 같은 자리를 지웠다가 다시 넣으면 삭제 예약만 걷어 낸다(정본은 그대로). - const removedAt = edits.removes.indexOf(keyOf(at)); - if (removedAt >= 0) edits.removes.splice(removedAt, 1); - else if (!edits.adds.some((entry) => Math.abs(entry.chainage_m - at) < 0.005)) - edits.adds.push({ chainage_m: at, ...(facility ? { facility } : {}) }); - writeEdits(key, edits); - }, - queueRemove(chainageM) { - const key = editKey(); - if (!key) return; - const edits = readEdits(key); - const at = Number(chainageM.toFixed(2)); - // 아직 정본에 없는(이번에 넣은) 관이면 추가 예약만 거둔다. - const addedAt = edits.adds.findIndex((entry) => Math.abs(entry.chainage_m - at) < 0.005); - if (addedAt >= 0) edits.adds.splice(addedAt, 1); - else if (!edits.removes.includes(keyOf(at))) edits.removes.push(keyOf(at)); - writeEdits(key, edits); - }, async flush() { try { - await flushCulvertOptions(projectId(), sessionKey(), moveKey(), editKey()); + await flushCulvertOptions(projectId(), sessionKey(), moveKey()); } catch (error) { onError?.(error instanceof Error ? error.message : "배수관 구간값 저장 실패"); throw error; diff --git a/B06_Section/B06_Section_UI_Page.ts b/B06_Section/B06_Section_UI_Page.ts index b9fe7344..5bc343a5 100644 --- a/B06_Section/B06_Section_UI_Page.ts +++ b/B06_Section/B06_Section_UI_Page.ts @@ -201,10 +201,6 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { queuePipeOptions: (chainageM, patch) => stationControls.queueCulvertOptions(chainageM, patch), applyPipeOptions: (chainageM, patch) => applyPipeOptionsToCache(pipeOptionsContext, chainageM, patch), - movePipe: (fromChainageM, toChainageM) => - stationControls.queueCulvertMove(fromChainageM, toChainageM), - addPipe: (chainageM, facility) => stationControls.queueCulvertAdd(chainageM, facility), - removePipe: (chainageM) => stationControls.queueCulvertRemove(chainageM), }); const dockDivider = document.createElement("hr"); diff --git a/B06_Section/B06_Section_UI_Page_Station_Controls.ts b/B06_Section/B06_Section_UI_Page_Station_Controls.ts index e777ea9e..c1d1460e 100644 --- a/B06_Section/B06_Section_UI_Page_Station_Controls.ts +++ b/B06_Section/B06_Section_UI_Page_Station_Controls.ts @@ -45,7 +45,6 @@ export interface StationControlDeps { | "extraspan" | "culvertopt" | "culvertmove" - | "culvertedit" | "revetlink" | "fordadjust" | "boxadjust", @@ -95,12 +94,6 @@ export interface StationControls { flushCulvertOptions: () => Promise; /** 관 옵션 조각을 예약한다 — 좌측 폼의 [수정]도 이 경로로 정본에 간다(2026-08-29). */ queueCulvertOptions: (chainageM: number, patch: Record) => void; - /** 기준 측점 이동을 예약한다 — B06 구조물 배치 폼의 측점 칸이 쓴다(2026-08-29). */ - queueCulvertMove: (fromChainageM: number, toChainageM: number) => void; - /** 관 추가를 예약한다 — B06 종단 우클릭·목록이 쓴다(2026-09-12 B05·B06 일원화). */ - queueCulvertAdd: (chainageM: number, facility?: string) => void; - /** 관 삭제를 예약한다. */ - queueCulvertRemove: (chainageM: number) => void; load: () => void; /** 전체 반영 — 개별 반폭을 전역값으로 덮는다(없으면 비운다). */ applyGlobalWidth: (requested: number | undefined, chainages: number[]) => void; @@ -437,7 +430,6 @@ export function createStationControls(deps: StationControlDeps): StationControls () => deps.sessionKey("culvertopt"), deps.onSaveError, () => deps.sessionKey("culvertmove"), - () => deps.sessionKey("culvertedit"), ); const linkSession = createLinkFlagSession(() => deps.sessionKey("revetlink")); const linkDetached = linkSession.detached; @@ -623,10 +615,6 @@ export function createStationControls(deps: StationControlDeps): StationControls }, flushCulvertOptions: () => culvertOptions.flush(), queueCulvertOptions: (chainageM, patch) => culvertOptions.queue(chainageM, patch), - queueCulvertMove: (fromChainageM, toChainageM) => - culvertOptions.queueMove(fromChainageM, toChainageM), - queueCulvertAdd: (chainageM, facility) => culvertOptions.queueAdd(chainageM, facility), - queueCulvertRemove: (chainageM) => culvertOptions.queueRemove(chainageM), load: () => { loadStationWidths(); loadRevetShifts(); diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index e7c048dc..4f53a3de 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -24,7 +24,7 @@ import { type StructureInstance, type StructureType, } from "../B05_Profile/B05_Profile_Api_Structures"; -import { readPendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft"; +import { readPendingPipes, writePendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft"; import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { readStructurePick, @@ -78,13 +78,6 @@ export interface B06StructuresPanelDeps { /** 바뀐 값을 그 측점 횡단 캐시(culvert 스펙)에 얹고 카드를 다시 그린다 — * 조정창과 같은 흐름이라 폼 조작도 도면에 바로 보인다(2026-08-29 사용자 보고). */ applyPipeOptions?: (chainageM: number, patch: Record) => void; - /** 기준 측점 이동을 예약한다 — 세부 배수유역 재분할은 [저장]·[확정] 때 서버가 - * 관 목록을 다시 받아 처리한다(2026-08-29 사용자: B06에서도 옮길 수 있어야 한다). */ - movePipe?: (fromChainageM: number, toChainageM: number) => void; - /** 관(계곡 통과 시설)을 넣는다 — 이동과 같은 예약 경로(2026-09-12 B05·B06 일원화). */ - addPipe?: (chainageM: number, facility?: string) => void; - /** 관을 뺀다 — 같은 예약 경로. */ - removePipe?: (chainageM: number) => void; /** 구조물 목록이 바뀌었다 — 벽(C군)은 횡단 제원으로 얹혀 **면적까지 달라지므로** * 화면이 횡단을 다시 받아 그려야 한다(2026-09-06 사용자 확정). */ onStructuresChanged?: () => void; @@ -267,7 +260,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc onReveal: () => deps.reveal?.(), // 관 추가 — 정본은 [저장]·[확정]에서 나간다. 화면 목록·알약은 바로 세운다. onPipeAdd: (chainageM, attributes) => { - if (!deps.addPipe) { + if (!deps.projectId) { showToast(PIPE_ADD_GUIDE, "error"); return; } @@ -276,38 +269,28 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc showToast("그 자리에는 계곡 통과 시설이 이미 있습니다.", "error"); return; } - const facility = attributes.facility ?? "pipe"; - deps.addPipe(at, facility); - pipeFacilities.push({ chainage_m: at, facility, options: attributes.options ?? {} }); + pipeFacilities.push({ + chainage_m: at, + facility: attributes.facility ?? "pipe", + source: "user", + options: attributes.options ?? {}, + }); pipeFacilities.sort((left, right) => left.chainage_m - right.chainage_m); currentChainageM = at; + persistPipes(); section.setPipeFacilities(pipeFacilities); pushMarks(); deps.onStructuresChanged?.(); showToast(PIPE_ADD_NOTICE, "success"); }, - // 값 수정은 B06에서도 받는다 — 조정창 구간값과 같은 저장 경로(캐시 예약 → - // [저장]·[확정])로 보낸다. 기준점 이동만 배수유역 재분할이 걸려 B05 몫이다. + // 값 수정·기준점 이동 모두 B06 에서 받는다 — 이동은 종단 끌기와 **같은 함수**를 탄다 + // (2026-09-12 일원화). 재분할은 [저장]·[확정] 뒤 서버 몫이다. onPipeUpdate: (fromChainageM, toChainageM, attributes) => { // 위치가 "옮겨졌다"고 볼 기준은 관 매칭과 같은 0.51m다 — 폼의 측점 표기는 // 0.1m로 반올림되므로 0.005m 기준으로 보면 값만 고쳐도 이동으로 잡힌다 // (2026-08-29 사용자 보고: 높이만 바꿨는데 이동 안내가 떴다). - const moved = Math.abs(fromChainageM - toChainageM) > PIPE_MATCH_M; - if (moved) { - if (deps.movePipe) { - deps.movePipe(fromChainageM, toChainageM); - const hit = pipeFacilities.find( - (entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M, - ); - if (hit) hit.chainage_m = toChainageM; - currentChainageM = toChainageM; - section.setPipeFacilities(pipeFacilities); - pushMarks(); - showToast(PIPE_MOVE_NOTICE, "success"); - } else { - showToast(PIPE_MOVE_GUIDE, "error"); - } - } + if (Math.abs(fromChainageM - toChainageM) > PIPE_MATCH_M) + movePipeTo(fromChainageM, toChainageM); const patch = attributes.options; if (!patch || !Object.keys(patch).length) return; if (!deps.queuePipeOptions) { @@ -324,6 +307,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc (entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M, ); if (hit) hit.options = { ...(hit.options ?? {}), ...patch }; + persistPipes(); section.setPipeFacilities(pipeFacilities); }, onPipeRemove: (chainageM) => removePipeAt(chainageM), @@ -440,14 +424,33 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc section.setPipeFacilities(pipeFacilities); } + /** + * 관 목록을 **미저장 스냅샷**으로 남긴다 — B05 가 쓰는 바로 그 세션이다(2026-09-12 + * 사용자: 담는 꼴도 한 벌이어야 한다). 종전에는 B06 만 「바뀐 것만」 따로 담아 + * 두 페이지가 서로 다른 자리를 보고 저장 순서에 따라 결과가 갈렸다. + */ + function persistPipes(): void { + if (!deps.projectId) return; + writePendingPipes( + deps.projectId, + pipeFacilities.map((pipe) => ({ + chainage_m: pipe.chainage_m, + source: pipe.source ?? "user", + facility: pipe.facility, + ...(pipe.start_m !== undefined ? { start_m: pipe.start_m } : {}), + ...(pipe.end_m !== undefined ? { end_m: pipe.end_m } : {}), + ...(pipe.options ? { options: pipe.options } : {}), + })), + ); + } + /** 관 옮기기 — 폼의 측점 칸과 종단 끌기가 같은 길을 탄다. 재분할은 [저장]·[확정] 뒤. */ function movePipeTo(fromChainageM: number, toChainageM: number): void { - if (!deps.movePipe) { + if (!deps.projectId) { showToast(PIPE_MOVE_GUIDE, "error"); return; } const to = Number(toChainageM.toFixed(2)); - deps.movePipe(fromChainageM, to); const hit = pipeFacilities.find( (entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M, ); @@ -455,6 +458,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc if (currentChainageM !== null && Math.abs(currentChainageM - fromChainageM) < PIPE_MATCH_M) currentChainageM = to; pipeFacilities.sort((left, right) => left.chainage_m - right.chainage_m); + persistPipes(); section.setPipeFacilities(pipeFacilities); pushMarks(); showToast(PIPE_MOVE_NOTICE, "success"); @@ -462,17 +466,17 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc /** 관 빼기 — 목록·폼·종단 우클릭이 같은 길을 탄다. 정본은 [저장]·[확정]에서 나간다. */ function removePipeAt(chainageM: number): void { - if (!deps.removePipe) { + if (!deps.projectId) { showToast(PIPE_ADD_GUIDE, "error"); return; } const at = Number(chainageM.toFixed(2)); - deps.removePipe(at); pipeFacilities = pipeFacilities.filter( (entry) => Math.abs(entry.chainage_m - at) >= PIPE_MATCH_M, ); if (currentChainageM !== null && Math.abs(currentChainageM - at) < PIPE_MATCH_M) currentChainageM = null; + persistPipes(); section.setPipeFacilities(pipeFacilities); pushMarks(); deps.onStructuresChanged?.(); diff --git a/resources/tester/test_b06_culvert_edit_queue.py b/resources/tester/test_b06_culvert_edit_queue.py index 0bca3917..5a167fda 100644 --- a/resources/tester/test_b06_culvert_edit_queue.py +++ b/resources/tester/test_b06_culvert_edit_queue.py @@ -1,9 +1,14 @@ -"""B06 종단에서 넣고 뺀 **계곡 통과 시설**이 세션에 제대로 쌓이는지(2026-09-12 사용자 지시). +"""관 편집이 **한 자리에만** 쌓이는지(2026-09-12 사용자: 담는 꼴도 한 벌이어야 한다). -B05·B06 은 한 페이지이므로 관 추가·삭제도 양쪽에서 되어야 한다. 다만 정본에 바로 쓰지 -않고 **세션에 예약**했다가 [저장]·[확정]에서 한 번에 내보낸다(CLAUDE.md 5장). 넣었다 바로 -빼면 예약만 걷어 내야 하고, 뺐다 다시 넣어도 마찬가지다 — 그러지 않으면 저장 때 있지도 -않은 관을 지우거나 같은 자리에 둘이 선다. +B05 는 관 목록 전체를 세션 스냅샷(`pipes`)으로 담고, B06 은 바뀐 것만 따로 담고 있었다. +저장 순서에 따라 한쪽이 다른 쪽을 덮어 **넣은 관이 사라지는** 일이 생겼다. 그래서 관 +목록은 두 페이지가 같은 스냅샷을 쓰도록 합쳤다. + +여기서 지키는 것은 둘이다. + ① 저장기(`createCulvertOptionWriter`)에는 **관을 넣고 빼는 길이 없다** — 남아 있으면 + 관 목록을 담는 자리가 다시 둘이 된다. + ② 옛 세션에 남아 있던 **이동 예약은 저장 때 흘려보낸다** — 합치기 전에 만들어 둔 + 예약이 조용히 사라지면 안 된다. 실제 화면 코드를 그대로 컴파일해 Node 로 돌린다(다단 하한 시험과 같은 방식). """ @@ -25,15 +30,28 @@ const { writeFileSync } = require("node:fs"); const Module = require("node:module"); // 화면 코드가 번들러 별칭(`@config/…`)을 쓴다 — Node 에는 없으니 빈 껍데기로 돌려준다. -// 이 시험이 보는 것은 세션 예약 로직뿐이라 값은 안 쓴다. const load = Module._load; +const saved = []; Module._load = function (request, parent, isMain) { if (request.startsWith("@config/") || request.startsWith("@util/") || request.startsWith("@ui/")) return new Proxy({}, { get: () => () => undefined }); - return load.call(this, request, parent, isMain); + const mod = load.call(this, request, parent, isMain); + // 관 정본 읽기·쓰기는 서버로 나가므로 가로챈다. + if (request.includes("B04_PreProcess_Api_Fetch")) { + return { + ...mod, + fetchDetailPipePoints: async () => ({ + pipe_points: [ + { chainage_m: 120, lonlat: [0, 0], source: "stream" }, + { chainage_m: 200, lonlat: [0, 0], source: "stream" }, + ], + }), + saveDetailPipePoints: async (_projectId, points) => saved.push(points), + }; + } + return mod; }; -// 세션 저장소 흉내 — 화면 코드는 window.sessionStorage 만 쓴다. const store = new Map(); global.window = { sessionStorage: { @@ -43,39 +61,36 @@ global.window = { }, }; -const { createCulvertOptionWriter } = require(process.argv[3]); -const EDIT_KEY = "edit"; -const writer = createCulvertOptionWriter( +const api = require(process.argv[3]); +const writer = api.createCulvertOptionWriter( () => "p1", () => "opt", undefined, () => "move", - () => EDIT_KEY, ); -const read = () => JSON.parse(store.get(EDIT_KEY) ?? '{"adds":[],"removes":[]}'); -const steps = {}; -writer.queueAdd(120, "pipe"); -writer.queueAdd(140); -steps.afterTwoAdds = read(); +// ① 관을 넣고 빼는 길이 저장기에 남아 있으면 안 된다. +const methods = Object.keys(writer).sort(); -// 방금 넣은 자리를 다시 빼면 **추가 예약만** 사라진다(정본에는 아직 없다). -writer.queueRemove(140); -steps.afterUndoAdd = read(); +// ② 합치기 전 세션에 남아 있던 이동 예약은 저장 때 흘려보낸다. +store.set("move", JSON.stringify({ "120.00": 135 })); -// 정본에 있던 관을 빼면 삭제 예약으로 남는다. -writer.queueRemove(200); -steps.afterRemove = read(); - -// 뺀 자리를 다시 넣으면 삭제 예약만 걷어 낸다(새로 만들지 않는다). -writer.queueAdd(200); -steps.afterUndoRemove = read(); - -// 같은 자리를 두 번 넣어도 하나만 남는다. -writer.queueAdd(120); -steps.afterDuplicateAdd = read(); - -writeFileSync(process.argv[2], JSON.stringify(steps)); +api + .flushCulvertOptions("p1", "opt", "move") + .then(() => { + writeFileSync( + process.argv[2], + JSON.stringify({ + methods, + savedChainages: (saved[0] ?? []).map((point) => point.chainage_m), + moveLeft: store.get("move") ?? null, + }), + ); + }) + .catch((error) => { + writeFileSync(process.argv[2], JSON.stringify({ error: String(error) })); + process.exitCode = 1; + }); """ @@ -112,24 +127,13 @@ def _run(tmp_path: Path) -> dict: return json.loads(result.read_text(encoding="utf-8")) -def test_추가와_삭제_예약이_서로를_걷어_낸다(tmp_path: Path) -> None: - steps = _run(tmp_path) +def test_관_목록을_담는_자리는_하나뿐이다(tmp_path: Path) -> None: + result = _run(tmp_path) + assert "error" not in result, result.get("error") - # 넣은 둘이 그대로 쌓인다. - assert [entry["chainage_m"] for entry in steps["afterTwoAdds"]["adds"]] == [120.0, 140.0] - assert steps["afterTwoAdds"]["adds"][0]["facility"] == "pipe" - assert steps["afterTwoAdds"]["removes"] == [] + # ① 저장기에는 구간값 예약과 내보내기만 있다 — 관을 넣고 빼는 길은 스냅샷 한 곳뿐이다. + assert result["methods"] == ["flush", "queue"] - # 방금 넣은 자리를 빼면 추가 예약만 사라지고 삭제 예약은 안 생긴다. - assert [entry["chainage_m"] for entry in steps["afterUndoAdd"]["adds"]] == [120.0] - assert steps["afterUndoAdd"]["removes"] == [] - - # 정본에 있던 관은 삭제 예약으로 남는다. - assert steps["afterRemove"]["removes"] == ["200.00"] - - # 뺀 자리를 다시 넣으면 삭제 예약만 걷힌다 — 새 관을 만들지 않는다. - assert steps["afterUndoRemove"]["removes"] == [] - assert [entry["chainage_m"] for entry in steps["afterUndoRemove"]["adds"]] == [120.0] - - # 같은 자리를 두 번 넣어도 하나만 남는다. - assert [entry["chainage_m"] for entry in steps["afterDuplicateAdd"]["adds"]] == [120.0] + # ② 옛 세션의 이동 예약은 저장 때 반영되고 비워진다. + assert result["savedChainages"] == [135, 200] + assert result["moveLeft"] is None