From 165b281fe1b87cfbed6f01185628490d1cc79247 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 6 Sep 2026 20:25:12 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05,B06):=20=EB=B0=B0=EC=88=98=EA=B4=80=20?= =?UTF-8?q?=ED=8E=B8=EC=A7=91=EC=9D=84=20=EC=84=B8=EC=85=98=20=EC=B4=88?= =?UTF-8?q?=EC=95=88=EC=97=90=20=EC=8C=93=EC=95=84=20B06=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EC=97=90=EB=8F=84=20=EC=8B=A4=EB=A6=AC=EA=B2=8C=20?= =?UTF-8?q?=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 관 추가·이동·삭제가 B05 패널 메모리에만 있어, B06 으로 넘어가 [저장]·[확정]하면 그 편집이 사라졌음(대응표 조사에서 드러남). - 등록표에 `pipes` 초안(프로젝트 범위)을 되살림 - 배치가 바뀌는 순간 목록을 초안에 담고, 내보내기는 `flushPendingPipes` 한 창구로 모음 - B05 [임시저장]·B06 [저장]·[확정] 모두 이 창구를 지남 (자동저장 없음) 검증: 초안으로 관 11 → 10개 저장·초안 비움, 되돌려 11개 복구. 테스트 390 통과. Co-Authored-By: Claude Opus 5 (1M context) --- A00_Common/b_page_state.ts | 8 +++-- B05_Profile/B05_Profile_Api_Pipes_Draft.ts | 33 ++++++++++++++++++++ B05_Profile/B05_Profile_UI_Drainage_Panel.ts | 22 ++++++++++++- B05_Profile/B05_Profile_UI_Page_Actions.ts | 3 ++ B06_Section/B06_Section_UI_Page_Persist.ts | 7 +++++ 5 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 B05_Profile/B05_Profile_Api_Pipes_Draft.ts 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");