fix(B05 배수유역): 369-2 화면 검증 흠 둘 — 몰랐던 낡음 409 = 지금 노선 길로 다시 · 되돌리기 = 그 걸음 초안 속성 · id
- 화면이 처음 열릴 때 「낡음」을 모르면 [자동 제안]이 409 오류로 끝나던 흠 — 곧바로 drainage/live 로 다시 - 되돌리기가 관 자리만 되살리고 속성은 지금 것을 끌고 가 [배관 배치 초기화] 뒤 되돌려도 세월교가 사라지던 흠 — 되살린 걸음의 관 초안(세션)이 같은 자리면 그 속성 · id 로(adoptDraftPipes) - 시험 — 349 흐름(409 → 다시) · 347 pipe id(초안 속성 되살림) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
This commit is contained in:
@@ -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 = (
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -343,6 +343,22 @@ export function mountDrainageToggles(
|
||||
|
||||
/** 밖에서 바뀐 관 목록을 현재 목록과 맞춘다. 같으면 null — 되먹임 고리를 끊는 지점이다.
|
||||
* 다르면 새 목록을 돌려주되, 원래 생성 사유는 자리로 맞춰 이어 붙인다(기본/자동/수동 표시 보존). */
|
||||
/** 되돌리기(369) — 되살린 걸음의 관 초안(세션)이 `next` 와 같은 자리면 그 초안(속성 정본) · id 도
|
||||
* `next` 에 옮김. 자리가 다르면(구조물 폼이 옮김 등) null — 부르는 쪽이 속성을 따라 옮김. */
|
||||
export function adoptDraftPipes<D extends { chainage_m: number; id?: string }>(
|
||||
draft: ReadonlyArray<D> | null,
|
||||
next: PipePoint[],
|
||||
): ReadonlyArray<D> | 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<PipePoint>,
|
||||
chainages: ReadonlyArray<number>,
|
||||
|
||||
@@ -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 }]),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user