From a58fac58d0c519b71e7ed8c4390e596611a380f1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 2 Aug 2026 00:38:24 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05):=20=EC=B4=88=EA=B8=B0=ED=99=94?= =?UTF-8?q?=EA=B0=80=20=EC=A0=80=EC=9E=A5=EB=90=9C=20=EA=B4=80=20=EC=A7=80?= =?UTF-8?q?=EC=A0=90=EC=9D=84=20=EB=82=A8=EA=B8=B0=EB=8D=98=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [초기화]가 화면 목록만 자동 배치로 되돌리고 edits/pipe_points.json은 그대로 두어, 페이지를 다시 열면 옛 관이 살아났다. B04와 같은 파일을 보므로 B04에도 그대로 남았다. - DELETE /{project_id}/drainage/pipe-points 신설: 저장분과 파생물 (04_detailed_basins.geojson)까지 지우고 자동 배치 결과를 돌려준다. - clear_pipe_points() 헬퍼 추가. - B05 [초기화] 버튼이 이 경로를 쓴다. - loadSaved / analyze / resetPipes가 같은 뼈대를 반복해 run() 하나로 묶었다. 검증: 초기화 전 {user:1, stream:1, spacing:2} → 초기화 후 {stream:1, spacing:3}, 두 파일 삭제 확인, 재조회도 같은 자동 배치. Co-Authored-By: Claude Opus 5 (1M context) --- B04_wf1_Surface/B04_wf1_Surface_Api_Fetch.ts | 9 +++ .../B04_wf1_Surface_Router_Basins.py | 20 +++++++ .../B05_wf2_Route_UI_Drainage_Panel.ts | 59 ++++++++----------- common_util/common_util_drainage_pipes.py | 21 +++++++ 4 files changed, 74 insertions(+), 35 deletions(-) diff --git a/B04_wf1_Surface/B04_wf1_Surface_Api_Fetch.ts b/B04_wf1_Surface/B04_wf1_Surface_Api_Fetch.ts index f00179aa1..5ddf75a61 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_Api_Fetch.ts +++ b/B04_wf1_Surface/B04_wf1_Surface_Api_Fetch.ts @@ -506,6 +506,15 @@ export async function computeDetailBasins( }); } +/** 저장된 관 지점을 버리고 기본 관 + 자동 보충 배치로 되돌린다("초기화"). + * + * 화면만 되돌리면 다시 들어왔을 때 옛 관이 살아나므로 저장분까지 지운다. */ +export async function resetDetailPipePoints(projectId: string): Promise { + return requestJson(`/projects/${projectId}/drainage/pipe-points`, { + method: "DELETE", + }); +} + /** 관 매설 지점을 정본으로 확정하고 세부유역 산출물까지 남긴다(모델 확정 시점). */ export async function saveDetailPipePoints( projectId: string, diff --git a/B04_wf1_Surface/B04_wf1_Surface_Router_Basins.py b/B04_wf1_Surface/B04_wf1_Surface_Router_Basins.py index c2cb9464c..97e264380 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_Router_Basins.py +++ b/B04_wf1_Surface/B04_wf1_Surface_Router_Basins.py @@ -28,6 +28,7 @@ from common_util.common_util_drainage_detail import DrainageDetail, build_detail from common_util.common_util_drainage_pipes import ( PIPE_SOURCE_USER, PipePoint, + clear_pipe_points, load_pipe_points, parse_pipe_points, route_signature, @@ -218,6 +219,25 @@ async def post_detail_basins( return _payload(project_id, context, detail, points, saved=False) +@router.delete("/{project_id}/drainage/pipe-points", response_model=None) +async def delete_pipe_points(project_id: UUID) -> dict[str, Any] | JSONResponse: + """저장된 관 지점을 버리고 기본 관 + 자동 보충 배치로 되돌린다("초기화"). + + 화면만 되돌리면 다시 들어왔을 때 옛 관이 살아난다 — 저장분과 파생 산출물까지 지운다 + (2026-08-02 사용자 보고). + """ + context, reason = await load_drainage_context(project_id) + if context is None: + return JSONResponse(status_code=404, content={"status": "error", "message": reason}) + await asyncio.to_thread(clear_pipe_points, context.stored_path) + + resolved = await _resolve(project_id, None, use_stored=False) + if isinstance(resolved, JSONResponse): + return resolved + context, detail, points, _ = resolved + return _payload(project_id, context, detail, points, saved=False) + + @router.put("/{project_id}/drainage/pipe-points", response_model=None) async def put_pipe_points( project_id: UUID, payload: dict[str, Any] | None = None diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Panel.ts b/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Panel.ts index 4802f4572..701cc8f00 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Panel.ts @@ -5,6 +5,7 @@ import { computeDetailBasins, fetchDetailPipePoints, getVWorldMapUrl, + resetDetailPipePoints, saveDetailPipePoints, type DetailBasin, type DetailBasinResponse, @@ -389,43 +390,15 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra callbacks.onPipesChanged?.(pipeEditor.chainages()); } - /** 저장된 관 지점(없으면 자동 배치)을 불러온다. 화면에 들어올 때 1회. */ - async function loadSaved(): Promise { - if (!projectId) return; - status.hidden = false; - status.textContent = L("B05_Drainage_Status_Analyzing"); - showProgress(null, L("B05_Drainage_Status_Analyzing")); - try { - apply(await fetchDetailPipePoints(projectId)); - status.hidden = basins.length > 0; - if (basins.length === 0) status.textContent = L("B05_Drainage_Status_NoBasin"); - scheduleDraw(); - } catch (error) { - status.hidden = false; - status.textContent = - error instanceof Error ? error.message : L("B05_Drainage_Status_AnalyzeFailed"); - } finally { - showProgress(null, null); - } - } - - /** 세부유역을 다시 나눈다. `auto=true`면 편집분을 버리고 자동 배치로 되돌린다. - * 격자 해석은 하지 않으므로 즉시 끝난다. */ - async function analyze(auto = false): Promise { + /** 세부유역 요청 한 번을 감싼다 — 버튼 잠금·진행 문구·오류 표기를 한 자리에 모은다. */ + async function run(request: () => Promise): Promise { if (!projectId) return; analyzeButton.disabled = true; status.hidden = false; status.textContent = L("B05_Drainage_Status_Analyzing"); showProgress(null, L("B05_Drainage_Status_Analyzing")); try { - const points = - !auto && pipeEditor.pipes().length > 0 - ? pipeEditor.pipes().map((pipe) => ({ - chainage_m: pipe.chainage_m, - source: (pipe.reason || "user") as PipeSource, - })) - : []; - apply(await computeDetailBasins(projectId, points)); + apply(await request()); status.hidden = basins.length > 0; if (basins.length === 0) status.textContent = L("B05_Drainage_Status_NoBasin"); scheduleDraw(); @@ -439,12 +412,28 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra } } + /** 저장된 관 지점(없으면 자동 배치)을 불러온다. 화면에 들어올 때 1회. */ + const loadSaved = (): Promise => run(() => fetchDetailPipePoints(projectId as string)); + + /** 저장분까지 버리고 자동 배치로 되돌린다("초기화"). 화면만 되돌리면 다시 들어왔을 때 + * 옛 관이 살아난다(2026-08-02 사용자 보고). */ + const resetPipes = (): Promise => run(() => resetDetailPipePoints(projectId as string)); + + /** 세부유역을 다시 나눈다. 격자 해석은 하지 않으므로 즉시 끝난다. */ + const analyze = (): Promise => + run(() => + computeDetailBasins( + projectId as string, + pipeEditor.pipes().map((pipe) => ({ + chainage_m: pipe.chainage_m, + source: (pipe.reason || "user") as PipeSource, + })), + ), + ); + analyzeButton.addEventListener("click", () => void analyze()); deleteButton.addEventListener("click", () => void pipeEditor.deleteSelected()); - autoButton.addEventListener("click", () => { - pipeEditor.setPipes([]); - void analyze(true); - }); + autoButton.addEventListener("click", () => void resetPipes()); /** 노선 전체가 보이도록 배율·중심을 맞춘다(초기 보기 = 도로 기준 줌인). * diff --git a/common_util/common_util_drainage_pipes.py b/common_util/common_util_drainage_pipes.py index eb6eca84e..18f579baa 100644 --- a/common_util/common_util_drainage_pipes.py +++ b/common_util/common_util_drainage_pipes.py @@ -130,6 +130,27 @@ def save_pipe_points(stored_path: str, signature: str, points: list[PipePoint]) return len(points) +def clear_pipe_points(stored_path: str) -> bool: + """저장된 관 지점과 그 파생물을 지운다. 하나라도 지웠으면 True. + + "초기화"는 화면만 되돌리는 것이 아니라 **저장분까지** 되돌린다 — 화면만 되돌리면 다시 + 들어왔을 때 옛 관이 살아나 사용자가 초기화한 적 없는 상태를 보게 된다 + (2026-08-02 사용자 보고). + """ + removed = False + for path in (pipe_points_path(stored_path), detail_basins_path(stored_path)): + try: + path.unlink() + removed = True + except FileNotFoundError: + continue + except OSError: + logger.warning("배수유역: 저장분을 지우지 못했습니다 (%s).", path) + if removed: + logger.info("배수유역: 관 지점 저장분을 초기화했습니다 (%s).", stored_path) + return removed + + def save_detail_basins(stored_path: str, features: list[dict[str, Any]]) -> Path: """세부유역을 GeoJSON으로 남긴다(파생물 — 관 지점만 있으면 언제든 다시 만든다).""" path = detail_basins_path(stored_path)