From b95b17ca07be1c740bc8da26f1cbcdce4d26b18a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 17 Oct 2026 12:35:39 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05=20=EB=B0=B0=EC=88=98=EC=9C=A0=EC=97=AD)?= =?UTF-8?q?:=20369-2=20=ED=99=94=EB=A9=B4=20=EA=B2=80=EC=A6=9D=20=ED=9D=A0?= =?UTF-8?q?=20=EB=91=98=20=E2=80=94=20=EB=AA=B0=EB=9E=90=EB=8D=98=20?= =?UTF-8?q?=EB=82=A1=EC=9D=8C=20409=20=3D=20=EC=A7=80=EA=B8=88=20=EB=85=B8?= =?UTF-8?q?=EC=84=A0=20=EA=B8=B8=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20=C2=B7=20?= =?UTF-8?q?=EB=90=98=EB=8F=8C=EB=A6=AC=EA=B8=B0=20=3D=20=EA=B7=B8=20?= =?UTF-8?q?=EA=B1=B8=EC=9D=8C=20=EC=B4=88=EC=95=88=20=EC=86=8D=EC=84=B1=20?= =?UTF-8?q?=C2=B7=20id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 화면이 처음 열릴 때 「낡음」을 모르면 [자동 제안]이 409 오류로 끝나던 흠 — 곧바로 drainage/live 로 다시 - 되돌리기가 관 자리만 되살리고 속성은 지금 것을 끌고 가 [배관 배치 초기화] 뒤 되돌려도 세월교가 사라지던 흠 — 되살린 걸음의 관 초안(세션)이 같은 자리면 그 속성 · id 로(adoptDraftPipes) - 시험 — 349 흐름(409 → 다시) · 347 pipe id(초안 속성 되살림) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn --- B04_PreProcess/B04_PreProcess_Router_Basins.py | 1 + B05_Profile/B05_Profile_UI_Drainage_Live.ts | 17 +++++++++++++++-- B05_Profile/B05_Profile_UI_Drainage_Panel.ts | 14 +++++++++----- B05_Profile/B05_Profile_UI_Drainage_Parts.ts | 16 ++++++++++++++++ .../tester/helper_347_pipe_id_reconcile.cjs | 16 +++++++++++++++- .../tester/helper_349_drainage_live_flow.cjs | 17 +++++++++++++++++ resources/tester/test_347_pipe_id.py | 4 ++++ resources/tester/test_349_drainage_live_flow.py | 7 +++++-- 8 files changed, 82 insertions(+), 10 deletions(-) diff --git a/B04_PreProcess/B04_PreProcess_Router_Basins.py b/B04_PreProcess/B04_PreProcess_Router_Basins.py index e382f6929..d4a6f9726 100644 --- a/B04_PreProcess/B04_PreProcess_Router_Basins.py +++ b/B04_PreProcess/B04_PreProcess_Router_Basins.py @@ -67,6 +67,7 @@ router = APIRouter(prefix="/api/projects", tags=["B04 Surface Drainage Basins"]) _NO_ANALYSIS = "배수유역 분석 결과가 없습니다. [유역 분석]을 먼저 실행하세요." # 노선 지문이 분석 때와 다름(옛 저장분은 지문 없음) — 옛 격자로 나누면 틀린 유역이 나온다(342-3). +# 머리 「배수유역 낡음」 = 화면 `STALE_PREFIX`(B05 Drainage_Live · 369) 짝. # [자동 제안] 방식(369) — None = 지금 목록 그대로(없으면 세류선 교차만) · all · add AUTO_MODES = (None, "all", "add") STALE_MESSAGE = ( diff --git a/B05_Profile/B05_Profile_UI_Drainage_Live.ts b/B05_Profile/B05_Profile_UI_Drainage_Live.ts index d7a64ff92..e83a28ef8 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Live.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Live.ts @@ -98,6 +98,9 @@ export function readDrainageMode(): DrainageMode { const L = (key: keyof typeof ui_locales): string => ui_locales[key][currentLanguageIndex]; +/** 서버 「낡음」 글 머리 — 짝 `B04_PreProcess_Router_Basins.STALE_MESSAGE`. */ +const STALE_PREFIX = "배수유역 낡음"; + /** [자동 제안] · [배관 배치 초기화] 요청 — 관 목록 없음(null) = 세류선 교차만. */ type Proposal = { points: DetailPipeInput[] | null; auto?: "all" | "add" }; @@ -205,10 +208,20 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive { }); } - /** [자동 제안] · [배관 배치 초기화] — 저장 분석이 지금 노선과 같으면 빠른 길(detail-basins). */ + /** [자동 제안] · [배관 배치 초기화] — 저장 분석이 지금 노선과 같으면 빠른 길(detail-basins) · + * 그 길이 「낡음」(409 · 화면이 아직 몰랐던 낡음)이면 곧바로 지금 노선으로 셈 길로 다시. */ function proposeDraft(proposal: Proposal): void { if (analysisStale || draftMakesStale(route)) return refresh(proposal); - draft((projectId) => computeDetailBasins(projectId, proposal.points, proposal.auto)); + draft(async (projectId) => { + try { + return await computeDetailBasins(projectId, proposal.points, proposal.auto); + } catch (error) { + if (!(error instanceof Error) || !error.message.startsWith(STALE_PREFIX)) throw error; + analysisStale = true; + queueMicrotask(() => refresh(proposal)); // 이 요청은 조용히 버림(뒤 차례가 받음) + throw new DOMException("stale", "AbortError"); + } + }); } /** 평면 · 관 자리 조작 — 처음으로(요청 끊기 · 다시 흐림 또는 다시 3 s). */ diff --git a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts index 20efdb083..c65c6d1e8 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts @@ -58,6 +58,7 @@ import { summaryText, basinIndexOfPipe, pipeMarkerColor, + adoptDraftPipes, reconcilePipes, WIDTH_KEY, type DrainageLayer, @@ -591,11 +592,14 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra const before = pipeEditor.pipes().map((pipe) => pipe.chainage_m); const next = reconcilePipes(pipeEditor.pipes(), chainages); if (!next) return; // 같은 목록 — 되돌아온 것이므로 여기서 끊는다 - // 되돌리기 · 구조물 폼으로 옮겨진 관도 속성을 데려감 - facilityStore.follow( - before, - next.map((pipe) => pipe.chainage_m), - ); + // 되돌리기 = 그 걸음 초안의 속성 · id(369) · 구조물 폼으로 옮겨진 관은 속성을 데려감 + const draft = adoptDraftPipes(readPendingPipes(projectId as string), next); + if (draft) facilityStore.replaceFromResponse(draft); + else + facilityStore.follow( + before, + next.map((pipe) => pipe.chainage_m), + ); pipeEditor.setPipes(next); void analyze(); }, diff --git a/B05_Profile/B05_Profile_UI_Drainage_Parts.ts b/B05_Profile/B05_Profile_UI_Drainage_Parts.ts index fc92c1ec2..babcebadf 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Parts.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Parts.ts @@ -343,6 +343,22 @@ export function mountDrainageToggles( /** 밖에서 바뀐 관 목록을 현재 목록과 맞춘다. 같으면 null — 되먹임 고리를 끊는 지점이다. * 다르면 새 목록을 돌려주되, 원래 생성 사유는 자리로 맞춰 이어 붙인다(기본/자동/수동 표시 보존). */ +/** 되돌리기(369) — 되살린 걸음의 관 초안(세션)이 `next` 와 같은 자리면 그 초안(속성 정본) · id 도 + * `next` 에 옮김. 자리가 다르면(구조물 폼이 옮김 등) null — 부르는 쪽이 속성을 따라 옮김. */ +export function adoptDraftPipes( + draft: ReadonlyArray | null, + next: PipePoint[], +): ReadonlyArray | null { + const same = + draft?.length === next.length && + draft.every((item, index) => Math.abs(item.chainage_m - next[index].chainage_m) < 0.01); + if (!draft || !same) return null; + next.forEach((pipe, index) => { + if (draft[index].id) pipe.id = draft[index].id; + }); + return draft; +} + export function reconcilePipes( before: ReadonlyArray, chainages: ReadonlyArray, diff --git a/resources/tester/helper_347_pipe_id_reconcile.cjs b/resources/tester/helper_347_pipe_id_reconcile.cjs index dc734afd3..fd84022cb 100644 --- a/resources/tester/helper_347_pipe_id_reconcile.cjs +++ b/resources/tester/helper_347_pipe_id_reconcile.cjs @@ -21,7 +21,7 @@ function loadTs(file, real) { return box.exports; } -const { reconcilePipes } = loadTs( +const { adoptDraftPipes, reconcilePipes } = loadTs( path.join(ROOT, "B05_Profile", "B05_Profile_UI_Drainage_Parts.ts"), ["design_selection"], ); @@ -35,5 +35,19 @@ process.stdout.write( moved: reconcilePipes(before, [100, 250]), added: reconcilePipes(before, [100, 200, 300]), twoMoved: reconcilePipes(before, [150, 250]), + // 369 되돌리기 — 그 걸음 초안이 같은 자리면 초안 속성 · id 를 씀 · 다르면 null + adopt: (() => { + const next = [ + { chainage_m: 100, reason: "user" }, + { chainage_m: 200, reason: "user" }, + ]; + const draft = [ + { chainage_m: 100, id: "a1", facility: "ford_bridge" }, + { chainage_m: 200.004, id: "b2" }, + ]; + return { draft: adoptDraftPipes(draft, next), next }; + })(), + adoptOther: adoptDraftPipes([{ chainage_m: 120, id: "z" }], [{ chainage_m: 100 }]), + adoptNone: adoptDraftPipes(null, [{ chainage_m: 100 }]), }), ); diff --git a/resources/tester/helper_349_drainage_live_flow.cjs b/resources/tester/helper_349_drainage_live_flow.cjs index 08e7abfb5..5e7f6af17 100644 --- a/resources/tester/helper_349_drainage_live_flow.cjs +++ b/resources/tester/helper_349_drainage_live_flow.cjs @@ -88,6 +88,7 @@ const state = new Map(); const detailCalls = []; const detailAutos = []; // [자동 제안](369) 방식 — 없으면 null let records = 0; // 사진첩 한 걸음(369) +let detailStale = false; // detail-basins 가 409 낡음으로 답함(369 — 화면이 몰랐던 낡음) let modeRow = 1; // ⚙ · [자동 제안] 창에서 고를 줄 — ⚙ 0 수동 · 1 자동 / 제안 0 전체 · 1 지정 포함 const stubs = { "@config/config_frontend": { API_BASE_URL: "/api" }, @@ -113,6 +114,7 @@ const stubs = { computeDetailBasins: async (projectId, points, auto) => { detailCalls.push(points); detailAutos.push(auto ?? null); + if (detailStale) throw new Error("배수유역 낡음 — 노선이 분석 때와 다릅니다."); return { basins: [], pipe_points: points ?? [], stale: null }; }, }, @@ -348,6 +350,21 @@ async function main() { remembered: remembered - remembered0, records: records - records0, }; + // 화면이 몰랐던 낡음 — 빠른 길이 409 낡음이면 곧바로 지금 노선으로 셈 길로 다시 + detailStale = true; + modeRow = 0; + const fallbackFrom = calls.length; + propose.fire("click"); + await flush(); + const fallbackCall = calls[calls.length - 1]; + out.proposeFallback = { + sent: calls.length - fallbackFrom, + auto: fallbackCall.body.auto, + status: status.textContent, + }; + answer(fallbackCall); + await flush(); + detailStale = false; // 저장 분석이 옛 노선(서버 낡음 답) → [업데이트] 뒤 [자동 제안] = 지금 노선으로 셈 길에 방식 실음 await runner.run(async () => ({ basins: [], pipe_points: pipes, stale: "배수유역 낡음" })); update.fire("click"); diff --git a/resources/tester/test_347_pipe_id.py b/resources/tester/test_347_pipe_id.py index 8d2175cee..4d334b6a4 100644 --- a/resources/tester/test_347_pipe_id.py +++ b/resources/tester/test_347_pipe_id.py @@ -95,3 +95,7 @@ def test_screen_reconcile_carries_id(): assert result["moved"][1]["reason"] == "confirmed" assert [p.get("id") for p in result["added"]] == ["a1", "b2", None] assert [p.get("id") for p in result["twoMoved"]] == [None, None] + adopt = result["adopt"] + assert [p["id"] for p in adopt["next"]] == ["a1", "b2"] # 369 되돌리기 — 그 걸음 초안 id + assert adopt["draft"][0]["facility"] == "ford_bridge" + assert result["adoptOther"] is None and result["adoptNone"] is None diff --git a/resources/tester/test_349_drainage_live_flow.py b/resources/tester/test_349_drainage_live_flow.py index 7a84ae5a2..16ccc91f3 100644 --- a/resources/tester/test_349_drainage_live_flow.py +++ b/resources/tester/test_349_drainage_live_flow.py @@ -88,9 +88,12 @@ def test_자동_제안_두_방식_초기화_세류선만_한_걸음(r): "resetPoints": None, "resetAuto": None, "detailCalls": 0, - "remembered": 5, - "records": 5, + "remembered": 6, # 앞 「409 → 다시」 한 번 포함 + "records": 6, } + fallback = r["proposeFallback"] + assert fallback["sent"] == 1 and fallback["auto"] == "all" + assert "낡음" not in fallback["status"] # 409 글을 오류로 안 띄움(조용히 다시) def test_move_while_busy_cancels(r):