feat(M02): 구조물 조합 사본 구멍 메움 — 관 옮김 이름만 · 실패 알림 · 알약 · 그룹 · 가져온이(142-2)

- sync_snapshots: 관 지점을 옮겨도 같은 항목 · 같은 그룹 꼬리면 폴더 이름만 바꿈(가까운 측점 먼저)
- safe_sync 못만듦을 구조물 · 관 지점 저장 응답(snapshot_failed)에 실음 · B05/B06 [저장] 알림 한 줄
- 사전조합 참조사본에 안 로직 알약(배 · 단위) 굳힘 · 사본 셈이 지금 M01 을 안 읽음
- 옵션 그룹 이름을 사본 manifest 에 굳힘 · 집계 채움이 사본 이름으로(정의가 바뀌거나 사라져도)
- manifest 가져온이(세션) · 사본 목록 부분 · 가져온이 · [개인에 저장]이 일위대가 문서도 가져감
- 시험: 관 옮김 → 같은 사본 · 못만듦 · 가져온이 · 그룹 사본 값 · 알약 굳힘 · 개인 저장 일위대가 · 저장 응답

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sDZSjFvoiDjdGpgj3muc3
This commit is contained in:
2026-10-09 15:05:52 +09:00
co-authored by Claude Opus 5.5
parent f8b38d828b
commit b7beeb292a
15 changed files with 326 additions and 32 deletions
+23 -4
View File
@@ -17,6 +17,7 @@ import {
API_SURFACE_BUILD_TIMEOUT_MS,
API_TIMEOUT_MS,
} from "@config/config_frontend";
import { showToast } from "@ui/ui_template_elements";
import type { GroupItems, SummaryItem } from "../common_util/common_util_summary_item";
/** 지표면 분석 실행 요청 (SurfaceAnalyzeRequest) */
@@ -557,6 +558,22 @@ export interface DetailBasinResponse {
arrow_spacing_m: number;
/** 유역 안쪽 상류 세류망 — 하이라이트 토글용. */
upstream_lonlat: Array<Array<[number, number]>>;
/** [저장] 때 못 만든 구조물 조합 사본 폴더(142-2 · `*` = 통째 실패). */
snapshot_failed?: string[];
}
let lastNote = { text: "", at: 0 };
/** 못 만든 구조물 조합 사본(저장 응답 `snapshot_failed` · 142-2) — [저장] 알림 한 줄 ·
* 관 · 구조물 저장이 같은 맞춤을 두 번 알리면 한 번만. */
export function noteSnapshotFailed(failed: readonly string[] | undefined): void {
if (!failed?.length) return;
const text = failed.includes("*")
? "구조물 조합 사본 맞춤에 실패했습니다 — 다시 [저장]해 주세요."
: `구조물 조합 사본 ${failed.length}곳을 못 만들었습니다(원본 조합 없음) — 조합을 다시 골라 [저장]해 주세요.`;
const now = Date.now();
if (text === lastNote.text && now - lastNote.at < 5000) return;
lastNote = { text, at: now };
showToast(text, "error");
}
/** 저장된 관 매설 지점과 그 세부유역. 저장분이 없으면 백엔드가 자동 생성해 돌려준다. */
@@ -593,8 +610,10 @@ export async function saveDetailPipePoints(
projectId: string,
points: DetailPipeInput[],
): Promise<DetailBasinResponse> {
return requestJson<DetailBasinResponse>(`/projects/${projectId}/drainage/pipe-points`, {
method: "PUT",
body: JSON.stringify({ points }),
});
const saved = await requestJson<DetailBasinResponse>(
`/projects/${projectId}/drainage/pipe-points`,
{ method: "PUT", body: JSON.stringify({ points }) },
);
noteSnapshotFailed(saved.snapshot_failed);
return saved;
}
+10 -4
View File
@@ -20,11 +20,12 @@ import math
from typing import Any
from uuid import UUID
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Export import drainage_dir
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
from common_util.common_util_auth import get_optional_session
from common_util.common_util_drainage_context import DrainageContext, load_drainage_context
from common_util.common_util_drainage_detail import DrainageDetail, build_detail
from common_util.common_util_drainage_pipes import (
@@ -403,7 +404,9 @@ async def delete_pipe_points(project_id: UUID) -> dict[str, Any] | JSONResponse:
@router.put("/{project_id}/drainage/pipe-points", response_model=None)
async def put_pipe_points(
project_id: UUID, payload: dict[str, Any] | None = None
project_id: UUID,
payload: dict[str, Any] | None = None,
session: dict[str, Any] | None = Depends(get_optional_session), # 가져온이(142-2)
) -> dict[str, Any] | JSONResponse:
"""관 지점을 정본으로 확정하고 세부유역 산출물까지 함께 남긴다(모델 확정 시점)."""
resolved = await _resolve(project_id, payload, use_stored=True)
@@ -424,9 +427,12 @@ async def put_pipe_points(
)
# 지정한 집계 항목을 프로젝트에 복사 · 안 쓰는 복사본 지움(PLAN 21-3 · 실패해도 저장 유지)
from common_util.common_util_storage import resolve_stored_project_path
from M02_MasterTemplete.M02_Item_Snapshot import safe_sync
from M02_MasterTemplete.M02_Item_Snapshot import importer, safe_sync
await asyncio.to_thread(safe_sync, resolve_stored_project_path(context.stored_path))
failed = await asyncio.to_thread(
safe_sync, resolve_stored_project_path(context.stored_path), importer(session)
)
result = _payload(project_id, context, detail, points, saved=True)
result["saved_count"] = saved
result["snapshot_failed"] = failed # 못 만든 구조물 조합 복사본(142-2) — [저장] 알림
return result