diff --git a/A00_Common/b_page_state.ts b/A00_Common/b_page_state.ts index 85b9bb59..57a6c426 100644 --- a/A00_Common/b_page_state.ts +++ b/A00_Common/b_page_state.ts @@ -92,9 +92,11 @@ export const STATE_REGISTRY = { structures: { bucket: "draft", scope: "project", legacy: (p) => `b05:structures:${p}` }, /** 3D 램프로 바꾼 측점 상단측(측구 방향). */ uphill: { bucket: "draft", scope: "project", legacy: (p) => `b05:uphill:${p}` }, - /* `pipes`(옛 `b05:pipes`)는 2026-09-06 에 뺐다 — 읽는 곳도 쓰는 곳도 없었다. - 관 위치의 정본은 `pipe_points.json` 이고, 화면은 [저장] 때 `savePipes()` 로 바로 - 내보낸다. 초안처럼 보이는 이름만 남아 대응표에서 「저장 자리 없음」으로 잡혔다. */ + /** B05 배수유역도에서 고친 관 목록(추가·이동·삭제) — 2026-09-06 되살림. + * 예전에는 패널 메모리에만 있어 B06 으로 넘어가면 편집이 사라졌다(대응표 조사). + * 정본은 `pipe_points.json` 이고, 이 초안은 [저장]·[확정]에서 `flushPendingPipes` 가 + * 내보낸 뒤 비운다. */ + pipes: { bucket: "draft", scope: "project", legacy: (p) => `b05:pipes:${p}` }, /** B05 에서 고른 구조물을 B06 이 이어받는 자리 — 예전 `aislo:structure-pick:*`. */ "structure-pick": { bucket: "draft", diff --git a/B05_Profile/B05_Profile_Api_Pipes_Draft.ts b/B05_Profile/B05_Profile_Api_Pipes_Draft.ts new file mode 100644 index 00000000..03c7fe96 --- /dev/null +++ b/B05_Profile/B05_Profile_Api_Pipes_Draft.ts @@ -0,0 +1,33 @@ +/* ============================================================================= + * B05_Profile_Api_Pipes_Draft.ts + * B05 배수유역도에서 고친 **관 목록 초안**(세션) — [저장]·[확정]에서만 정본으로 나간다. + * + * 왜 생겼나(2026-09-06 대응표 조사) — 관 추가·이동·삭제가 패널 메모리에만 있어, B06 으로 + * 넘어가 [저장]하면 그 편집이 통째로 사라졌다(`savePipes()` 를 부르는 곳이 B05 [임시저장] + * 하나뿐이었다). 다른 조작값과 같은 규칙으로 세션에 쌓고 저장 앞단에서 함께 내보낸다. + * + * 자동저장은 하지 않는다(CLAUDE.md 5장). + * ========================================================================== */ + +import { readState, writeState } from "../A00_Common/b_page_state"; +import { saveDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; +import type { DetailPipeInput } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; + +/** 세션에 담는 관 한 벌 — 정본 PUT 이 그대로 받는 꼴이다. */ +export type PendingPipes = DetailPipeInput[]; + +export function readPendingPipes(projectId: string): PendingPipes | null { + return readState("pipes", projectId); +} + +export function writePendingPipes(projectId: string, next: PendingPipes | null): void { + writeState("pipes", next, projectId); +} + +/** 초안이 있으면 정본에 쓰고 비운다. 없으면 아무 일도 하지 않는다. */ +export async function flushPendingPipes(projectId: string): Promise { + const pending = readPendingPipes(projectId); + if (!pending) return; + await saveDetailPipePoints(projectId, pending); + writePendingPipes(projectId, null); +} diff --git a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts index 860b4abe..a4c7eb77 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts @@ -28,6 +28,7 @@ import { buildStrengthArray } from "../B04_PreProcess/B04_PreProcess_UI_FlowRamp import { resampleRoute } from "../B04_PreProcess/B04_PreProcess_UI_RouteSamples"; import { createPipeEditor } from "./B05_Profile_UI_Drainage_Pipes"; import { createFacilityStore } from "./B05_Profile_UI_Drainage_Facility"; +import { writePendingPipes } from "./B05_Profile_Api_Pipes_Draft"; import { createDrainageChrome } from "./B05_Profile_UI_Drainage_Chrome"; import { bindDrainageInteractions } from "./B05_Profile_UI_Drainage_Interact"; import { drawDrainageScene } from "./B05_Profile_UI_Drainage_Render"; @@ -129,7 +130,12 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra scheduleDraw(); }, // 배치가 실제로 바뀐 순간(추가·삭제·이동 완료)에만 세부유역을 다시 나눈다. - () => void analyze(), + // 같은 순간에 **세션 초안**에도 담는다 — 예전에는 패널 메모리에만 있어 B06 으로 + // 넘어가 저장하면 편집이 사라졌다(2026-09-06 대응표 조사). + () => { + rememberPipes(); + void analyze(); + }, ); // 2차 전체 배수유역 외곽선 = 분수령. 별도 "능선" 레이어를 두지 않고 이 선 하나로 표시한다 // (전체 유역 외곽선과 능선이 같은 선이므로 — 2026-07-31 사용자 지시). @@ -389,6 +395,20 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra } } + /** 지금 화면의 관 목록을 세션 초안에 담는다 — 정본 쓰기는 [저장]·[확정] 몫이다. */ + function rememberPipes(): void { + if (!projectId) return; + writePendingPipes( + projectId, + facilityStore.attach( + pipeEditor.pipes().map((pipe) => ({ + chainage_m: pipe.chainage_m, + source: (pipe.reason || "user") as PipeSource, + })), + ), + ); + } + /** 저장된 관 지점(없으면 자동 배치)을 불러온다. 화면에 들어올 때 1회. */ const loadSaved = (): Promise => run(() => fetchDetailPipePoints(projectId as string)); diff --git a/B05_Profile/B05_Profile_UI_Page_Actions.ts b/B05_Profile/B05_Profile_UI_Page_Actions.ts index f5aac8a3..b709f688 100644 --- a/B05_Profile/B05_Profile_UI_Page_Actions.ts +++ b/B05_Profile/B05_Profile_UI_Page_Actions.ts @@ -24,6 +24,7 @@ import { type RouteLatestResponse, } from "./B05_Profile_Api_Fetch"; import { flushCulvertOptions } from "../B06_Section/B06_Section_Api_Culvert_Options"; +import { flushPendingPipes } from "./B05_Profile_Api_Pipes_Draft"; import { invalidateSectionDetail, saveCachedCrossPatches, @@ -131,6 +132,8 @@ export async function tempSaveAction(ctx: PageActionContext): Promise { const latest = ctx.latest(); const projectId = ctx.projectId; // 관 매설 지점을 B04와 같은 저장소에 남긴다 — 저장 실패가 임시저장을 막지는 않는다. + // 세션 초안(B06 저장도 같은 창구를 쓴다)을 먼저 내보내고, 화면 목록으로 한 번 더 맞춘다. + await flushPendingPipes(projectId).catch(() => undefined); await ctx .profilePanel() .drainage.savePipes() diff --git a/B06_Section/B06_Section_UI_Page_Persist.ts b/B06_Section/B06_Section_UI_Page_Persist.ts index dd02a77f..5da34f58 100644 --- a/B06_Section/B06_Section_UI_Page_Persist.ts +++ b/B06_Section/B06_Section_UI_Page_Persist.ts @@ -17,6 +17,7 @@ import { type SectionDetailResponse, } from "./B06_Section_Api_Fetch"; import { invalidateSectionDetail } from "./B06_Section_Section_Store"; +import { flushPendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft"; import { flushPendingStructures } from "../B05_Profile/B05_Profile_Api_Structures"; import { flushUphillOverrides } from "../B05_Profile/B05_Profile_Api_Fetch"; import { buildCrossPatches, type CrossPatchSources } from "./B06_Section_UI_Page_Patches"; @@ -250,6 +251,12 @@ async function flushPendingEdits(ctx: SectionPersistContext, projectId: string): // B05에서 만지고 넘어온 구조물 조작분도 여기서 정본에 남긴다. 실패해도 횡단 // 저장까지 막지는 않는다 — 미저장분은 세션에 남으므로 다시 시도할 수 있다 // (2026-08-29 실측: 타입이 거절되자 sections/save가 아예 나가지 않았다). + // B05 배수유역도에서 고친 관 목록(추가·이동·삭제)도 여기서 정본에 남긴다 — 예전에는 + // B05 [임시저장]에만 실려, B06 에서 저장하면 그 편집이 사라졌다(2026-09-06 대응표). + await flushPendingPipes(projectId).catch((error) => { + const detail = error instanceof Error ? ` ${error.message}` : ""; + showToast(`배수관 저장에 실패했습니다.${detail}`, "error"); + }); await flushPendingStructures(projectId).catch((error) => { const detail = error instanceof Error ? ` ${error.message}` : ""; showToast(`구조물 저장에 실패했습니다.${detail}`, "error");