fix(B05 배수유역): 369-2 노선 초안 중 · 저장 분석 낡음 때 [자동 제안] = 지금 노선으로 셈 길

- 저장 분석으로 셈 = 옛 노선 자리(초안) · 409(낡음) 이던 흠 — drainage/live 몸통에 auto 실음
- [배관 배치 초기화]도 같은 고름 · 답 = 관 초안 + 사진첩 한 걸음
- 검증_sub5 실측(live) — all 관 11 · 최대 간격 99 m · add 관 12 · 없음 = 세류선 관 1 + 후보 10

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
This commit is contained in:
2026-10-17 12:11:58 +09:00
co-authored by Claude Opus 5.5
parent d816c7c647
commit a24494119f
4 changed files with 68 additions and 11 deletions
@@ -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(
+27 -8
View File
@@ -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<typeof liveRequestBody>,
body: ReturnType<typeof liveRequestBody> | (Proposal & { generation: number }),
signal: AbortSignal,
): Promise<DetailBasinResponse & { status?: string; generation?: number }> {
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<HTMLElement>(".b05-drainage__viewport") as HTMLElement;
const header = deps.root.querySelector<HTMLElement>(".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;
},
@@ -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 };
@@ -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):