fix(B05): 초기화가 저장된 관 지점을 남기던 문제
[초기화]가 화면 목록만 자동 배치로 되돌리고 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) <noreply@anthropic.com>
This commit is contained in:
@@ -506,6 +506,15 @@ export async function computeDetailBasins(
|
||||
});
|
||||
}
|
||||
|
||||
/** 저장된 관 지점을 버리고 기본 관 + 자동 보충 배치로 되돌린다("초기화").
|
||||
*
|
||||
* 화면만 되돌리면 다시 들어왔을 때 옛 관이 살아나므로 저장분까지 지운다. */
|
||||
export async function resetDetailPipePoints(projectId: string): Promise<DetailBasinResponse> {
|
||||
return requestJson<DetailBasinResponse>(`/projects/${projectId}/drainage/pipe-points`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
/** 관 매설 지점을 정본으로 확정하고 세부유역 산출물까지 남긴다(모델 확정 시점). */
|
||||
export async function saveDetailPipePoints(
|
||||
projectId: string,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
/** 세부유역 요청 한 번을 감싼다 — 버튼 잠금·진행 문구·오류 표기를 한 자리에 모은다. */
|
||||
async function run(request: () => Promise<DetailBasinResponse>): Promise<void> {
|
||||
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<void> => run(() => fetchDetailPipePoints(projectId as string));
|
||||
|
||||
/** 저장분까지 버리고 자동 배치로 되돌린다("초기화"). 화면만 되돌리면 다시 들어왔을 때
|
||||
* 옛 관이 살아난다(2026-08-02 사용자 보고). */
|
||||
const resetPipes = (): Promise<void> => run(() => resetDetailPipePoints(projectId as string));
|
||||
|
||||
/** 세부유역을 다시 나눈다. 격자 해석은 하지 않으므로 즉시 끝난다. */
|
||||
const analyze = (): Promise<void> =>
|
||||
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());
|
||||
|
||||
/** 노선 전체가 보이도록 배율·중심을 맞춘다(초기 보기 = 도로 기준 줌인).
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user