diff --git a/A00_Common/design/design_page.ts b/A00_Common/design/design_page.ts index 77b29b225..bfa6f04c4 100644 --- a/A00_Common/design/design_page.ts +++ b/A00_Common/design/design_page.ts @@ -23,7 +23,7 @@ import { workflowSteps } from "../b_page_scaffold"; import { clearWorkflowStateCache, fetchWorkflowState, - goToWorkflowStage, + moveStage, WORKFLOW_STEP_ROUTES, type WorkflowState, } from "../b_workflow_nav"; @@ -194,12 +194,13 @@ export function renderDesignPage(root: HTMLElement): MountedPage { currentStage: [2, 3, 4], onOptionsOpenChange: (isOpen) => isOpen && scrollToPick(shared.structures), // 499-1 routes: WORKFLOW_STEP_ROUTES, - // 종단설계 · 횡단설계 두 칸은 같은 페이지 — 주소만 바꿔 보기만 바꾼다. B07~B09 = 확정 뒤 이동(496-1). + // 종단설계 · 횡단설계 두 칸은 같은 페이지 — 주소만 바꿔 보기만 바꾼다. B07~B09 = 확정 뒤 이동(496-1) · + // 그 밖 = 페이지 넘김 잠금 길(520-4 · 떠남 확인은 잠금이 풀린 뒤 라우터가). onStepClick: (stepIndex) => { const route = WORKFLOW_STEP_ROUTES[stepIndex]; if (designViewOf(route)) navigateTo(route); else if (stepIndex >= 4) confirmAndGo(shared, route); - else goToWorkflowStage(projectId, route); + else void moveStage(projectId, route, []); }, }); layout.root.classList.add("design-page__layout"); diff --git a/A00_Common/design/design_save.ts b/A00_Common/design/design_save.ts index f814effb7..a9722939c 100644 --- a/A00_Common/design/design_save.ts +++ b/A00_Common/design/design_save.ts @@ -33,6 +33,7 @@ import { culvertOptionsPart } from "../../B06_Section/B06_Section_Api_Culvert_Op import { pageSettingsPart } from "../../B05_Profile/B05_Profile_Api_PageSettings"; import { DesignSaveError, + fetchDesignSaveProgress, postDesignSave, SAVE_PART_KEYS, type SavePart, @@ -136,6 +137,17 @@ const STEP_LABEL: Record = { cross: "횡단", }; +/** 잠금판 진행(520-4) — 서버 저장 차례 「n/N 이름」 · 안 돌면 null(페이지 넘김 `moveStage` progress 칸). */ +export async function designSaveProgress( + projectId: string, +): Promise<{ ratio: number; label: string } | null> { + const now = await fetchDesignSaveProgress(projectId); + if (!now?.active || !now.step || !now.index || !now.total) return null; + const name = + now.step === "basins" ? "배수유역 다시 셈" : (STEP_LABEL[now.step as SaveStepKey] ?? now.step); + return { ratio: (now.index - 1) / now.total, label: `${now.index}/${now.total} ${name}` }; +} + /** 서버가 버린 계획고 편집 자리(측점 · 관 · 구조물이 없는 자리 · 213-2)를 알린다. */ export function noticeDroppedEdits(saved: { dropped_edit_chainages?: number[] }): void { const dropped = saved.dropped_edit_chainages ?? []; diff --git a/A00_Common/design/design_save_api.ts b/A00_Common/design/design_save_api.ts index 4682568fa..952832ff6 100644 --- a/A00_Common/design/design_save_api.ts +++ b/A00_Common/design/design_save_api.ts @@ -108,3 +108,17 @@ export async function postDesignSave( window.clearTimeout(timer); } } + +/** 서버 저장 진행(520-4) — 차례 이름 · n/N · 지난 초 · 안 돌면 active 거짓 · 물음 실패 = null. */ +export async function fetchDesignSaveProgress(projectId: string): Promise<{ + active: boolean; + step?: string; + index?: number; + total?: number; + elapsed_s?: number; +} | null> { + const response = await fetch(`${API_BASE_URL}/projects/${projectId}/design/save/progress`, { + credentials: "include", + }); + return response.ok ? response.json() : null; +} diff --git a/A00_Common/design/design_shared.ts b/A00_Common/design/design_shared.ts index 26c29cd71..77d92cd9c 100644 --- a/A00_Common/design/design_shared.ts +++ b/A00_Common/design/design_shared.ts @@ -9,18 +9,14 @@ * 시설 줄(알약 · 비정규 측점 · 관 id 장부)은 페이지 한 벌(`design_facility_lane`) — 관 알림 · 노선 따라감을 여기서 넘김. * [저장] · 확정(B06 [>] · 단계 판 B07~B09 · 496-1)은 두 보기 몫을 모아 저장 한 줄기(`saveDesign`) 한 번으로 * 보낸다. 종단 보기의 확정은 횡단 보기를 숨긴 채 세워 횡단 자료를 같은 페이지에서 얻는다(`ensureView`). + * 확정 이동 = 페이지 넘김 공용 `moveStage` 잠금판 한 단계(서버 저장 차례 진행 · 520-4). * ========================================================================== */ import type { RoutePath } from "@config/config_frontend"; import { showConfirmDialog, showToast } from "@ui/ui_template_elements"; import { splitSidebarActions } from "@ui/ui_template_overlay"; import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; -import { - clearWorkflowStateCache, - fetchWorkflowState, - goToWorkflowStage, - WORKFLOW_STEP_ROUTES, -} from "../b_workflow_nav"; +import { clearWorkflowStateCache, fetchWorkflowState, moveStage } from "../b_workflow_nav"; import { readState, writeState } from "../b_page_state"; import { currentRoute, remountRoute } from "../router"; import { clearRouteLatestCache } from "../../B05_Profile/B05_Profile_Api_Fetch"; @@ -32,7 +28,13 @@ import { drainageSide } from "../../B05_Profile/B05_Profile_UI_Profile_SideTool" import { createDesignPanel, type DesignPanel } from "./design_panel"; import { createDesignProfile, type DesignProfile } from "./design_profile"; import { attachProfilePopout, attachSubPanelPopouts } from "./design_profile_popout"; -import { designSaveBusy, queueDesignSave, saveDesign, type PartOverride } from "./design_save"; +import { + designSaveBusy, + designSaveProgress, + queueDesignSave, + saveDesign, + type PartOverride, +} from "./design_save"; import type { SavePart, SavePartKey } from "./design_save_api"; import { designDirty } from "./design_leave"; import { pendingRouteSave } from "../../B05_Profile/B05_Profile_UI_RouteEdit_Apply"; @@ -366,7 +368,7 @@ export function sharedPanel(shared: DesignPageShared): DesignPanel { ), }, // 자료가 서기 전에는 잠근다 — 보기가 `designReady` 로 연다. - save: { run: () => saveSharedDesign(shared, false), enabled: false }, + save: { run: async () => void (await saveSharedDesign(shared, false)), enabled: false }, }, }, shared.view(), @@ -379,23 +381,26 @@ export function sharedPanel(shared: DesignPageShared): DesignPanel { return panel; } -/** 노선 바뀐 채 확정 → [B07 로] 고름 — 다시 세운 페이지가 자료를 세우면 새 노선으로 확정 뒤 그 주소(요구 300 B2). */ -const confirmOnReady = new Map(); +/** 노선 바뀐 채 확정 → [B07 로] 고름 — 다시 세운 페이지가 자료를 세우면 새 노선으로 확정 · 받는 값 = 그 결과 + * (요구 300 B2 · 잠금판이 다시 마운트를 넘어 기다렸다 이동 · 520-4). */ +const confirmOnReady = new Map void>(); +// ponytail: 다시 세운 페이지가 자료를 못 세우면 이만큼 뒤 머묾 — 마운트가 이보다 길면 늘림 +const CONFIRM_ON_READY_MS = 120_000; /** 자료가 섰다 — [저장] · 확정 이동을 연다(두 보기 어느 쪽이 먼저든 한 번이면 된다). */ export function designReady(shared: DesignPageShared): void { shared.ready = true; shared.panel?.dock.setActions({ save: { - run: () => saveSharedDesign(shared, false), + run: async () => void (await saveSharedDesign(shared, false)), enabled: true, title: L("B06_Profile_Btn_Save_Tip"), }, }); - const target = confirmOnReady.get(shared.projectId); - if (!target) return; + const settle = confirmOnReady.get(shared.projectId); + if (!settle) return; confirmOnReady.delete(shared.projectId); - void saveSharedDesign(shared, true, target); + void saveSharedDesign(shared, true).then(settle, () => settle(false)); } /** 고친 것 없고 단계 2 · 3 이미 완료 — [>]가 저장 없이 바로 감(요구 302 D1 · 그 밖은 496 확정 그대로). */ @@ -408,25 +413,18 @@ async function alreadyConfirmed(projectId: string): Promise { return !!done(2) && !!done(3); } -/** B06 [>] · 단계 판 B07~B09(496-1) — 확정 뒤 그 주소 · 실패 · 자료 서기 전 = 머묾. */ +/** B06 [>] · 단계 판 B07~B09(496-1 · 520-4) — 잠금판 「설계 확정」(서버 저장 차례 n/N · 낡은 배수유역은 관 차례가 + * 한 번 다시 셈 · 옛 확인 창 · 화면 [업데이트] 없음) 뒤 그 주소 · 실패 · 자료 서기 전 = 머묾. */ export function confirmAndGo(shared: DesignPageShared, route: RoutePath): void { if (!shared.ready) return void showToast(L("Design_Next_NotReady"), "info"); - const go = () => - alreadyConfirmed(shared.projectId).then((skip) => - skip ? goToWorkflowStage(shared.projectId, route) : saveSharedDesign(shared, true, route), - ); - void drainageFirst(shared).then((ready) => (ready ? go() : undefined)); -} - -/** 배수유역도가 낡은 채 B07 로 — 확인 창 → [업데이트] 끝난 뒤 감 · 취소 · 실패 = 머묾(요구 326-6). */ -async function drainageFirst(shared: DesignPageShared): Promise { - const side = shared.profile?.side; - if (!side?.stale()) return true; - if (!(await showConfirmDialog(L("Design_Drainage_Stale"), L("Design_Drainage_Stale_Go")))) - return false; - if (await side.update()) return true; - showToast(L("Design_Drainage_Stale_Failed"), "error"); - return false; + const id = shared.projectId; + void moveStage(id, route, [ + { + name: "설계 확정", + run: async () => (await alreadyConfirmed(id)) || saveSharedDesign(shared, true), + progress: () => designSaveProgress(id), + }, + ]); } /** @@ -446,23 +444,27 @@ async function routeChanged(shared: DesignPageShared): Promise { } /** - * [저장] · 확정 한 번(확정 = `target` 으로 감 · 기본 B07) — 관(배수유역도) · 계획고는 한 벌 종단도가, 구조물 · 상단측은 종단 보기가, + * [저장] · 확정 한 번(참 = 다 저장 · 확정 이동은 부른 쪽 `moveStage`) — 관(배수유역도) · 계획고는 한 벌 종단도가, 구조물 · 상단측은 종단 보기가, * 저장 앞 재계산 · 암거 옵션 · 횡단 본문은 횡단 보기가 낸다. 차례는 `saveDesign` 한 곳 · 줄 세움은 `queueDesignSave`(498-3). */ export async function saveSharedDesign( shared: DesignPageShared, confirm: boolean, - target: RoutePath = WORKFLOW_STEP_ROUTES[4], -): Promise { - await queueDesignSave(shared.projectId, confirm, () => saveOnce(shared, confirm, target)); +): Promise { + // 노선 바뀐 채 확정 = 다시 세운 페이지의 둘째 확정 — 저장 줄 밖에서 기다림(줄 안이면 둘째가 막힘) + const moved: { wait?: Promise } = {}; + const saved = await queueDesignSave(shared.projectId, confirm, () => + saveOnce(shared, confirm, moved), + ); + return saved === true && (moved.wait ? await moved.wait : true); } async function saveOnce( shared: DesignPageShared, confirm: boolean, - target: RoutePath, -): Promise { - if (await routeChanged(shared)) return; + moved: { wait?: Promise }, +): Promise { + if (await routeChanged(shared)) return false; // [저장] · [확정] 본문 = 횡단 보기 한 벌(표준 횡단면 · 전 측점 면적 · 카드 고침) — 들어온 화면 무관 · 숨긴 채라도 // 세움(B05 만 연 캐시 패치 길 지움 · 386-6) await shared.ensureView("cross"); @@ -471,11 +473,11 @@ async function saveOnce( const crossPart = crossSave?.crossPart; if (!crossPart) { showToast(L(confirm ? "B06_Profile_Confirm_Failed" : "B06_Profile_Save_Failed"), "error"); - return; + return false; } const profile = shared.profile; const side = profile?.side; - await saveDesign( + return saveDesign( { projectId: shared.projectId, routeId: () => routeIdOf(shared), @@ -510,7 +512,6 @@ async function saveOnce( // 서버가 정본을 다시 씀 — 다음 페이지(B07)가 브라우저 셈 · 옛 세션 사본 말고 정본을 받게(470 N1) invalidateSectionDetail(shared.projectId); showToast(L("B06_Profile_Confirm_Success"), "success"); - goToWorkflowStage(shared.projectId, target); return; } // 서버가 정본(계획선 · 횡단 · 면적)을 다시 썼다 — 공유 캐시를 한 번 비우고 두 보기가 새 상세 한 객체를 @@ -529,7 +530,16 @@ async function saveOnce( L("Design_Route_Moved_Go"), L("Design_Route_Moved_Stay"), ); - if (go) confirmOnReady.set(shared.projectId, target); + moved.wait = go + ? new Promise((settle) => { + confirmOnReady.set(shared.projectId, settle); + window.setTimeout(() => { + if (confirmOnReady.get(shared.projectId) !== settle) return; + confirmOnReady.delete(shared.projectId); + settle(false); + }, CONFIRM_ON_READY_MS); + }) + : Promise.resolve(false); }, failed: (error, isConfirm) => { const key = isConfirm ? "B06_Profile_Confirm_Failed" : "B06_Profile_Save_Failed"; diff --git a/B06_Section/B06_Section_Router_DesignSave.py b/B06_Section/B06_Section_Router_DesignSave.py index 9b669d2f3..1d0303c2e 100644 --- a/B06_Section/B06_Section_Router_DesignSave.py +++ b/B06_Section/B06_Section_Router_DesignSave.py @@ -35,6 +35,7 @@ from B05_Profile.B05_Profile_Schema import ( RouteConfirmRequest, ) from B05_Profile.B05_Profile_Structures_Schema import StructureSaveRequest +from B06_Section import B06_Section_Router_DesignSave_Progress as progress from B06_Section.B06_Section_Router_DesignSave_Route import RouteCarryRequest, StepFailed from B06_Section.B06_Section_Router_Stations import PlusStationsRequest from B06_Section.B06_Section_Schema import SectionConfirmRequest @@ -83,6 +84,15 @@ def _body(response: Any) -> dict[str, Any]: return dict(response or {}) +async def _stale_or_false(stale: Any, project_id: UUID) -> bool: + """진행 이름 고르기만 — 판정 실패는 저장을 막지 않음(관 차례가 다시 판정).""" + try: + return bool(await stale(project_id)) + except Exception: + logger.exception("한 본문 저장 배수유역 낡음 판정 실패: project_id=%s", project_id) + return False + + def _patched_points(stored: dict[str, Any], patches: dict[str, dict[str, Any]]) -> list | None: """저장된 관 목록에 관 옵션 조각 — 고친 것이 없으면 None(브라우저 `flushCulvertOptions` 짝).""" points, touched = [], False @@ -100,7 +110,11 @@ async def _run_steps( project_id: UUID, request: DesignSaveRequest, session: dict[str, Any] | None ) -> dict[str, Any]: """차례대로 안쪽 창구 · 차례 이름째 실패를 던짐 → 차례별 응답(알림 · 화면 맞춤용).""" - from B04_PreProcess.B04_PreProcess_Router_Basins import get_pipe_points, put_pipe_points + from B04_PreProcess.B04_PreProcess_Router_Basins import ( + _analysis_stale, + get_pipe_points, + put_pipe_points, + ) from B05_Profile.B05_Profile_Router import save_profile_alignment from B05_Profile.B05_Profile_Router_Lifecycle import confirm_latest_route, save_page_settings from B05_Profile.B05_Profile_Structures_Router import write_structures @@ -111,8 +125,26 @@ async def _run_steps( route_id = request.route_id moved = request.route is not None before: dict[str, Any] = {} + # 잠금판 진행(520-4) — 이 본문이 거칠 차례 목록 = N + planned = [ + name + for name, on in ( + ("route", moved), + ("settings", request.settings is not None), + ("pipes", request.pipes is not None or request.confirm), + ("structures", request.structures is not None), + ("plusStations", request.plus_stations is not None), + ("alignment", request.alignment is not None), + ("culvertOptions", bool(request.culvert_options)), + ("uphill", request.uphill is not None and not moved), + ("cross", moved or request.cross is not None), + ) + if on + ] + progress.begin(project_id, planned) - async def step(name: str, call: Any) -> dict[str, Any]: + async def step(name: str, call: Any, label: str | None = None) -> dict[str, Any]: + progress.mark(project_id, label or name) try: out[name] = _body(await call) except StepFailed as failed: @@ -146,8 +178,12 @@ async def _run_steps( if request.settings is not None: await step("settings", save_page_settings(project_id, request.settings)) if request.pipes is not None or request.confirm: + # [확정] + 낡은 분석 = 관 차례가 배수유역부터 다시 셈(한 번 · 화면 [업데이트] 없음 · 520-4) + basins = request.confirm and await _stale_or_false(_analysis_stale, project_id) pipes = await step( - "pipes", put_pipe_points(project_id, request.pipes or {}, session, request.confirm) + "pipes", + put_pipe_points(project_id, request.pipes or {}, session, request.confirm), + "basins" if basins else None, ) if pipes.get("stations_failed"): raise StepFailed("pipes", "관 자리 측점을 못 맞췄습니다") @@ -170,6 +206,7 @@ async def _run_steps( raise StepFailed("uphill", " · ".join(parts["failed_parts"]) + " 실패") # 노선 바뀜 = 옛 노선 세션 칸 → 새 측점 patch(Node) · 고침이 없어도 한 번(429 ④) if moved: + progress.mark(project_id, "cross") try: carried = await moving.carry_cross(project_id, route_id, request.carry, before) except StepFailed: @@ -251,9 +288,16 @@ async def save_design( ) finally: files.discard() + progress.end(project_id) return JSONResponse(content={"status": "success", "waited": waited, "steps": steps}) +@router.get("/{project_id}/design/save/progress") +async def get_design_save_progress(project_id: UUID) -> dict[str, Any]: + """잠금판 진행 묻기(520-4) — 차례 이름 · n/N · 지난 초 · 안 돌면 active 거짓.""" + return progress.snapshot(project_id) + + async def _restore(project_id: UUID, rows: dict[str, list], files: guard.StorageSnapshot) -> bool: """떠 둔 대로 — 성공 = 참 · 하나라도 못 되돌리면 로그 남기고 거짓(조용히 넘기지 않음).""" ok = True diff --git a/B06_Section/B06_Section_Router_DesignSave_Progress.py b/B06_Section/B06_Section_Router_DesignSave_Progress.py new file mode 100644 index 000000000..d75b648d9 --- /dev/null +++ b/B06_Section/B06_Section_Router_DesignSave_Progress.py @@ -0,0 +1,50 @@ +"""B05 · B06 한 본문 저장 진행(PLAN 520-4) — 지금 도는 차례 이름 · n/N · 지난 초를 메모리에. + +`B06_Section_Router_DesignSave._run_steps` 가 차례마다 `mark` · 끝에 `end` · 화면 잠금판이 1 s 마다 +`GET /design/save/progress`(창구는 `B06_Section_Router_DesignSave`)로 물음 +(페이지 넘김 `moveStage` 단계의 progress 칸). +""" + +from __future__ import annotations + +import time +from typing import Any +from uuid import UUID + +# ponytail: 프로세스 메모리 한 벌 — 서버 일꾼이 여럿이면 다른 일꾼 진행은 안 보임(그때 파일 · DB 로) +_PROGRESS: dict[str, dict[str, Any]] = {} + + +def begin(project_id: UUID, planned: list[str]) -> None: + """저장 한 번 시작 — 차례 이름 목록(N)을 적음.""" + _PROGRESS[str(project_id)] = {"planned": planned, "step": None, "started": time.monotonic()} + + +def mark(project_id: UUID, name: str) -> None: + """차례 들어감 — 목록 밖 이름(배수유역 다시 셈 등)은 앞 차례 자리 그대로.""" + entry = _PROGRESS.get(str(project_id)) + if entry is not None: + entry["step"] = name + + +def end(project_id: UUID) -> None: + _PROGRESS.pop(str(project_id), None) + + +def snapshot(project_id: UUID) -> dict[str, Any]: + """지금 진행 — 안 돌면 active 거짓.""" + entry = _PROGRESS.get(str(project_id)) + if entry is None or entry["step"] is None: + return {"active": False} + planned: list[str] = entry["planned"] + step = entry["step"] + # 배수유역 다시 셈 = 관 차례 안 · 자리는 관 + seat = "pipes" if step == "basins" else step + index = planned.index(seat) + 1 if seat in planned else len(planned) + return { + "active": True, + "step": step, + "index": index, + "total": len(planned), + "elapsed_s": round(time.monotonic() - entry["started"], 1), + } diff --git a/resources/tester/helper_ts_stub_loader.cjs b/resources/tester/helper_ts_stub_loader.cjs index 7115b4d5c..e0d400798 100644 --- a/resources/tester/helper_ts_stub_loader.cjs +++ b/resources/tester/helper_ts_stub_loader.cjs @@ -67,6 +67,7 @@ function makeLoader({ if (useCache && cache.has(full)) return cache.get(full); const stub = fileStub && fileStub(full); if (stub) return stub; + if (full.endsWith(".css")) return {}; // 화면 글꼴 import(ui_template_progress.css 등) — 트랜스파일 못 함 if (full.endsWith(".json")) { const data = JSON.parse(fs.readFileSync(full, "utf8")); if (useCache) cache.set(full, data); diff --git a/resources/tester/test_272_detail_memory.py b/resources/tester/test_272_detail_memory.py index d84c64d4c..475fe3f00 100644 --- a/resources/tester/test_272_detail_memory.py +++ b/resources/tester/test_272_detail_memory.py @@ -115,7 +115,8 @@ def test_화면_선만_바뀌면_낡음_다시_셈() -> None: def test_확정_성공도_상세_비움() -> None: # 470 N1 — [확정] 은 다음 페이지(B07)로 바로 감 · [저장]처럼 상세 메모리 · 옛 세션 사본을 비움 + # (이동은 잠금판 `moveStage` 가 · 520-4) shared = (ROOT / "A00_Common/design/design_shared.ts").read_text(encoding="utf-8") done = shared[shared.index(" done: async (isConfirm, reload = true) => {") :] - confirm = done[done.index("if (isConfirm) {") : done.index("goToWorkflowStage(")] + confirm = done[done.index("if (isConfirm) {") : done.index('L("B06_Profile_Confirm_Success")')] assert "invalidateSectionDetail(shared.projectId);" in confirm diff --git a/resources/tester/test_298_3_save_shape.py b/resources/tester/test_298_3_save_shape.py index 9c606ffb4..2ad88e8c0 100644 --- a/resources/tester/test_298_3_save_shape.py +++ b/resources/tester/test_298_3_save_shape.py @@ -97,15 +97,11 @@ def test_3_고친_것_없고_단계_완료면_바로_이동(): assert 's.state === "COMPLETE"' in fn and "done(2) && !!done(3)" in fn go = shared[shared.index("export function confirmAndGo(") :] go = go[: go.index("\n}\n")] - assert ( - "skip ? goToWorkflowStage(shared.projectId, route) : saveSharedDesign(shared, true, route)" - in go - ) + # 잠금판 한 단계 — 확정됐으면 저장 없이 이동(520-4 moveStage) + assert "run: async () => (await alreadyConfirmed(id)) || saveSharedDesign(shared, true)," in go # 줄 세움 = [저장] · 확정 한 곳 - assert ( - "await queueDesignSave(shared.projectId, confirm, () => saveOnce(shared, confirm, target));" - in shared - ) + assert "await queueDesignSave(shared.projectId, confirm, () =>" in shared + assert "saveOnce(shared, confirm, moved)," in shared def test_결과_한_줄_걷음(): diff --git a/resources/tester/test_512_2_drainage_update.py b/resources/tester/test_512_2_drainage_update.py index e09aecb64..752c1bbe1 100644 --- a/resources/tester/test_512_2_drainage_update.py +++ b/resources/tester/test_512_2_drainage_update.py @@ -55,14 +55,13 @@ def test_노선선은_답의_노선_낡은_답은_옛_그림(): assert "if (!routeFromAnswer) takeRoute(points, true);" in panel # 페이지 노선 = 첫 답 전만 -def test_B06_다음은_낡으면_확인_뒤_업데이트(): +def test_B06_다음은_잠금판_한_단계_배수유역은_서버가_한_번(): + # 520-4 — 낡음 확인 창 · 화면 [업데이트] 없앰 · 서버 [확정] 관 차례가 한 번 다시 셈(진행 이름 basins) shared = _read(ROOT / "A00_Common" / "design" / "design_shared.ts") go = shared[shared.index("export function confirmAndGo(") :] - go = go[: go.index("\n}\n", go.index("async function drainageFirst("))] - assert "void drainageFirst(shared).then((ready) => (ready ? go() : undefined));" in go - first = go[go.index("async function drainageFirst(") :] - assert ( - first.index("side?.stale()") - < first.index("showConfirmDialog(") - < first.index("side.update()") - ) + go = go[: go.index("\n}\n")] + assert "moveStage(id, route, [" in go and "progress: () => designSaveProgress(id)" in go + assert "drainageFirst" not in shared and "side.update()" not in go + save = _read(ROOT / "B06_Section" / "B06_Section_Router_DesignSave.py") + assert "basins = request.confirm and await _stale_or_false(_analysis_stale, project_id)" in save + assert '"basins" if basins else None' in save diff --git a/resources/tester/test_520_4_save_progress.py b/resources/tester/test_520_4_save_progress.py new file mode 100644 index 000000000..a869e0930 --- /dev/null +++ b/resources/tester/test_520_4_save_progress.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +"""520-4 — B05 · B06 한 본문 저장 진행(잠금판 「n/N 차례 이름」) 메모리 기록 · 배수유역 다시 셈 자리.""" + +from uuid import uuid4 + +from B06_Section import B06_Section_Router_DesignSave_Progress as progress + + +def test_차례_n_N_과_배수유역_자리() -> None: + pid = uuid4() + assert progress.snapshot(pid) == {"active": False} + progress.begin(pid, ["settings", "pipes", "structures", "cross"]) + assert progress.snapshot(pid) == {"active": False} # 첫 차례 전 + progress.mark(pid, "basins") + now = progress.snapshot(pid) + assert (now["step"], now["index"], now["total"]) == ("basins", 2, 4) # 관 차례 자리 + progress.mark(pid, "cross") + assert progress.snapshot(pid)["index"] == 4 + progress.mark(pid, "stages") # 목록 밖 = 끝 자리 + assert progress.snapshot(pid)["index"] == 4 + progress.end(pid) + assert progress.snapshot(pid) == {"active": False} diff --git a/resources/tester/test_b06_save_channels.py b/resources/tester/test_b06_save_channels.py index 6fa959d27..e50f57303 100644 --- a/resources/tester/test_b06_save_channels.py +++ b/resources/tester/test_b06_save_channels.py @@ -92,9 +92,9 @@ def test_두_페이지가_저장_한_줄기를_탄다(): shared = (ROOT / "A00_Common" / "design" / "design_shared.ts").read_text(encoding="utf-8") actions = (ROOT / "B05_Profile" / "B05_Profile_UI_Page_Actions.ts").read_text(encoding="utf-8") persist = (ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts").read_text(encoding="utf-8") - assert shared.count("await saveDesign(") == 1, "저장 한 줄기를 한 자리에서만 부름" + assert shared.count(" saveDesign(") == 1, "저장 한 줄기를 한 자리에서만 부름" page = (ROOT / "A00_Common" / "design" / "design_page.ts").read_text(encoding="utf-8") - assert "saveSharedDesign(shared, true, route)" in shared, "확정(B06 [>])이 한 줄기를 안 탐" + assert "saveSharedDesign(shared, true)" in shared, "확정(B06 [>])이 한 줄기를 안 탐" assert "confirmAndGo(shared, route)" in page, "B06 [>] · 단계 판 B07~B09 가 확정을 안 탐(496-1)" assert 'await shared.ensureView("cross")' in shared, "종단 보기 [확정]이 횡단 자료를 못 얻음" for name, text in (("종단 보기", actions), ("횡단 보기", persist), ("한 벌", shared)): diff --git a/resources/tester/test_b06_save_recompute.py b/resources/tester/test_b06_save_recompute.py index 3d140f9b1..89f5e7a5f 100644 --- a/resources/tester/test_b06_save_recompute.py +++ b/resources/tester/test_b06_save_recompute.py @@ -35,7 +35,9 @@ def test_저장이_면적을_모으기_전에_다시_계산한다() -> None: assert "collectSectionEdits(ctx)" in body[body.index("crossPart:") :], ( "면적 수집이 횡단 조각 밖" ) - shared = _body(SHARED.read_text(encoding="utf-8"), "export async function saveSharedDesign") + shared = _body( + SHARED.read_text(encoding="utf-8"), "export async function saveSharedDesign", 2000 + ) assert "prepare: crossSave?.prepare" in shared, "한 벌이 재계산을 저장 한 줄기에 안 넘긴다" save = SAVE.read_text(encoding="utf-8") # 472-6 — [저장] = 한 본문(`saveDesign` · 노선 바뀜도) · 재계산 뒤에 횡단 조각을 모음