diff --git a/B04_PreProcess/B04_PreProcess_Router_Watershed_Live.py b/B04_PreProcess/B04_PreProcess_Router_Watershed_Live.py index 471da8f43..1ec330489 100644 --- a/B04_PreProcess/B04_PreProcess_Router_Watershed_Live.py +++ b/B04_PreProcess/B04_PreProcess_Router_Watershed_Live.py @@ -22,7 +22,7 @@ from fastapi.responses import JSONResponse from B04_PreProcess.B04_PreProcess_Engine_Watershed_Analyze import preview_stages from B04_PreProcess.B04_PreProcess_Engine_Watershed_Expand import AnalysisStopped from B04_PreProcess.B04_PreProcess_Engine_Watershed_Export import drainage_dir -from B04_PreProcess.B04_PreProcess_Router_Basins import _build, _payload +from B04_PreProcess.B04_PreProcess_Router_Basins import AUTO_MODES, _build, _payload from B04_PreProcess.B04_PreProcess_Router_Watershed import _prepare, router from common_util.common_util_drainage_context import _open_sampler, load_drainage_context from common_util.common_util_drainage_detail import RoadRouting @@ -78,7 +78,8 @@ def live_routing(preview: Any, route_xy: list[tuple[float, float]], to_lonlat: A async def post_drainage_live( project_id: UUID, payload: dict[str, Any] | None = None ) -> dict[str, Any] | JSONResponse: - """몸통 = {generation, route: RouteDraft | null(저장 노선), points: 관 목록 | null(자동)}.""" + """몸통 = {generation, route: RouteDraft | null(저장 노선), + points: 관 목록 | null(세류선 교차만), auto: [자동 제안] 방식(all · add · 369) | 없음}.""" body = payload or {} generation = int(body.get("generation") or 0) key = str(project_id) @@ -87,6 +88,9 @@ async def post_drainage_live( def superseded() -> bool: return _LATEST_GENERATION.get(key) != generation + auto = body.get("auto") + if auto not in AUTO_MODES: + return JSONResponse(status_code=400, content={"status": "error", "message": "auto 틀림"}) try: raw = body.get("points") points = None if raw is None else parse_pipe_points(raw) @@ -139,7 +143,7 @@ async def post_drainage_live( [list(to_lonlat(x, y)) for x, y in line.coords] for line in preview.region.split.upstream ] detail, carried = await asyncio.to_thread( - _build, context.stored_path, live, points, routing, upstream + lambda: _build(context.stored_path, live, points, routing, upstream, auto=auto) ) if detail is None: return JSONResponse( diff --git a/B05_Profile/B05_Profile_UI_Drainage_Live.ts b/B05_Profile/B05_Profile_UI_Drainage_Live.ts index 17c60e01e..d7a64ff92 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Live.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Live.ts @@ -16,7 +16,8 @@ * - 덮개 · 잠금 없음 — 다른 칸 조작은 그대로. * - [자동 제안](369) — 고름 창 ① 전체 자동(all) ② 지정 포함 추가(add) · [배관 배치 초기화] = 세류선 교차 * 관만 · 셋 다 초안(세션) · [저장] 때 정본 · 답이 오면 사진첩 한 걸음(늦은 답도 앞 걸음에 안 묻힘) · - * 낡은 동안 [자동 제안] 끔. + * 낡은 동안 [자동 제안] 끔 · 노선 초안 중이거나 저장 분석이 낡았으면(옛 노선) 「지금 노선으로 셈」 길에 + * 방식을 실어 보냄(저장 분석으로 셈 = 옛 노선 자리 · 409). * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; @@ -76,7 +77,7 @@ export interface DrainageLive { /** 「지금 노선으로 셈」 — 서버 `post_drainage_live` · 작업본에 안 씀. */ async function fetchLive( projectId: string, - body: ReturnType, + body: ReturnType | (Proposal & { generation: number }), signal: AbortSignal, ): Promise { const response = await fetch(`${API_BASE_URL}/projects/${projectId}/drainage/live`, { @@ -97,6 +98,9 @@ export function readDrainageMode(): DrainageMode { const L = (key: keyof typeof ui_locales): string => ui_locales[key][currentLanguageIndex]; +/** [자동 제안] · [배관 배치 초기화] 요청 — 관 목록 없음(null) = 세류선 교차만. */ +type Proposal = { points: DetailPipeInput[] | null; auto?: "all" | "add" }; + export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive { const viewport = deps.root.querySelector(".b05-drainage__viewport") as HTMLElement; const header = deps.root.querySelector(".b05-drainage__header") as HTMLElement; @@ -124,6 +128,8 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive { let drafted = false; /** 저장분 다시 받기 답이 「낡음」이었나(`markStale` 이 세움). */ let savedStale = false; + /** 저장 분석이 옛 노선(서버 「낡음」 답 · [확정] 전까지) — [자동 제안]은 지금 노선으로 셈. */ + let analysisStale = false; let generation = 0; /** 마지막 단계 입력 지문 — 같으면 묶음이 다시 돈 것뿐. */ let key = ""; @@ -169,7 +175,8 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive { inflight = null; } - function refresh(): void { + /** 지금 노선으로 셈 — `proposal` 이면 그 관 목록 · 방식으로(답 = 관 초안 · 사진첩 한 걸음). */ + function refresh(proposal?: Proposal): void { const projectId = deps.runner.projectId(); if (!projectId) return; cancel(); @@ -177,7 +184,7 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive { const controller = new AbortController(); inflight = controller; paint(); - const body = liveRequestBody(sent.generation, route, deps.pipeRequest()); + const body = { ...liveRequestBody(sent.generation, route, deps.pipeRequest()), ...proposal }; void deps.runner .run(async () => { const response = await fetchLive(projectId, body, controller.signal); @@ -190,10 +197,20 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive { if (inflight !== controller) return; inflight = null; if (applied) stale = false; + if (applied && proposal) { + deps.rememberPipes(); + pageHistory()?.record(); + } paint(); }); } + /** [자동 제안] · [배관 배치 초기화] — 저장 분석이 지금 노선과 같으면 빠른 길(detail-basins). */ + function proposeDraft(proposal: Proposal): void { + if (analysisStale || draftMakesStale(route)) return refresh(proposal); + draft((projectId) => computeDetailBasins(projectId, proposal.points, proposal.auto)); + } + /** 평면 · 관 자리 조작 — 처음으로(요청 끊기 · 다시 흐림 또는 다시 3 s). */ function invalidate(): void { cancel(); @@ -264,7 +281,7 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive { input.name = "b05-drainage-propose"; input.addEventListener("change", () => { close(); - draft((projectId) => computeDetailBasins(projectId, deps.pipeRequest(), value)); + proposeDraft({ points: deps.pipeRequest(), auto: value }); }); const text = document.createElement("span"); text.textContent = `${order === 0 ? "①" : "②"} ${label} — ${hint}`; @@ -284,7 +301,7 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive { paint(); } - update.addEventListener("click", refresh); + update.addEventListener("click", () => refresh()); gear.addEventListener("click", openSettings); propose?.addEventListener("click", openPropose); @@ -300,10 +317,11 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive { else if (wasDraft) backToSaved(); return null; }, - refresh, + refresh: () => refresh(), suggest, markStale() { savedStale = true; + analysisStale = true; if (stale) return; stale = true; if (mode === "auto" && !inflight) timer = window.setTimeout(refresh, QUIET_MS); @@ -312,9 +330,10 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive { markFresh() { cancel(); stale = false; + analysisStale = false; paint(); }, - resetToStreams: () => draft((projectId) => computeDetailBasins(projectId, null)), + resetToStreams: () => proposeDraft({ points: null }), get stale() { return stale; }, diff --git a/resources/tester/helper_349_drainage_live_flow.cjs b/resources/tester/helper_349_drainage_live_flow.cjs index 50f56af99..08e7abfb5 100644 --- a/resources/tester/helper_349_drainage_live_flow.cjs +++ b/resources/tester/helper_349_drainage_live_flow.cjs @@ -348,6 +348,30 @@ async function main() { remembered: remembered - remembered0, records: records - records0, }; + // 저장 분석이 옛 노선(서버 낡음 답) → [업데이트] 뒤 [자동 제안] = 지금 노선으로 셈 길에 방식 실음 + await runner.run(async () => ({ basins: [], pipe_points: pipes, stale: "배수유역 낡음" })); + update.fire("click"); + answer(calls[calls.length - 1]); + await flush(); + const viaDetail = detailCalls.length; + modeRow = 0; + propose.fire("click"); + const liveCall = calls[calls.length - 1]; + answer(liveCall); + await flush(); + live.resetToStreams(); + const resetCall = calls[calls.length - 1]; + answer(resetCall); + await flush(); + out.proposeLive = { + auto: liveCall.body.auto, + points: liveCall.body.points.length, + resetPoints: resetCall.body.points, + resetAuto: resetCall.body.auto ?? null, + detailCalls: detailCalls.length - viaDetail, + remembered: remembered - remembered0, + records: records - records0, + }; // 입력 지문 — 판 번호 · 관 차례는 안 봄 · 꼭짓점 · 관 자리(cm) · id 는 봄 const { inputKey } = load(path.join(B05, "logic", "B05_Profile_Drainage_Live.ts")); const a = { id: "a1", chainage_m: 100 }; diff --git a/resources/tester/test_349_drainage_live_flow.py b/resources/tester/test_349_drainage_live_flow.py index 944424f58..7a84ae5a2 100644 --- a/resources/tester/test_349_drainage_live_flow.py +++ b/resources/tester/test_349_drainage_live_flow.py @@ -81,6 +81,16 @@ def test_자동_제안_두_방식_초기화_세류선만_한_걸음(r): "remembered": 3, # 셋 다 초안 "records": 3, # 답마다 사진첩 한 걸음 } + # 저장 분석이 옛 노선 — 지금 노선으로 셈 길(detail-basins 안 부름) · 같은 초안 · 한 걸음 + assert r["proposeLive"] == { + "auto": "all", + "points": 1, + "resetPoints": None, + "resetAuto": None, + "detailCalls": 0, + "remembered": 5, + "records": 5, + } def test_move_while_busy_cancels(r):