feat(520-4): B05 · B06 페이지 넘김 공용 moveStage — B06 [>] 잠금판 「설계 확정」 서버 저장 차례 n/N 이름 · 배수유역 다시 셈 한 번

- B06 [>] · 단계 판 B07~B09 = moveStage 한 단계(설계 확정) · progress = GET /design/save/progress 1 s 물음 · B03 · B04 로 = moveStage 잠금 길(떠남 확인은 잠금 풀린 뒤)
- 낡음 확인 창 · 화면 side.update() 걷음 — 배수유역은 서버 [확정] 관 차례가 한 번만 셈 · 진행 이름 「배수유역 다시 셈」(basins)
- 서버 _run_steps step() 진행을 메모리(B06_Section_Router_DesignSave_Progress) · 차례 목록 = N · 끝에 비움
- 노선 바뀐 채 [>] = 물음(잠금판 위 · z 1050 > 900) → 다시 세운 페이지 둘째 확정을 저장 줄 밖에서 기다림 → 잠금판이 다시 마운트를 넘어 이어져 이동 · 120 s 넘으면 머묾
- saveSharedDesign 이 참 · 거짓을 돌려줌 · 확정 이동은 부른 쪽(moveStage)
- 낡은 시험 고침 — 512_2(확인 창 → 잠금판 한 단계 · 서버 basins) · 298_3 · 272 · b06_save_channels · b06_save_recompute(글 약속 새 길)
- helper_ts_stub_loader .css = 빈 모듈 — 520-3 ui_template_progress.css import 로 451 · 384_4 · 416 등 helper 시험이 깨짐(브레인 승인)
- 새 시험 test_520_4_save_progress(n/N · 배수유역 자리)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 08:19:22 +09:00
co-authored by Claude Opus 5.5
parent 80615cce2f
commit ed9e014db2
13 changed files with 221 additions and 69 deletions
+4 -3
View File
@@ -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");
+12
View File
@@ -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<SaveStepKey, string> = {
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 ?? [];
+14
View File
@@ -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;
}
+52 -42
View File
@@ -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<string, RoutePath>();
/** 노선 바뀐 채 확정 → [B07 로] 고름 — 다시 세운 페이지가 자료를 세우면 새 노선으로 확정 · 받는 값 = 그 결과
* (요구 300 B2 · 잠금판이 다시 마운트를 넘어 기다렸다 이동 · 520-4). */
const confirmOnReady = new Map<string, (ok: boolean) => 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<boolean> {
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<boolean> {
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<boolean> {
}
/**
* [저장] · 확정 한 번(확정 = `target` 으로 감 · 기본 B07) — 관(배수유역도) · 계획고는 한 벌 종단도가, 구조물 · 상단측은 종단 보기가,
* [저장] · 확정 한 번(참 = 다 저장 · 확정 이동은 부른 쪽 `moveStage`) — 관(배수유역도) · 계획고는 한 벌 종단도가, 구조물 · 상단측은 종단 보기가,
* 저장 앞 재계산 · 암거 옵션 · 횡단 본문은 횡단 보기가 낸다. 차례는 `saveDesign` 한 곳 · 줄 세움은 `queueDesignSave`(498-3).
*/
export async function saveSharedDesign(
shared: DesignPageShared,
confirm: boolean,
target: RoutePath = WORKFLOW_STEP_ROUTES[4],
): Promise<void> {
await queueDesignSave(shared.projectId, confirm, () => saveOnce(shared, confirm, target));
): Promise<boolean> {
// 노선 바뀐 채 확정 = 다시 세운 페이지의 둘째 확정 — 저장 줄 밖에서 기다림(줄 안이면 둘째가 막힘)
const moved: { wait?: Promise<boolean> } = {};
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<void> {
if (await routeChanged(shared)) return;
moved: { wait?: Promise<boolean> },
): Promise<boolean> {
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<boolean>((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";
+47 -3
View File
@@ -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
@@ -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),
}
@@ -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);
+2 -1
View File
@@ -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
+4 -8
View File
@@ -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_결과_한_줄_걷음():
@@ -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
@@ -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}
+2 -2
View File
@@ -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)):
+3 -1
View File
@@ -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` · 노선 바뀜도) · 재계산 뒤에 횡단 조각을 모음