feat(472-6 6-A 브라우저 한 본문): 노선 안 바뀐 [저장] · [확정] = 차례 조각을 모아 /design/save 한 요청
- design_save: 노선 초안이 없으면 saveOneBody — 차례마다 조각(본문 · 저장 뒤 정리) · 서버가 다 저장한 뒤에만 정리(초안 비움 · 화면 맞춤) · 실패 = 차례 이름째 「아무것도 저장 안 함」 · 되돌림 실패 = 「복구 확인 필요」 · 기다림 알림 - 조각 함수: 페이지 설정 · 관 · 구조물(보기 목록) · 추가 측점 · 계획고(한 벌 종단도) · 관 옵션 · 상단측 · 횡단(횡단 보기) — 옛 flush 함수는 노선 바뀐 저장 길에 그대로(조각을 써서 같은 본문) - 보기 몫 steps · cross → parts · crossPart · 노선 바뀐 저장 길은 옛 차례 그대로(6-B) - 옛 9 차례를 단언하던 시험 10 개를 한 본문 꼴로(옛 PUT 줄 → SAVE 줄 · 조각 정리 차례) · 위키 [저장] 표 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,11 @@ import {
|
||||
toDesignProfile,
|
||||
} from "../../B05_Profile/logic/B05_Profile_UI_Profile_Data";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { saveProfileAlignment } from "../../B05_Profile/B05_Profile_Api_Fetch";
|
||||
import {
|
||||
saveProfileAlignment,
|
||||
type ProfileAlignmentSaveResponse,
|
||||
} from "../../B05_Profile/B05_Profile_Api_Fetch";
|
||||
import type { SavePart } from "./design_save_api";
|
||||
import { hasStaleDesigns } from "../../B06_Section/B06_Section_UI_Section_Common";
|
||||
import {
|
||||
blocksMinCover,
|
||||
@@ -297,6 +301,28 @@ export function createDesignProfile(ctx: DesignProfileContext) {
|
||||
listedBase(storedBase, detail, planted ? irregularStations : null);
|
||||
|
||||
/** 편집 후 다시 그린다 — 길게 누르기(초당 10회)도 한 프레임에 한 번만 그림. */
|
||||
/** 한 본문 [저장] 조각(472-6) — 편집 없음 · 노선 초안 미리보기면 null · 저장 뒤 서버 선형으로 다시 그림. */
|
||||
function alignmentSavePart(): SavePart | null {
|
||||
if (!routeId || !store.dirty() || store === preview) return null; // 노선 초안 미리보기 편집은 안 보냄
|
||||
return {
|
||||
body: { route_id: routeId, ...store.edits() },
|
||||
after: (response) => adoptSaved(response as unknown as ProfileAlignmentSaveResponse),
|
||||
};
|
||||
}
|
||||
|
||||
function adoptSaved(saved: ProfileAlignmentSaveResponse): void {
|
||||
noticeDroppedEdits(saved);
|
||||
const next = saved.profile_alignment as ProfileAlignment | undefined;
|
||||
if (next?.base_pvi?.length) {
|
||||
storedBase = toAlignmentBase(next);
|
||||
base = listed();
|
||||
alignment = buildAlignment(base!, store.edits());
|
||||
if (detail) detail.longitudinal.profile_alignment = next;
|
||||
}
|
||||
store.markSaved();
|
||||
draw();
|
||||
}
|
||||
|
||||
function rebuild(): void {
|
||||
if (base) alignment = buildAlignment(base, store.edits());
|
||||
pagePipeline.pipelineGrade(projectId, store.edits());
|
||||
@@ -597,19 +623,17 @@ export function createDesignProfile(ctx: DesignProfileContext) {
|
||||
},
|
||||
/** [확정] 직전에 호출한다. 편집이 없으면 아무 것도 하지 않는다. */
|
||||
async save(): Promise<void> {
|
||||
if (!routeId || !store.dirty() || store === preview) return; // 노선 초안 미리보기 편집은 안 보냄
|
||||
const saved = await saveProfileAlignment(projectId, routeId, store.edits());
|
||||
noticeDroppedEdits(saved);
|
||||
const next = saved.profile_alignment as ProfileAlignment | undefined;
|
||||
if (next?.base_pvi?.length) {
|
||||
storedBase = toAlignmentBase(next);
|
||||
base = listed();
|
||||
alignment = buildAlignment(base!, store.edits());
|
||||
if (detail) detail.longitudinal.profile_alignment = next;
|
||||
}
|
||||
store.markSaved();
|
||||
draw();
|
||||
const part = alignmentSavePart();
|
||||
if (!part) return;
|
||||
await part.after?.(
|
||||
(await saveProfileAlignment(projectId, routeId!, store.edits())) as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
>,
|
||||
);
|
||||
},
|
||||
/** 한 본문 [저장] 조각(472-6) — 편집 없음 · 노선 초안 미리보기면 null · 저장 뒤 서버 선형으로 다시 그림. */
|
||||
savePart: () => alignmentSavePart(),
|
||||
clear() {
|
||||
detail = null;
|
||||
base = null;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import { hideLoadingOverlay, showLoadingOverlay, showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
flushPendingPipes,
|
||||
pipesPart,
|
||||
readPendingPipes,
|
||||
writePendingPipes,
|
||||
} from "../../B05_Profile/B05_Profile_Api_Pipes_Draft";
|
||||
@@ -34,17 +35,39 @@ import {
|
||||
fetchStructures,
|
||||
flushPendingStructures,
|
||||
readPendingStructures,
|
||||
structuresPart,
|
||||
writePendingStructures,
|
||||
} from "../../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { flushPlusStations, outsideNotice } from "../../B05_Profile/B05_Profile_Api_PlusStations";
|
||||
import { flushRouteParts, saveProfileAlignment } from "../../B05_Profile/B05_Profile_Api_Fetch";
|
||||
import {
|
||||
flushPlusStations,
|
||||
outsideNotice,
|
||||
plusStationsPart,
|
||||
} from "../../B05_Profile/B05_Profile_Api_PlusStations";
|
||||
import {
|
||||
flushRouteParts,
|
||||
routePartsBody,
|
||||
saveProfileAlignment,
|
||||
} from "../../B05_Profile/B05_Profile_Api_Fetch";
|
||||
import {
|
||||
markAlignmentSaved,
|
||||
pendingAlignment,
|
||||
writeAlignmentDraft,
|
||||
} from "../../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||
import { flushCulvertOptions } from "../../B06_Section/B06_Section_Api_Culvert_Options";
|
||||
import { flushPageSettings } from "../../B05_Profile/B05_Profile_Api_PageSettings";
|
||||
import {
|
||||
culvertOptionsPart,
|
||||
flushCulvertOptions,
|
||||
} from "../../B06_Section/B06_Section_Api_Culvert_Options";
|
||||
import {
|
||||
flushPageSettings,
|
||||
pageSettingsPart,
|
||||
} from "../../B05_Profile/B05_Profile_Api_PageSettings";
|
||||
import {
|
||||
DesignSaveError,
|
||||
postDesignSave,
|
||||
SAVE_PART_KEYS,
|
||||
type SavePart,
|
||||
type SavePartKey,
|
||||
} from "./design_save_api";
|
||||
import { clearState, stateKey } from "../b_page_state";
|
||||
import {
|
||||
carryAfterRouteSave,
|
||||
@@ -75,18 +98,23 @@ export interface DesignSaveContext {
|
||||
/** 저장 앞 준비 — 면적을 모으기 전 전 측점 재계산(B06). */
|
||||
prepare?: () => Promise<void>;
|
||||
/**
|
||||
* 차례마다 **기본 창구 대신** 도는 일. 없으면 기본 창구(세션 초안 → 정본)가 돈다.
|
||||
* `pipes` 만 예외 — 기본(세션 초안)이 먼저 나가고 그 뒤에 돈다(저장분으로 화면만 맞춤 · PUT 없음 · 433-7).
|
||||
* 한 본문 차례 조각을 보기가 고침(472-6) — 받는 값 = 기본 조각(세션 초안 → 본문 · 없으면 null) ·
|
||||
* 돌려주는 조각이 그 차례 본문 · 저장 뒤 정리(보기 화면 맞춤을 덧붙이거나 보기 목록으로 갈음).
|
||||
*/
|
||||
steps?: Partial<Record<Exclude<DesignSaveStep, "cross" | "route">, () => Promise<void>>>;
|
||||
/** 횡단 — 보기마다 본문이 다르다. `confirm` 이면 단계를 닫는 길로. */
|
||||
cross: (confirm: boolean) => Promise<void>;
|
||||
parts?: Partial<Record<SavePartKey, PartOverride>>;
|
||||
/** 횡단 조각 — 보기마다 본문이 다르다(표준 횡단 · 카드 고침 · 유토). */
|
||||
crossPart: (confirm: boolean) => Promise<SavePart | null>;
|
||||
/** 모두 나간 뒤 — 캐시 비우기 · 화면 다시 읽기 · 알림 · 다음 단계. */
|
||||
done?: (confirm: boolean) => Promise<void> | void;
|
||||
/** 멈춘 저장을 알린다. */
|
||||
failed: (error: unknown, confirm: boolean) => void;
|
||||
}
|
||||
|
||||
export type PartOverride = (
|
||||
base: SavePart | null,
|
||||
confirm: boolean,
|
||||
) => Promise<SavePart | null> | SavePart | null;
|
||||
|
||||
const saving = new Set<string>();
|
||||
|
||||
/** 초안을 내보내는 중인가(323-9) — 그동안 바뀜 신호로 낡은 상세 · 빈 초안을 그리지 않음(뒤처리가 한 번 다시). */
|
||||
@@ -241,6 +269,9 @@ export async function saveDesign(
|
||||
const confirm = options.confirm ?? false;
|
||||
const fromRouteId = ctx.routeId();
|
||||
if (fromRouteId === null) return false;
|
||||
// 노선 초안이 없으면 한 본문 한 요청(472-6 6-A · 서버가 전부 아니면 없음) · 있으면 옛 차례(6-B 전)
|
||||
const routeBody = await pendingRouteSave(ctx.projectId);
|
||||
if (!routeBody) return saveOneBody(ctx, confirm, fromRouteId);
|
||||
let routeId = fromRouteId;
|
||||
/** 노선을 새로 저장했으면 그 결과 — 뒤 차례는 새 노선 · 화면 몫(옛 노선 판) 일은 건너뜀. */
|
||||
let routeSaved: Awaited<ReturnType<typeof saveRouteDraft>> | null = null;
|
||||
@@ -253,8 +284,7 @@ export async function saveDesign(
|
||||
for (const step of DESIGN_SAVE_ORDER) {
|
||||
try {
|
||||
if (step === "route") {
|
||||
const body = await pendingRouteSave(ctx.projectId); // 안 바뀌었으면 null — 안 보냄
|
||||
if (!body) continue;
|
||||
const body = routeBody;
|
||||
const [before, beforeStructures] = await Promise.all([
|
||||
savedPipesIfDraft(ctx.projectId),
|
||||
savedStructuresIfDraft(ctx.projectId),
|
||||
@@ -273,27 +303,15 @@ export async function saveDesign(
|
||||
continue;
|
||||
}
|
||||
if (step === "cross") {
|
||||
if (!routeSaved) await ctx.cross(confirm);
|
||||
else {
|
||||
carried = true;
|
||||
const outside = await carryAfterRouteSave(ctx.projectId, fromRouteId, routeId);
|
||||
showToast(routeSaveSummary(routeSaved, outside), "info");
|
||||
}
|
||||
carried = true;
|
||||
const outside = await carryAfterRouteSave(ctx.projectId, fromRouteId, routeId);
|
||||
if (routeSaved) showToast(routeSaveSummary(routeSaved, outside), "info");
|
||||
continue;
|
||||
}
|
||||
// 상단측은 옛 누가거리 열쇠 — 노선이 바뀌면 횡단 차례(`carryAfterRouteSave`)가 새 측점으로 옮긴 뒤 보냄(443-2)
|
||||
if (step === "uphill" && routeSaved) continue;
|
||||
// 노선이 바뀌면 화면 몫 일(옛 노선 판 목록 · 계획고 판)은 건너뛰고 기본 창구만 새 노선으로.
|
||||
const own = routeSaved ? undefined : ctx.steps?.[step];
|
||||
const base = defaultStep(step, ctx.projectId, routeId, fromRouteId, confirm);
|
||||
const run =
|
||||
step === "pipes"
|
||||
? async () => {
|
||||
await base();
|
||||
await own?.();
|
||||
}
|
||||
: (own ?? base);
|
||||
await run();
|
||||
await defaultStep(step, ctx.projectId, routeId, fromRouteId, confirm)();
|
||||
} catch (error) {
|
||||
throw stepFailed(step, error); // 그 차례에서 멈춤 · 이름째 알림(429 ⑨)
|
||||
}
|
||||
@@ -322,3 +340,86 @@ export async function saveDesign(
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
/** 한 본문 기본 조각 — 세션 초안 그대로(옛 기본 창구와 같은 본문). */
|
||||
async function defaultPart(
|
||||
key: Exclude<SavePartKey, "cross">,
|
||||
projectId: string,
|
||||
routeId: number,
|
||||
confirm: boolean,
|
||||
): Promise<SavePart | null> {
|
||||
switch (key) {
|
||||
case "settings":
|
||||
return pageSettingsPart(projectId);
|
||||
case "pipes":
|
||||
return pipesPart(projectId, confirm);
|
||||
case "structures":
|
||||
return structuresPart(projectId);
|
||||
case "plusStations":
|
||||
return plusStationsPart(projectId);
|
||||
case "alignment": {
|
||||
const draft = pendingAlignment(routeId);
|
||||
if (!draft) return null;
|
||||
return {
|
||||
body: { route_id: routeId, ...draft },
|
||||
after: (response) => {
|
||||
if (response) noticeDroppedEdits(response);
|
||||
markAlignmentSaved(routeId);
|
||||
},
|
||||
};
|
||||
}
|
||||
case "culvertOptions":
|
||||
return culvertOptionsPart(stateKey("culvertopt", projectId, routeId));
|
||||
case "uphill":
|
||||
return {
|
||||
body: await routePartsBody(projectId),
|
||||
after: () => clearState("uphill", projectId),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** 서버 실패 → 화면 글 — 차례 이름째 · 모두 그대로 · 되돌림마저 실패면 「복구 확인 필요」. */
|
||||
function oneBodyError(error: unknown): Error {
|
||||
if (!(error instanceof DesignSaveError))
|
||||
return error instanceof Error ? error : new Error(String(error));
|
||||
if (!error.restored) return new Error(`저장 실패 · 복구 확인 필요 — ${error.message}`);
|
||||
const where = error.step ? `${STEP_LABEL[error.step]} 차례에서 ` : "";
|
||||
return new Error(`${where}실패 — 아무것도 저장하지 않았습니다(모두 그대로). ${error.message}`);
|
||||
}
|
||||
|
||||
/** 노선 안 바뀐 [저장] · [확정] — 조각을 모아 서버 한 창구에 한 번(472-6 6-A). 서버가 다 저장한 뒤에만 조각
|
||||
* 정리(초안 비움 · 화면 맞춤) · 실패면 초안 그대로(다음 [저장]에서 다시). */
|
||||
async function saveOneBody(
|
||||
ctx: DesignSaveContext,
|
||||
confirm: boolean,
|
||||
routeId: number,
|
||||
): Promise<boolean> {
|
||||
showLoadingOverlay();
|
||||
saving.add(ctx.projectId);
|
||||
try {
|
||||
await ctx.prepare?.();
|
||||
const parts: Partial<Record<SavePartKey, SavePart | null>> = {};
|
||||
for (const key of SAVE_PART_KEYS) {
|
||||
if (key === "cross") {
|
||||
parts.cross = await ctx.crossPart(confirm);
|
||||
continue;
|
||||
}
|
||||
const base = await defaultPart(key, ctx.projectId, routeId, confirm);
|
||||
const own = ctx.parts?.[key];
|
||||
parts[key] = own ? await own(base, confirm) : base;
|
||||
}
|
||||
const result = await postDesignSave(ctx.projectId, routeId, confirm, parts);
|
||||
if (result.waited) showToast("앞 저장이 끝나기를 기다린 뒤 저장했습니다.", "info");
|
||||
for (const key of SAVE_PART_KEYS) await parts[key]?.after?.(result.steps[key]);
|
||||
clearState("structure-type-cache", ctx.projectId); // 종류별 캐시는 저장 뒤 비움(54-32)
|
||||
saving.delete(ctx.projectId); // 다 나감 — 뒤처리는 새 상세로 그림
|
||||
await ctx.done?.(confirm);
|
||||
return true;
|
||||
} catch (error) {
|
||||
ctx.failed(oneBodyError(error), confirm);
|
||||
return false;
|
||||
} finally {
|
||||
saving.delete(ctx.projectId);
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/design_save_api.ts
|
||||
* [저장] · [확정] 한 본문(PLAN 472-6 6-A) — 차례 조각 꼴 · 서버 한 창구 `POST /design/save`.
|
||||
*
|
||||
* 차례마다 조각 하나 = 본문(없으면 null · 서버가 그 차례를 건너뜀) + 저장 뒤 정리(초안 비움 · 화면 맞춤).
|
||||
* 서버는 전부 저장하거나 아무것도 안 바꿈 — 실패면 차례 이름 · 까닭(`restored` 거짓 = 복구 확인 필요).
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_ANALYSIS_TIMEOUT_MS, API_BASE_URL } from "@config/config_frontend";
|
||||
|
||||
/** 서버 본문 칸 차례 그대로(`B06_Section_Router_DesignSave`). */
|
||||
export const SAVE_PART_KEYS = [
|
||||
"settings",
|
||||
"pipes",
|
||||
"structures",
|
||||
"plusStations",
|
||||
"alignment",
|
||||
"culvertOptions",
|
||||
"uphill",
|
||||
"cross",
|
||||
] as const;
|
||||
|
||||
export type SavePartKey = (typeof SAVE_PART_KEYS)[number];
|
||||
|
||||
/** 차례 조각 — `after` 는 서버가 다 저장한 뒤에만(그 차례 응답을 받음). */
|
||||
export interface SavePart {
|
||||
body: unknown | null;
|
||||
after?: (response: Record<string, unknown> | undefined) => Promise<void> | void;
|
||||
}
|
||||
|
||||
const BODY_KEY: Record<SavePartKey, string> = {
|
||||
settings: "settings",
|
||||
pipes: "pipes",
|
||||
structures: "structures",
|
||||
plusStations: "plus_stations",
|
||||
alignment: "alignment",
|
||||
culvertOptions: "culvert_options",
|
||||
uphill: "uphill",
|
||||
cross: "cross",
|
||||
};
|
||||
|
||||
export interface DesignSaveResult {
|
||||
waited: boolean;
|
||||
steps: Partial<Record<SavePartKey, Record<string, unknown>>>;
|
||||
}
|
||||
|
||||
/** 실패 — 차례 이름(`step`) · 되돌림 됨(`restored`). */
|
||||
export class DesignSaveError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly step: SavePartKey | null,
|
||||
readonly restored: boolean,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/** 조각 본문을 모아 한 번에 보냄. */
|
||||
export async function postDesignSave(
|
||||
projectId: string,
|
||||
routeId: number,
|
||||
confirm: boolean,
|
||||
parts: Partial<Record<SavePartKey, SavePart | null>>,
|
||||
): Promise<DesignSaveResult> {
|
||||
const body: Record<string, unknown> = { route_id: routeId, confirm };
|
||||
for (const key of SAVE_PART_KEYS) {
|
||||
const part = parts[key];
|
||||
if (part && part.body !== null && part.body !== undefined) body[BODY_KEY[key]] = part.body;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
// [확정]은 낡은 배수유역을 다시 셈 — 옛 차례들의 시간 한도 합 만큼
|
||||
const timer = window.setTimeout(() => controller.abort(), API_ANALYSIS_TIMEOUT_MS * 3);
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/design/save`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok)
|
||||
throw new DesignSaveError(
|
||||
payload?.message ?? `HTTP ${response.status}`,
|
||||
(payload?.step as SavePartKey) ?? null,
|
||||
payload?.restored !== false,
|
||||
);
|
||||
return { waited: payload?.waited === true, steps: payload?.steps ?? {} };
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === "AbortError")
|
||||
throw new DesignSaveError(
|
||||
"저장 응답을 시간 안에 못 받았습니다 — 새로고침해 저장됐는지 확인해 주세요.",
|
||||
null,
|
||||
false,
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,8 @@ 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 { saveDesign, type DesignSaveStep } from "./design_save";
|
||||
import { saveDesign, type PartOverride } from "./design_save";
|
||||
import type { SavePart, SavePartKey } from "./design_save_api";
|
||||
import { markDesignClean } from "./design_leave";
|
||||
import { resetDesign } from "./design_reset";
|
||||
import type { DesignView } from "./design_dock";
|
||||
@@ -71,10 +72,10 @@ export interface DesignProfileBinding {
|
||||
export interface DesignViewSave {
|
||||
routeId: () => number | null;
|
||||
prepare?: () => Promise<void>;
|
||||
steps?: Partial<
|
||||
Record<Exclude<DesignSaveStep, "cross" | "pipes" | "alignment">, () => Promise<void>>
|
||||
>;
|
||||
cross?: (confirm: boolean) => Promise<void>;
|
||||
/** 한 본문 차례 조각 고침(472-6) — 구조물 · 상단측(종단 보기) 등. */
|
||||
parts?: Partial<Record<SavePartKey, PartOverride>>;
|
||||
/** 횡단 조각 — 횡단 보기 몫(표준 횡단 · 카드 고침 · 유토). */
|
||||
crossPart?: (confirm: boolean) => Promise<SavePart | null>;
|
||||
/** 저장 뒤 이 보기만의 캐시 비우기 · 다시 읽기(알림 · 다음 단계는 여기서 한다). */
|
||||
done?: (confirm: boolean) => Promise<void> | void;
|
||||
}
|
||||
@@ -399,8 +400,8 @@ export async function saveSharedDesign(shared: DesignPageShared, confirm: boolea
|
||||
await shared.ensureView("cross");
|
||||
const profileSave = shared.bindings.profile?.save;
|
||||
const crossSave = shared.bindings.cross?.save;
|
||||
const crossBody = crossSave?.cross;
|
||||
if (!crossBody) {
|
||||
const crossPart = crossSave?.crossPart;
|
||||
if (!crossPart) {
|
||||
showToast(L(confirm ? "B06_Profile_Confirm_Failed" : "B06_Profile_Save_Failed"), "error");
|
||||
return;
|
||||
}
|
||||
@@ -411,16 +412,26 @@ export async function saveSharedDesign(shared: DesignPageShared, confirm: boolea
|
||||
projectId: shared.projectId,
|
||||
routeId: () => routeIdOf(shared),
|
||||
prepare: crossSave?.prepare,
|
||||
steps: {
|
||||
// 관 정본 PUT 은 세션 초안 한 번(기본 창구) — 배수유역도는 저장분을 다시 받아 화면만 맞춤(433-7 · 판
|
||||
parts: {
|
||||
// 관 본문은 세션 초안(기본 조각) — 배수유역도는 저장 뒤 저장분을 다시 받아 화면만 맞춤(433-7 · 판
|
||||
// 메모리를 다시 보내면 B06 고침 직후 · 판이 안 선 [저장]에서 관을 잃음).
|
||||
...(side ? { pipes: async () => side.reload(confirm) } : {}),
|
||||
// 계획고 초안 — 한 벌 종단도가 저장하고 다시 그린다.
|
||||
...(profile ? { alignment: () => profile.save() } : {}),
|
||||
...profileSave?.steps,
|
||||
...crossSave?.steps,
|
||||
...(side
|
||||
? {
|
||||
pipes: (base: SavePart | null) => ({
|
||||
body: base?.body ?? null,
|
||||
after: async (response: Record<string, unknown> | undefined) => {
|
||||
await base?.after?.(response);
|
||||
await side.reload(confirm);
|
||||
},
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
// 계획고 초안 — 한 벌 종단도가 본문을 내고 저장 뒤 서버 선형으로 다시 그린다.
|
||||
...(profile ? { alignment: () => profile.savePart() } : {}),
|
||||
...profileSave?.parts,
|
||||
...crossSave?.parts,
|
||||
},
|
||||
cross: crossBody,
|
||||
crossPart,
|
||||
done: async (isConfirm) => {
|
||||
markDesignClean(shared.projectId); // 떠남 확인 — 지금 초안이 저장분(346-4)
|
||||
if (isConfirm) {
|
||||
|
||||
@@ -255,17 +255,22 @@ function uphillOverridesOf(
|
||||
* 횡단을 맞춤) · 비정규 목록 = 빈 목록(서버가 정본 관 · 구조물에서 셈) · 상단측 = 세션. 종단 보기를 열었든
|
||||
* 안 열었든 같은 요청(기본 창구 · 종단 보기 몫 둘 다 이것). */
|
||||
export async function flushRouteParts(projectId: string): Promise<void> {
|
||||
await saveRouteParts(projectId, await routePartsBody(projectId));
|
||||
}
|
||||
|
||||
/** 상단측 차례 본문 — 노선 캐시 지표 조건 + 확정 지표면 id + 세션 상단측. */
|
||||
export async function routePartsBody(projectId: string): Promise<RouteConfirmRequest> {
|
||||
const latest =
|
||||
readRouteLatestCache(projectId) ?? (await fetchLatestRoute(projectId).catch(() => null));
|
||||
const surface = await fetchConfirmedSurface(projectId).catch(() => null);
|
||||
await saveRouteParts(projectId, {
|
||||
return {
|
||||
filter_key: latest?.surface_params?.source_filter,
|
||||
method: latest?.surface_params?.method,
|
||||
smooth: latest?.surface_params?.smooth,
|
||||
surface_model_id: surface?.model_id ?? undefined,
|
||||
irregular_stations: [],
|
||||
uphill_overrides: uphillOverridesOf(projectId),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/** [저장] 차례의 확정(단계 전이 없음) — 하위 차례가 실패하면 던짐(443 · 저장 한 줄기가 그 차례에서 멈춤 ·
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
type PageSettingsBody,
|
||||
} from "./B05_Profile_Api_Fetch";
|
||||
import type { RoutePanelValues } from "./B05_Profile_UI_Panel_Fields";
|
||||
import type { SavePart } from "../A00_Common/design/design_save_api";
|
||||
|
||||
export type PageSettingsDraft = PageSettingsBody;
|
||||
|
||||
@@ -68,9 +69,20 @@ onPageHistory({
|
||||
/** [저장] `settings` 차례 — 초안이 없으면 안 보냄 · 성공하면 최신 응답 캐시에 얹고 초안을 비움
|
||||
* (실패 = 던짐 · 초안 그대로 다음 [저장]에서 다시). */
|
||||
export async function flushPageSettings(projectId: string): Promise<void> {
|
||||
const part = pageSettingsPart(projectId);
|
||||
if (!part) return;
|
||||
await savePageSettings(projectId, part.body as PageSettingsDraft);
|
||||
part.after?.(undefined);
|
||||
}
|
||||
|
||||
/** 한 본문 [저장] 조각(472-6) — 초안이 없으면 null · 저장 뒤 최신 응답 캐시에 얹고 초안 비움. */
|
||||
export function pageSettingsPart(projectId: string): SavePart | null {
|
||||
const draft = readPageSettings(projectId);
|
||||
if (!draft) return;
|
||||
await savePageSettings(projectId, draft);
|
||||
if (!draft) return null;
|
||||
return { body: draft, after: () => adoptPageSettings(projectId, draft) };
|
||||
}
|
||||
|
||||
function adoptPageSettings(projectId: string, draft: PageSettingsDraft): void {
|
||||
const latest = readRouteLatestCache(projectId);
|
||||
if (latest?.route_params) {
|
||||
const { design_speed_kph, terrain_type, ...top } = draft;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import { useDesignStore } from "../A00_Common/design/design_store";
|
||||
import { saveDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import type { DetailPipeInput } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import type { SavePart } from "../A00_Common/design/design_save_api";
|
||||
|
||||
/** 세션에 담는 관 한 벌 — 정본 PUT 이 그대로 받는 꼴이다. */
|
||||
export type PendingPipes = DetailPipeInput[];
|
||||
@@ -35,3 +36,14 @@ export async function flushPendingPipes(projectId: string, confirm = false): Pro
|
||||
await saveDetailPipePoints(projectId, pending, confirm);
|
||||
writePendingPipes(projectId, null);
|
||||
}
|
||||
|
||||
/** 한 본문 [저장] 조각(472-6) — 초안 = `{points}` · 초안 없는 [확정] = 빈 본문(서버 저장분으로 다시 셈) ·
|
||||
* 초안 없는 [저장] = null. 저장 뒤 초안 비움. */
|
||||
export function pipesPart(projectId: string, confirm: boolean): SavePart | null {
|
||||
const pending = readPendingPipes(projectId);
|
||||
if (!pending && !confirm) return null;
|
||||
return {
|
||||
body: pending ? { points: pending } : {},
|
||||
after: () => writePendingPipes(projectId, null),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { API_BASE_URL, API_TIMEOUT_MS } from "../config/config_frontend";
|
||||
import { draftProjectId, useDesignStore } from "../A00_Common/design/design_store";
|
||||
import { onPageHistory } from "./B05_Profile_UI_Profile_History";
|
||||
import type { SavePart } from "../A00_Common/design/design_save_api";
|
||||
|
||||
const saved = new Map<string, number[]>();
|
||||
const loaded = new Map<string, Promise<void>>();
|
||||
@@ -126,6 +127,16 @@ export function followPlusDraft(projectId: string, next: FollowedDraft | null |
|
||||
/** [저장] 차례 — 사용자 초안이 있으면 정본에 씀 · 없으면(follow 만 쓴 초안 포함 · 459) 저장분만 다시 받음
|
||||
* (노선 저장이 좌표로 옮겼을 수 있음). */
|
||||
export async function flushPlusStations(projectId: string): Promise<void> {
|
||||
const part = plusStationsPart(projectId);
|
||||
const list = part.body
|
||||
? await request(projectId, { method: "PUT", body: JSON.stringify(part.body) })
|
||||
: await request(projectId);
|
||||
await part.after?.({ chainages: list });
|
||||
}
|
||||
|
||||
/** 한 본문 [저장] 조각(472-6) — 사용자 초안이 있으면 `{chainages}` · 없으면(follow 만 쓴 초안 포함 · 459) null ·
|
||||
* 저장 뒤 저장분을 받은 목록(없으면 다시 받음)으로 · 초안 비움. */
|
||||
export function plusStationsPart(projectId: string): SavePart {
|
||||
const auto = followedOnly.get(projectId);
|
||||
followedOnly.delete(projectId);
|
||||
beforeRoute.delete(projectId);
|
||||
@@ -133,11 +144,15 @@ export async function flushPlusStations(projectId: string): Promise<void> {
|
||||
if (kept && auto === JSON.stringify(kept))
|
||||
useDesignStore(projectId, null).set("plusStations", null);
|
||||
const draft = readPlusDraft(projectId);
|
||||
const list = draft
|
||||
? await request(projectId, { method: "PUT", body: JSON.stringify({ chainages: draft }) })
|
||||
: await request(projectId);
|
||||
saved.set(projectId, list);
|
||||
loaded.set(projectId, Promise.resolve());
|
||||
if (draft) useDesignStore(projectId, null).set("plusStations", null);
|
||||
else notify(projectId);
|
||||
return {
|
||||
body: draft ? { chainages: draft } : null,
|
||||
after: async (response) => {
|
||||
const given = response?.chainages;
|
||||
const list = Array.isArray(given) ? given.map(Number) : await request(projectId);
|
||||
saved.set(projectId, list);
|
||||
loaded.set(projectId, Promise.resolve());
|
||||
if (draft) useDesignStore(projectId, null).set("plusStations", null);
|
||||
else notify(projectId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
type StructureDefinition,
|
||||
withDefinition,
|
||||
} from "./logic/B05_Profile_UI_Structures_Define";
|
||||
import type { SavePart } from "../A00_Common/design/design_save_api";
|
||||
|
||||
/** 배치형태 — 점형(측점 1개) / 구간형(시~종점) / 부지형(위치+면적). */
|
||||
export type StructurePlacement = "point" | "interval" | "site";
|
||||
@@ -393,3 +394,18 @@ export async function flushPendingStructures(projectId: string): Promise<void> {
|
||||
await saveStructures(projectId, stored.revision, pending ?? stored.structures); // 고침 없음 = 판 도장만
|
||||
writePendingStructures(projectId, null);
|
||||
}
|
||||
|
||||
/** 한 본문 [저장] 조각(472-6) — 초안(없고 정의 판이 낡았으면 저장분 = 판 도장) · 판번호는 지금 서버 값 ·
|
||||
* 초안도 낡음도 없으면 null · 저장 뒤 초안 비움. `list` = 보기가 쥔 목록(초안 없을 때 · 구조물 보기). */
|
||||
export async function structuresPart(
|
||||
projectId: string,
|
||||
list: StructureInstance[] | null = null,
|
||||
): Promise<SavePart | null> {
|
||||
const pending = readPendingStructures(projectId);
|
||||
if (!pending && !list && !definitionStale(projectId)) return null;
|
||||
const stored = await fetchStructures(projectId);
|
||||
return {
|
||||
body: { base_revision: stored.revision, structures: pending ?? list ?? stored.structures },
|
||||
after: () => writePendingStructures(projectId, null),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* 조작은 [저장]·[확정]에서만 정본으로 나간다). [초기화]는 한 벌(`design_reset`)이 한다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { flushRouteParts, type RouteLatestResponse } from "./B05_Profile_Api_Fetch";
|
||||
import type { RouteLatestResponse } from "./B05_Profile_Api_Fetch";
|
||||
import type { DesignViewSave } from "../A00_Common/design/design_shared";
|
||||
import { pageRouteId } from "../A00_Common/design/design_page_route";
|
||||
import type { createRoutePanel } from "./B05_Profile_UI_Panel";
|
||||
@@ -41,16 +41,18 @@ export function profileSaveParts(ctx: PageActionContext): DesignViewSave {
|
||||
const projectId = ctx.projectId;
|
||||
return {
|
||||
routeId: () => (ctx.routeReady() ? pageRouteId(projectId) : null), // 노선 id 한 칸(386-2b)
|
||||
steps: {
|
||||
// 구조물 — 저장 뒤 서버 목록(새 식별자)으로 화면을 맞춘다. 실패는 초안을 남기고 던져
|
||||
// 저장 한 줄기가 이 차례에서 멈춘다(443).
|
||||
structures: () => ctx.bridge().saveStructuresIfDirty(),
|
||||
// 상단측 + 비정규 측점 횡단 맞춤 — 기본 창구와 같은 본문(463 · 종단 보기를 안 열어도 같은 요청).
|
||||
// 하위 실패 = 던짐 · 성공 = 상단측 세션 비움(443) · 메모리 값도 비움(다시 안 보냄).
|
||||
uphill: async () => {
|
||||
await flushRouteParts(projectId);
|
||||
ctx.uphillOverrides.clear();
|
||||
},
|
||||
parts: {
|
||||
// 구조물 — 보낼 것 = 세션 초안 ?? 이 보기 목록 · 저장 뒤 서버 목록(새 식별자)으로 화면을 맞춘다(472-6).
|
||||
structures: () => ctx.bridge().structuresSavePart(),
|
||||
// 상단측 + 비정규 측점 횡단 맞춤 — 기본 조각 본문 그대로(463) · 저장 뒤 세션(기본) · 메모리 값도 비움.
|
||||
uphill: (base) =>
|
||||
base && {
|
||||
body: base.body,
|
||||
after: async (response) => {
|
||||
await base.after?.(response);
|
||||
ctx.uphillOverrides.clear();
|
||||
},
|
||||
},
|
||||
},
|
||||
// 횡단 본문은 늘 횡단 보기 몫 — [저장]이 숨긴 채라도 세움(386-6 · 옛 캐시 패치 길 지움)
|
||||
// 공유 캐시는 한 벌(`design_shared`)이 저장 끝에 한 번 비움 — 여기서는 새 자료를 다시 읽어 종단 ·
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
readPendingStructures,
|
||||
writePendingStructures,
|
||||
StructureConflictError,
|
||||
structuresPart,
|
||||
type StructureSaveResponse,
|
||||
type StructureInstance,
|
||||
type StructureType,
|
||||
} from "./B05_Profile_Api_Structures";
|
||||
@@ -30,6 +32,7 @@ import { listPick } from "../common_util/common_util_structure_catalog";
|
||||
import { pipeChainageOf } from "../A00_Common/design/design_pipe_ids";
|
||||
import { pageStructures, pageTypesChecked } from "../A00_Common/design/design_page_data";
|
||||
import type { FacilityLane } from "../A00_Common/design/design_facility_lane";
|
||||
import type { SavePart } from "../A00_Common/design/design_save_api";
|
||||
|
||||
export interface StructuresBridgeDeps {
|
||||
projectId: string;
|
||||
@@ -117,6 +120,30 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
|
||||
await run;
|
||||
}
|
||||
|
||||
/** 한 본문 [저장] 조각(472-6) — 보낼 것 = 세션 초안 ?? 이 보기 목록(`saveStructuresIfDirty` 와 같음) ·
|
||||
* 저장 뒤 판번호 · 서버 목록(새 식별자)으로 화면을 맞추고 뒤 단계 표시 실패를 알림. */
|
||||
async function structuresSavePart(): Promise<SavePart | null> {
|
||||
if (deps.isRestoring() || (readPending() === null && !definitionStale(deps.projectId)))
|
||||
return null;
|
||||
const part = await structuresPart(deps.projectId, readPending() ?? ownStructures);
|
||||
if (!part) return null;
|
||||
return {
|
||||
body: part.body,
|
||||
after: async (response) => {
|
||||
part.after?.(response);
|
||||
const saved = response as unknown as StructureSaveResponse | undefined;
|
||||
if (saved) structureRevision = saved.revision;
|
||||
await refreshStructuresFromServer();
|
||||
if (saved?.needs_downstream_invalidation && !saved.invalidated_downstream)
|
||||
showToast(
|
||||
"구조물은 저장되었지만 이후 단계(횡단·수량) 재작업 표시에 실패했습니다. " +
|
||||
"B06을 다시 실행해 주세요.",
|
||||
"error",
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 서버 정본을 다시 받아 화면(사이드 목록·그래프 마크)을 그 상태로 맞춘다. */
|
||||
async function refreshStructuresFromServer(): Promise<boolean> {
|
||||
const stored = await fetchStructures(deps.projectId).catch(() => null);
|
||||
@@ -262,6 +289,7 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
|
||||
deps.panel().structures.setStructures(ownStructures);
|
||||
},
|
||||
saveStructuresIfDirty,
|
||||
structuresSavePart,
|
||||
/** 서버 정본을 다시 받아 화면을 그 상태로 맞춘다. */
|
||||
refreshFromServer: refreshStructuresFromServer,
|
||||
/** 진입·새로고침 — 타입 레지스트리와 구조물 정본을 받아 화면을 채운다. */
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
type StateName,
|
||||
} from "../A00_Common/b_page_state";
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
import type { SavePart } from "../A00_Common/design/design_save_api";
|
||||
|
||||
/** 측점 하나에 얹을 옵션 조각 — 키는 `pipe_points` 옵션 키 그대로.
|
||||
* 값은 수치가 대부분이나 날개벽 설치("있음"/"없음")처럼 문자열도 있다. */
|
||||
@@ -92,6 +93,14 @@ export async function flushCulvertOptions(
|
||||
writePending(sessionKey, {});
|
||||
}
|
||||
|
||||
/** 한 본문 [저장] 조각(472-6) — 세션 조각 {누가거리 0.01 글: 패치}(서버가 저장된 관 위에 얹음) · 없으면 null ·
|
||||
* 저장 뒤 세션 비움. */
|
||||
export function culvertOptionsPart(sessionKey: string | null): SavePart | null {
|
||||
const pending = readPending(sessionKey);
|
||||
if (!Object.keys(pending).length) return null;
|
||||
return { body: pending, after: () => writePending(sessionKey, {}) };
|
||||
}
|
||||
|
||||
/** 구조물(관 · 박스 · 세월교) 조정값 열쇠 — 종류를 바꾸면 버리는 몫 · 서버 짝. */
|
||||
export const STRUCTURE_ADJUST_KEYS = [
|
||||
"inlet_structure",
|
||||
|
||||
@@ -121,10 +121,7 @@ export async function confirmSections(
|
||||
/** 프론트에서 계산한 유토곡선 결과. 확정 시점에만 영구 저장한다. */
|
||||
massHaul?: Record<string, unknown>,
|
||||
): Promise<SectionConfirmResponse> {
|
||||
const body: Record<string, unknown> = {};
|
||||
if (standardCrossSection) body.standard_cross_section = standardCrossSection;
|
||||
if (crossPatches?.length) body.cross_patches = crossPatches;
|
||||
if (massHaul) body.mass_haul = massHaul;
|
||||
const body = sectionConfirmBody(standardCrossSection, crossPatches, massHaul);
|
||||
return warnUnrecomputed(
|
||||
requestJson<SectionConfirmResponse>(`/projects/${projectId}/sections/${routeId}/confirm`, {
|
||||
method: "POST",
|
||||
@@ -139,6 +136,25 @@ async function warnUnrecomputed(
|
||||
request: Promise<SectionConfirmResponse>,
|
||||
): Promise<SectionConfirmResponse> {
|
||||
const result = await request;
|
||||
warnSectionSave(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 횡단 저장 본문(저장 · 확정 · 한 본문 [저장] 조각 공통). */
|
||||
export function sectionConfirmBody(
|
||||
standardCrossSection?: StandardCrossSection,
|
||||
crossPatches?: CrossSectionPatch[],
|
||||
massHaul?: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const body: Record<string, unknown> = {};
|
||||
if (standardCrossSection) body.standard_cross_section = standardCrossSection;
|
||||
if (crossPatches?.length) body.cross_patches = crossPatches;
|
||||
if (massHaul) body.mass_haul = massHaul;
|
||||
return body;
|
||||
}
|
||||
|
||||
/** 횡단 저장 응답의 경고 — 재계산 실패 · 서버가 버린 고침 · 측점 맞춤 실패(한 본문 [저장]도 부름). */
|
||||
export function warnSectionSave(result: Partial<SectionConfirmResponse>): void {
|
||||
const dropped = [
|
||||
...new Set([
|
||||
...(result.dropped_patch_chainages ?? []),
|
||||
@@ -163,7 +179,6 @@ async function warnUnrecomputed(
|
||||
if (result.recomputed === false) showToast(L("B06_Recompute_Failed"), "warning", 8000);
|
||||
if (lost.length) showToast(lost.join(" "), "warning", 8000);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,10 +192,7 @@ export async function saveSections(
|
||||
crossPatches?: CrossSectionPatch[],
|
||||
massHaul?: Record<string, unknown>,
|
||||
): Promise<SectionConfirmResponse> {
|
||||
const body: Record<string, unknown> = {};
|
||||
if (standardCrossSection) body.standard_cross_section = standardCrossSection;
|
||||
if (crossPatches?.length) body.cross_patches = crossPatches;
|
||||
if (massHaul) body.mass_haul = massHaul;
|
||||
const body = sectionConfirmBody(standardCrossSection, crossPatches, massHaul);
|
||||
return warnUnrecomputed(
|
||||
requestJson<SectionConfirmResponse>(`/projects/${projectId}/sections/${routeId}/save`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
confirmSections,
|
||||
saveSections,
|
||||
sectionConfirmBody,
|
||||
warnSectionSave,
|
||||
type CrossSectionPatch,
|
||||
type SectionContextResponse,
|
||||
type SectionDetailResponse,
|
||||
@@ -411,23 +411,27 @@ export function crossSaveParts(ctx: SectionPersistContext): DesignViewSave {
|
||||
// 안 만진 측점이 옛 면적으로 실려 나간다(2026-09-09 실측).
|
||||
prepare: () => ctx.reconcileDesigns(),
|
||||
// 조정창 구간값은 조정창 저장기가 낸다 — 실패하면 알리고 저장을 멈춘다(예전 그대로).
|
||||
steps: { culvertOptions: () => ctx.flushCulvertOptions() },
|
||||
cross: async (confirm) => {
|
||||
// 관 옵션(조정창 구간값) = 기본 조각(세션 `culvertopt`) — 서버가 저장된 관 위에 얹음(472-6)
|
||||
crossPart: async () => {
|
||||
const routeId = ctx.routeId();
|
||||
if (routeId === null || !ctx.projectId) return;
|
||||
if (routeId === null || !ctx.projectId) return null;
|
||||
const projectId = ctx.projectId;
|
||||
const edits = collectSectionEdits(ctx);
|
||||
// 바탕 = 캐시 설계의 사용자 칸(측점마다 통째 · 옮긴 측점은 지반 등 입력까지) + 세션 고침(445 H2 · H3 · M2)
|
||||
// 가게는 늦게 불러옴(가게 → 다시 셈 → 이 파일 순환 없음)
|
||||
const { cachedCrossPatches } = await import("./B06_Section_Section_Store");
|
||||
const patches = cachedCrossPatches(ctx.projectId, routeId, edits.crossPatches);
|
||||
await (confirm ? confirmSections : saveSections)(
|
||||
ctx.projectId,
|
||||
routeId,
|
||||
ctx.standardValues(),
|
||||
patches.length ? patches : undefined,
|
||||
edits.massHaul,
|
||||
);
|
||||
clearState("kindreset", ctx.projectId); // 비울 자리를 보냈음(318-4)
|
||||
const patches = cachedCrossPatches(projectId, routeId, edits.crossPatches);
|
||||
return {
|
||||
body: sectionConfirmBody(
|
||||
ctx.standardValues(),
|
||||
patches.length ? patches : undefined,
|
||||
edits.massHaul,
|
||||
),
|
||||
after: (response) => {
|
||||
clearState("kindreset", projectId); // 비울 자리를 보냈음(318-4)
|
||||
if (response) warnSectionSave(response); // 버린 고침 · 측점 맞춤(445 M5)
|
||||
},
|
||||
};
|
||||
},
|
||||
done: async () => {
|
||||
// 저장으로 구조물 초안이 정본이 됐다 — 화면이 들고 있던 상세는 초안 기준으로 벽을
|
||||
|
||||
@@ -19,12 +19,15 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "docs/raw/
|
||||
| 새로고침 | 세션 남음 · 메모리 전부 잃음 | 세션 상세로 먼저 섬 → 서버 정본과 비교(`checkSessionCanon`) → 다르면 갈아씀 · 노선 초안 있으면 hold · 관 · 구조물 · 추가 측점 초안 있으면 측점 냄 · 없으면 조용히 열고 가게 측점 = 상세 측점 | 상세 GET(정본 확인) | — | — | `B06_Section_Section_Store.ts:117-135` · `design_pipeline_page.ts:528-545` · `design_pipeline_page_publish.ts` opened · shown |
|
||||
| 떠남 [취소] | 없음 — 페이지 그대로 | — | — | — | — | `design_leave.ts:95-123` |
|
||||
| 떠남 [폐기] | 세션 고침 칸 지움(모든 노선 · `NOT_EDITS` · B08 · B09 칸 뺌) → 떠남 = dispose | dispose = 묶음 · 시설 줄 · 페이지 자료(`forgetPageData`) · 상세 메모리(`forgetSectionMemory`) 버림 · 다시 열면 새로고침 길 | — | — | — | `design_leave.ts:27-42 · 85-87` · `design_page.ts:286-295` · `B06_Section_Section_Store.ts:424-428` |
|
||||
| [저장] | — | 앞 단계 `prepare` = 전 측점 강제 다시 셈(서버가 면적 버림) · 화면 잠금 | 아래 차례 표 · 서버 정본 다시 셈 | 끝 = `invalidateSectionDetail` · 두 보기가 새 상세 한 객체 받음 · 노선 바뀜 = 페이지 다시 세움 | — | `design_shared.ts:395-448` · `design_save.ts:237-324` |
|
||||
| [저장] | — | 앞 단계 `prepare` = 전 측점 강제 다시 셈(서버가 면적 버림) · 화면 잠금 | 노선 안 바뀜 = 조각 한 본문 `POST /design/save`(서버가 아래 차례 · 전부 아니면 없음) · 노선 바뀜 = 아래 차례 표(옛 길) · 서버 정본 다시 셈 | 끝 = `invalidateSectionDetail` · 두 보기가 새 상세 한 객체 받음 · 노선 바뀜 = 페이지 다시 세움 | — | `design_shared.ts:395-448` · `design_save.ts:237-324` |
|
||||
| [확정] | — | [저장]과 같음 | [저장] + 빈 측점 기본 설계 · 단계 완료 · 측구 쪽을 B05 종단 파일로 되씀 | 끝 = `invalidateSectionDetail` → B07 로 넘어감 · 노선이 바뀐 [확정] = 노선만 저장 · 다시 [확정] 알림 | — | `design_shared.ts:426-432` · `design_save.ts:303-310` · `B06_Section_Router_Confirm.py:368-496` |
|
||||
| [초기화] | — | 상세 비움 · 새로고침(메모리 전부 버림) | `/route/reset` | DB 노선 행 지우고 초기값으로 · 파일 트리 복원 · 세션 초안 전부 지움(B08 · B09 뺌) | — | `design_reset.ts:30-62` · `B05_Profile_Router_Lifecycle.py:234-390` · `common_util_initial_snapshot.py:38-54 · 266-327` |
|
||||
|
||||
## [저장] 차례(`DESIGN_SAVE_ORDER` `design_save.ts:58-68`)
|
||||
|
||||
- 노선 안 바뀐 [저장] · [확정](472-6 6-A) — 브라우저가 차례마다 조각(본문 · 저장 뒤 정리)을 모아 한 요청 (`design_save.ts` saveOneBody · `design_save_api.ts`) · 서버 `B06_Section_Router_DesignSave.py` 가 같은 차례로 아래 창구 함수를 부름 · 하나라도 실패 = DB 행 · storage 되돌림(`common_util_save_guard.py`) · 차례 이름째 알림 · 같은 프로젝트 저장 겹침 = 뒤 요청 기다림.
|
||||
- 노선 바뀐 [저장] = 아래 표 차례마다 요청(6-B 전).
|
||||
|
||||
| 차례 | 기본 창구 | 서버 | 노선 바뀐 [저장] | 위치(파일:줄) |
|
||||
|---|---|---|---|---|
|
||||
| route | `pendingRouteSave` → `saveRouteDraft` | `/route/replan` · 관 · 구조물 따라감 · 노선 밖 알림 | 새 노선 id · 관 · 구조물 초안 다시 얹음 | `design_save.ts:255-274` |
|
||||
|
||||
@@ -49,7 +49,7 @@ source: ["CLAUDE.md §5", "docs/raw/조사/2026-10-19_465_요구272_값안변함
|
||||
| 내용 | 자리(파일:줄) | 상태 |
|
||||
|---|---|---|
|
||||
| 저장 밖 정본 쓰기 셋 — 넓힘 `/regenerate` · [측점 만들기] · 추천 [적용] | `B06_Section_Router.py:378-388` · `B06_Section_Router_Stations.py:236-296` · `B05_Profile_UI_RouteEdit_Apply.ts:220-245` | 사용자 확정 · 확인창 · 472-6 협의 |
|
||||
| [저장] 9 차례 · 차례마다 따로 커밋 — 실패 = 앞 차례만 저장 | `design_save.ts:253-299` | 472-6 |
|
||||
| 노선 바뀐 [저장]만 9 차례 · 차례마다 따로 커밋 — 실패 = 앞 차례만 저장(노선 안 바뀐 저장 = 한 본문 · 전부 아니면 없음) | `design_save.ts:253-299` · `B06_Section_Router_DesignSave.py` | 472-6 6-B |
|
||||
| `pipe_points.json` 한 [저장]에 두 번 PUT(관 · 관 옵션) | `B06_Section_Api_Culvert_Options.ts:67-95` | 코드 확인 |
|
||||
| 노선 바뀐 [저장]의 관 옵션 차례 = 옛 노선 열쇠 | `design_save.ts:187-188` | 추정 |
|
||||
| ↶ ↷ 가 세션을 직접 되씀 — 관 · 구조물 · 추가 측점은 되쓴 뒤 가게 듣는 쪽을 부름(`announceDesignStore` · 472-5) · B06 조정 메모리 맵 · B05 상단측 맵은 아직 옛 값일 수 있음 | `B05_Profile_UI_Profile_History.ts` apply · `design_store.ts` announceDesignStore | 관 · 구조물 = `test_472_5_shop_owner` · 나머지 추정 |
|
||||
|
||||
@@ -31,6 +31,7 @@ globalThis.window = {
|
||||
const REAL = new Set(
|
||||
[
|
||||
"A00_Common/design/design_save.ts",
|
||||
"A00_Common/design/design_save_api.ts",
|
||||
"A00_Common/design/design_page_route.ts",
|
||||
"A00_Common/b_page_state.ts",
|
||||
"B05_Profile/B05_Profile_Api_Fetch.ts",
|
||||
@@ -86,6 +87,7 @@ globalThis.fetch = async (url, init) => {
|
||||
if (url.includes("/surface/confirmed")) reply = { model_id: 7 };
|
||||
else if (url.includes("/route/latest")) reply = LATEST;
|
||||
else if (url.includes("/route/confirm")) reply = { failed_parts: [] };
|
||||
else if (url.includes("/design/save")) reply = { status: "success", steps: { uphill: {} } };
|
||||
return { ok: true, status: 200, json: async () => reply };
|
||||
};
|
||||
|
||||
@@ -96,14 +98,14 @@ function seed(withLatest) {
|
||||
state.writeState("uphill", { "40.000": "right" }, P);
|
||||
}
|
||||
|
||||
async function run(steps) {
|
||||
async function run(parts) {
|
||||
lines = [];
|
||||
const failed = [];
|
||||
const ok = await save.saveDesign({
|
||||
projectId: P,
|
||||
routeId: () => 3,
|
||||
steps,
|
||||
cross: async () => undefined,
|
||||
parts,
|
||||
crossPart: async () => null,
|
||||
failed: (error) => failed.push(String(error?.message ?? error)),
|
||||
});
|
||||
return { ok, failed, lines, uphillAfter: state.readState("uphill", P) };
|
||||
@@ -125,10 +127,10 @@ async function run(steps) {
|
||||
latest: () => LATEST,
|
||||
confirmedSurface: () => ({ model_id: 7 }),
|
||||
routeReady: () => true,
|
||||
bridge: () => ({ saveStructuresIfDirty: async () => undefined }),
|
||||
bridge: () => ({ structuresSavePart: async () => null }),
|
||||
uphillOverrides: memory,
|
||||
});
|
||||
out.open = await run({ uphill: parts.steps.uphill });
|
||||
out.open = await run({ uphill: parts.parts.uphill });
|
||||
out.openMemoryAfter = memory.size;
|
||||
|
||||
// ③ 새로고침해 B06 로 바로 — 노선 캐시 없음 = 서버에서 한 번 읽고 같은 본문
|
||||
|
||||
@@ -41,6 +41,28 @@ const STUBS = {
|
||||
fetchSectionDetail: async () => (calls.push(`fetch:${server}`), detail()),
|
||||
saveSections: async () => calls.push("cross:save"),
|
||||
confirmSections: async () => calls.push("cross:confirm"),
|
||||
sectionConfirmBody: () => ({}),
|
||||
warnSectionSave: () => undefined,
|
||||
},
|
||||
};
|
||||
// 472-6 한 본문 창구 — 서버가 차례대로 정본을 쓰는 동안 가게가 바뀜(바뀜 신호 = 저장 도중) → 횡단 저장
|
||||
let duringSave = async () => undefined;
|
||||
STUBS[path.join(ROOT, "A00_Common", "design", "design_save_api.ts")] = {
|
||||
SAVE_PART_KEYS: [
|
||||
"settings",
|
||||
"pipes",
|
||||
"structures",
|
||||
"plusStations",
|
||||
"alignment",
|
||||
"culvertOptions",
|
||||
"uphill",
|
||||
"cross",
|
||||
],
|
||||
DesignSaveError: class extends Error {},
|
||||
postDesignSave: async (_p, _r, confirm, parts) => {
|
||||
await duringSave();
|
||||
if (parts.cross?.body) calls.push(confirm ? "cross:confirm" : "cross:save");
|
||||
return { waited: false, steps: {} };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -74,7 +96,11 @@ STUBS["@util/common_util_station_key"] = loadTs(
|
||||
);
|
||||
// 상단측 차례 본문(463 · 지표 조건 · 확정 지표면 GET · 확정 POST) — 이 시험은 횡단 뒤처리만 봄
|
||||
const B05_FETCH = path.join(ROOT, "B05_Profile", "B05_Profile_Api_Fetch.ts");
|
||||
STUBS[B05_FETCH] = { ...loadTs(B05_FETCH), flushRouteParts: async () => undefined };
|
||||
STUBS[B05_FETCH] = {
|
||||
...loadTs(B05_FETCH),
|
||||
flushRouteParts: async () => undefined,
|
||||
routePartsBody: async () => ({}),
|
||||
};
|
||||
const { saveDesign, savingDesign } = loadTs(
|
||||
path.join(ROOT, "A00_Common", "design", "design_save.ts"),
|
||||
);
|
||||
@@ -117,21 +143,22 @@ const { invalidateSectionDetail, loadSectionDetail } = loadTs(
|
||||
const parts = crossSaveParts(ctx);
|
||||
calls.length = 0;
|
||||
const savingSeen = [];
|
||||
// 관 · 구조물 초안이 정본으로 나가며 가게가 바뀜 → 바뀜 신호(저장 도중)
|
||||
duringSave = async () => {
|
||||
server = "새 자리 4+8 · 47+0";
|
||||
savingSeen.push(savingDesign(P));
|
||||
await refresh();
|
||||
};
|
||||
const ok = await saveDesign({
|
||||
projectId: P,
|
||||
routeId: () => R,
|
||||
steps: {
|
||||
// 관 · 구조물 초안이 정본으로 나가며 가게가 바뀜 → 바뀜 신호(저장 도중)
|
||||
structures: async () => {
|
||||
server = "새 자리 4+8 · 47+0";
|
||||
savingSeen.push(savingDesign(P));
|
||||
await refresh();
|
||||
},
|
||||
// 추가 측점(368-2) — 초안 없음 · 이 시험은 안 봄(진짜는 서버 GET)
|
||||
plusStations: async () => undefined,
|
||||
...parts.steps,
|
||||
parts: {
|
||||
// 구조물 · 추가 측점 — 초안 없음 · 이 시험은 안 봄(진짜는 서버 GET)
|
||||
structures: () => null,
|
||||
plusStations: () => null,
|
||||
...parts.parts,
|
||||
},
|
||||
cross: (confirm) => parts.cross(confirm),
|
||||
crossPart: (confirm) => parts.crossPart(confirm),
|
||||
// 뒤처리 = 한 벌(`design_shared` saveSharedDesign done) 차례 그대로 — 공용 캐시를 한 번 비운 뒤 보기 몫(416 M1 ·
|
||||
// 횡단 보기 done 은 비우지 않고 다시 그리기만)
|
||||
done: async () => {
|
||||
|
||||
@@ -71,12 +71,17 @@ STUBS["B04_PreProcess_Api_Fetch"] = {
|
||||
};
|
||||
STUBS["B05_Profile_Api_Structures"] = {
|
||||
flushPendingStructures: async () => failing("structures"),
|
||||
structuresPart: async () => null,
|
||||
readPendingStructures: () => null, // 구조물 초안 없음 — 노선 저장 뒤 얹기(459) 안 돎
|
||||
};
|
||||
STUBS["B05_Profile_Api_PlusStations"] = { flushPlusStations: async () => undefined };
|
||||
STUBS["B05_Profile_Api_PlusStations"] = {
|
||||
flushPlusStations: async () => undefined,
|
||||
plusStationsPart: () => ({ body: null }),
|
||||
};
|
||||
STUBS["B05_Profile_Api_Fetch"] = {
|
||||
flushUphillOverrides: async () => undefined,
|
||||
flushRouteParts: async () => undefined, // 상단측 차례(463)
|
||||
routePartsBody: async () => ({}),
|
||||
saveProfileAlignment: async (_projectId, routeId, draft) => {
|
||||
calls.push(["ALIGN", routeId, draft]);
|
||||
return {};
|
||||
@@ -87,7 +92,38 @@ STUBS["B05_Profile_UI_Profile_Edit"] = {
|
||||
markAlignmentSaved: (routeId) => alignment.delete(routeId),
|
||||
writeAlignmentDraft: (routeId, edits) => alignment.set(routeId, edits),
|
||||
};
|
||||
STUBS["B06_Section_Api_Culvert_Options"] = { flushCulvertOptions: async () => undefined };
|
||||
STUBS["B06_Section_Api_Culvert_Options"] = {
|
||||
flushCulvertOptions: async () => undefined,
|
||||
culvertOptionsPart: () => null,
|
||||
};
|
||||
// 472-6 한 본문 창구 — 관 칸 · 계획고 칸만 적음(옛 PUT 줄과 같은 꼴)
|
||||
STUBS["design_save_api"] = {
|
||||
SAVE_PART_KEYS: [
|
||||
"settings",
|
||||
"pipes",
|
||||
"structures",
|
||||
"plusStations",
|
||||
"alignment",
|
||||
"culvertOptions",
|
||||
"uphill",
|
||||
"cross",
|
||||
],
|
||||
DesignSaveError: class extends Error {},
|
||||
postDesignSave: async (_projectId, routeId, confirm, parts) => {
|
||||
const pipes = parts.pipes?.body;
|
||||
calls.push([
|
||||
"SAVE",
|
||||
pipes === undefined || pipes === null ? "none" : (pipes.points ?? null),
|
||||
confirm,
|
||||
]);
|
||||
const align = parts.alignment?.body;
|
||||
if (align) {
|
||||
const { route_id: _id, ...draft } = align;
|
||||
calls.push(["ALIGN", routeId, draft]);
|
||||
}
|
||||
return { waited: false, steps: {} };
|
||||
},
|
||||
};
|
||||
STUBS["B05_Profile_UI_RouteEdit_Apply"] = {
|
||||
pendingRouteSave: async () => routeBody,
|
||||
saveRouteDraft: async () => (calls.push(["ROUTE"]), { route_id: 8 }),
|
||||
@@ -114,8 +150,16 @@ async function once({ route = false, draft = null, confirm = false, fail = null,
|
||||
projectId: P,
|
||||
routeId: () => routeId,
|
||||
// 화면 몫 = 배수판 다시 받기(PUT 없음) — 433-7 뒤 design_shared 와 같음
|
||||
steps: { pipes: async () => void calls.push(["RELOAD"]) },
|
||||
cross: async () => void calls.push(["CROSS"]),
|
||||
parts: {
|
||||
pipes: (base) => ({
|
||||
body: base?.body ?? null,
|
||||
after: async (response) => {
|
||||
await base?.after?.(response);
|
||||
calls.push(["RELOAD"]);
|
||||
},
|
||||
}),
|
||||
},
|
||||
crossPart: async () => ({ body: {} }),
|
||||
failed: (error) => (failure = String(error && error.message ? error.message : error)),
|
||||
},
|
||||
{ confirm },
|
||||
|
||||
@@ -44,6 +44,33 @@ const routeStub = {
|
||||
routeSaveSummary: (saved, outside) => (calls.push(`summary:${saved.route_id}:${outside}`), ""),
|
||||
rebuildAfterRouteSave: async () => calls.push("rebuild"),
|
||||
};
|
||||
class DesignSaveError extends Error {
|
||||
constructor(message, step, restored) {
|
||||
super(message);
|
||||
this.step = step;
|
||||
this.restored = restored;
|
||||
}
|
||||
}
|
||||
const SAVE_PART_KEYS = [
|
||||
"settings",
|
||||
"pipes",
|
||||
"structures",
|
||||
"plusStations",
|
||||
"alignment",
|
||||
"culvertOptions",
|
||||
"uphill",
|
||||
"cross",
|
||||
];
|
||||
const saveApiStub = {
|
||||
SAVE_PART_KEYS,
|
||||
DesignSaveError,
|
||||
postDesignSave: async (_p, routeId, confirm, parts) => {
|
||||
const sent = SAVE_PART_KEYS.filter((key) => parts[key] && parts[key].body != null);
|
||||
calls.push(`post:${routeId}:${confirm}:${sent.join(",")}`);
|
||||
if (failStructures) throw new DesignSaveError("structures 실패", "structures", true);
|
||||
return { waited: false, steps: { alignment: { dropped_edit_chainages: [] } } };
|
||||
},
|
||||
};
|
||||
const STUBS = {
|
||||
"../../B05_Profile/B05_Profile_UI_RouteEdit_Apply": routeStub,
|
||||
"@ui/ui_template_elements": {
|
||||
@@ -53,6 +80,7 @@ const STUBS = {
|
||||
},
|
||||
"../../B05_Profile/B05_Profile_Api_Pipes_Draft": {
|
||||
flushPendingPipes: () => record("pipes")(),
|
||||
pipesPart: () => ({ body: { points: [] }, after: () => calls.push("pipes:after") }),
|
||||
readPendingPipes: () => null, // 관 초안 없음 — 노선 저장 뒤 얹기(349-0u)는 안 돎
|
||||
writePendingPipes: () => undefined,
|
||||
},
|
||||
@@ -65,24 +93,39 @@ const STUBS = {
|
||||
},
|
||||
"../../B05_Profile/B05_Profile_Api_Structures": {
|
||||
flushPendingStructures: () => record("structures", failStructures)(),
|
||||
structuresPart: async () => ({
|
||||
body: { base_revision: 0, structures: [] },
|
||||
after: () => calls.push("structures:after"),
|
||||
}),
|
||||
readPendingStructures: () => null, // 구조물 초안 없음 — 노선 저장 뒤 얹기(459)는 안 돎
|
||||
writePendingStructures: () => undefined,
|
||||
fetchStructures: async () => ({ structures: [] }),
|
||||
},
|
||||
"../../B05_Profile/B05_Profile_Api_PlusStations": {
|
||||
flushPlusStations: () => record("plusStations")(),
|
||||
plusStationsPart: () => ({ body: null, after: () => calls.push("plusStations:after") }),
|
||||
},
|
||||
"../../B05_Profile/B05_Profile_Api_Fetch": {
|
||||
flushUphillOverrides: () => record("uphill")(),
|
||||
flushRouteParts: () => record("uphill")(), // 상단측 차례 = 종단 보기 몫과 같은 본문(463)
|
||||
routePartsBody: async () => ({ irregular_stations: [] }),
|
||||
saveProfileAlignment: async (_p, routeId, edits) =>
|
||||
calls.push(`alignment:${routeId}:${JSON.stringify(edits.station_offsets)}`),
|
||||
},
|
||||
"../../B06_Section/B06_Section_Api_Culvert_Options": {
|
||||
flushCulvertOptions: async (_p, key) => calls.push(`culvert:${key}`),
|
||||
culvertOptionsPart: (key) => ({
|
||||
body: { "10.00": {} },
|
||||
after: () => calls.push(`culvert:after:${key}`),
|
||||
}),
|
||||
},
|
||||
// 448 페이지 설정 — 초안 없음(차례만 · 내보내기는 test_442_2_page_settings)
|
||||
"../../B05_Profile/B05_Profile_Api_PageSettings": { flushPageSettings: async () => undefined },
|
||||
"../../B05_Profile/B05_Profile_Api_PageSettings": {
|
||||
flushPageSettings: async () => undefined,
|
||||
pageSettingsPart: () => null,
|
||||
},
|
||||
// 472-6 한 본문 창구 — 보낸 칸만 적음 · 구조물 실패 = 서버가 그 차례에서 되돌림
|
||||
"./design_save_api": saveApiStub,
|
||||
};
|
||||
|
||||
const cache = new Map();
|
||||
@@ -160,8 +203,19 @@ async function run() {
|
||||
projectId: "p1",
|
||||
routeId: () => 7,
|
||||
prepare: record("prepare"),
|
||||
steps: { pipes: record("pipes:screen") },
|
||||
cross: async (confirm) => calls.push(`cross:${confirm}`),
|
||||
parts: {
|
||||
pipes: (base) => ({
|
||||
body: base.body,
|
||||
after: async (response) => {
|
||||
await base.after(response);
|
||||
calls.push("pipes:screen");
|
||||
},
|
||||
}),
|
||||
},
|
||||
crossPart: async (confirm) => ({
|
||||
body: { confirm },
|
||||
after: () => calls.push(`cross:after:${confirm}`),
|
||||
}),
|
||||
done: async (confirm) => calls.push(`done:${confirm}`),
|
||||
failed: () => calls.push("failed"),
|
||||
});
|
||||
@@ -177,8 +231,7 @@ async function run() {
|
||||
{
|
||||
projectId: "p1",
|
||||
routeId: () => 7,
|
||||
steps: { uphill: record("uphill:screen") },
|
||||
cross: async (confirm) => calls.push(`cross:${confirm}`),
|
||||
crossPart: async (confirm) => ({ body: { confirm } }),
|
||||
failed: (error) => (calls.push("failed"), (out.tolerantError = error.message)),
|
||||
},
|
||||
{ confirm: true },
|
||||
|
||||
@@ -30,8 +30,11 @@ def test_save_path_and_merge_kept():
|
||||
# 저장 길(세션 → 저장 몸통)과 B06 이 부르는 종단 병합 함수는 그대로
|
||||
page = _read("A00_Common", "design", "design_view_profile.ts")
|
||||
assert "persistUphillOverrides" in page
|
||||
# 상단측 본문 = 종단 보기 몫 · 기본 창구 한 함수(463)
|
||||
assert "flushRouteParts(projectId)" in _read("B05_Profile", "B05_Profile_UI_Page_Actions.ts")
|
||||
# 상단측 본문 = 기본 조각 한 벌(463 · 472-6 종단 보기 몫은 그 본문 그대로)
|
||||
assert "body: base.body," in _read("B05_Profile", "B05_Profile_UI_Page_Actions.ts")
|
||||
assert "body: await routePartsBody(projectId)," in _read(
|
||||
"A00_Common", "design", "design_save.ts"
|
||||
)
|
||||
assert "uphill_overrides: uphillOverridesOf(projectId)," in _read(
|
||||
"B05_Profile", "B05_Profile_Api_Fetch.ts"
|
||||
)
|
||||
|
||||
@@ -41,9 +41,10 @@ def got() -> dict:
|
||||
|
||||
|
||||
def _confirm_body(lines: list[str]) -> dict:
|
||||
posts = [line for line in lines if line.startswith("POST ") and "/route/confirm" in line]
|
||||
"""상단측 차례 본문 — 472-6 부터 한 본문 창구(`/design/save`)의 `uphill` 칸."""
|
||||
posts = [line for line in lines if line.startswith("POST ") and "/design/save" in line]
|
||||
assert len(posts) == 1, lines
|
||||
return json.loads(posts[0].split(" ", 2)[2])
|
||||
return json.loads(posts[0].split(" ", 2)[2])["uphill"]
|
||||
|
||||
|
||||
def test_종단_보기를_열든_안_열든_같은_요청(got):
|
||||
@@ -79,6 +80,8 @@ def test_서버는_그_본문이면_비정규_측점_횡단을_맞춤(got):
|
||||
def test_종단_보기_몫은_기본_창구_함수를_부름():
|
||||
actions = (ROOT / "B05_Profile" / "B05_Profile_UI_Page_Actions.ts").read_text("utf-8")
|
||||
save = (ROOT / "A00_Common" / "design" / "design_save.ts").read_text("utf-8")
|
||||
assert "await flushRouteParts(projectId);" in actions
|
||||
assert "saveRouteParts(" not in actions
|
||||
# 472-6 — 종단 보기 몫은 기본 조각 본문 그대로(메모리만 더 비움) · 노선 바뀐 저장은 옛 창구
|
||||
assert "uphill: (base) =>" in actions and "body: base.body," in actions
|
||||
assert "saveRouteParts(" not in actions and "flushRouteParts" not in actions
|
||||
assert "body: await routePartsBody(projectId)," in save
|
||||
assert "return () => flushRouteParts(projectId);" in save
|
||||
|
||||
@@ -243,7 +243,7 @@ def test_wiring_sources() -> None:
|
||||
assert "forgetStationAdjusts(ctx.projectId, pipeItem.chainage_m, prev.type_id" in events[live:]
|
||||
persist = _src("B06_Section", "B06_Section_UI_Page_Persist.ts")
|
||||
assert "patchFor(at).structure_reset = true" in persist
|
||||
assert 'clearState("kindreset", ctx.projectId)' in persist
|
||||
assert 'clearState("kindreset", projectId)' in persist # 472-6 횡단 조각 저장 뒤
|
||||
store = _src("B06_Section", "B06_Section_Section_Store.ts")
|
||||
assert "crossPatchesFromCache(detail, kindChangedChainages(projectId), savedAt)" in store
|
||||
view = _src("A00_Common", "design", "design_view_cross.ts")
|
||||
|
||||
@@ -28,13 +28,14 @@ def test_저장도_횡단_보기를_세움():
|
||||
body = _save_body()
|
||||
assert 'await shared.ensureView("cross");' in body
|
||||
assert 'if (confirm) await shared.ensureView("cross")' not in body
|
||||
assert "cross: crossBody," in body
|
||||
# 472-6 — 횡단 본문 = 횡단 보기 조각(`crossPart`) · 한 본문 창구에 실림
|
||||
assert " crossPart," in body
|
||||
assert "profileSave?.cross" not in body
|
||||
|
||||
|
||||
def test_못_세우면_실패_알림():
|
||||
body = _save_body()
|
||||
assert "if (!crossBody) {" in body
|
||||
assert "if (!crossPart) {" in body
|
||||
assert '"B06_Profile_Save_Failed"' in body and '"B06_Profile_Confirm_Failed"' in body
|
||||
|
||||
|
||||
@@ -43,4 +44,4 @@ def test_종단_보기_몫엔_횡단_본문_없음():
|
||||
start = actions.index("export function profileSaveParts(")
|
||||
parts = actions[start:]
|
||||
assert "saveCachedCrossPatches" not in actions
|
||||
assert "\n cross: " not in parts
|
||||
assert "\n cross: " not in parts and "crossPart" not in parts
|
||||
|
||||
@@ -72,8 +72,8 @@ def test_확정_하위_실패면_던짐(out):
|
||||
|
||||
def test_종단_보기_상단측_차례도_같은_창구():
|
||||
actions = (ROOT / "B05_Profile" / "B05_Profile_UI_Page_Actions.ts").read_text("utf-8")
|
||||
# 기본 창구와 같은 함수(463) — 상단측은 세션에서 읽음(메모리 옛 값 안 보냄)
|
||||
assert "await flushRouteParts(projectId);" in actions
|
||||
# 기본 창구와 같은 본문(463 · 472-6 기본 조각) — 상단측은 세션에서 읽음(메모리 옛 값 안 보냄)
|
||||
assert "body: base.body," in actions and "flushRouteParts" not in actions
|
||||
fetch = (ROOT / "B05_Profile" / "B05_Profile_Api_Fetch.ts").read_text("utf-8")
|
||||
assert "uphill_overrides: uphillOverridesOf(projectId)," in fetch
|
||||
assert "confirmRoute(" not in actions
|
||||
|
||||
@@ -243,7 +243,9 @@ def test_H5_노선_저장_뒤_본문_M7_kindreset_옮김():
|
||||
for needle in ("crossDesignChoices(", "readRockBoundarySession(", "readCutSlopeSession("):
|
||||
assert needle in overlay, needle
|
||||
persist = (ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts").read_text(encoding="utf-8")
|
||||
assert "cachedCrossPatches(ctx.projectId, routeId, edits.crossPatches)" in persist
|
||||
assert (
|
||||
"cachedCrossPatches(projectId, routeId, edits.crossPatches)" in persist
|
||||
) # 472-6 횡단 조각
|
||||
keys = (ROOT / "B06_Section" / "B06_Section_Station_Keys.ts").read_text(encoding="utf-8")
|
||||
carry = keys[keys.index("export function carryChainageStates") :]
|
||||
assert 'writeState("kindreset", rekey("revetx", resets), projectId)' in carry
|
||||
|
||||
@@ -40,31 +40,32 @@ def out() -> dict:
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _puts(case: dict) -> list:
|
||||
return [call for call in case["calls"] if call[0] == "PUT"]
|
||||
def _saves(case: dict) -> list:
|
||||
"""472-6 — 노선 안 바뀐 저장 = 한 본문 창구 한 번 · ["SAVE", 관 칸(points · "none"), 확정]."""
|
||||
return [call for call in case["calls"] if call[0] == "SAVE"]
|
||||
|
||||
|
||||
def test_관_PUT_은_초안_한_번(out):
|
||||
case = out["draft"]
|
||||
assert case["ok"] and _puts(case) == [
|
||||
["PUT", [{"chainage_m": 15, "source": "user", "id": "p1"}], False]
|
||||
assert case["ok"] and _saves(case) == [
|
||||
["SAVE", [{"chainage_m": 15, "source": "user", "id": "p1"}], False]
|
||||
]
|
||||
assert case["calls"].index(["RELOAD"]) > case["calls"].index(
|
||||
_puts(case)[0]
|
||||
_saves(case)[0]
|
||||
) # 화면은 뒤에 받기만
|
||||
assert case["left"] is None
|
||||
|
||||
|
||||
def test_초안_없으면_PUT_없음(out):
|
||||
assert out["noDraft"]["ok"] and _puts(out["noDraft"]) == []
|
||||
assert out["noDraft"]["ok"] and _saves(out["noDraft"]) == [["SAVE", "none", False]]
|
||||
|
||||
|
||||
def test_확정은_칸_없이_다시_셈(out):
|
||||
assert _puts(out["confirmNoDraft"]) == [["PUT", None, True]]
|
||||
assert _saves(out["confirmNoDraft"]) == [["SAVE", None, True]]
|
||||
|
||||
|
||||
def test_빈_초안은_다_지움(out):
|
||||
assert _puts(out["emptyDraft"]) == [["PUT", [], False]]
|
||||
assert _saves(out["emptyDraft"]) == [["SAVE", [], False]]
|
||||
|
||||
|
||||
def test_노선_저장_뒤_실패_초안_새_노선(out):
|
||||
|
||||
@@ -75,10 +75,14 @@ def test_저장이_창구를_다섯_다_거친다():
|
||||
"flushPendingStructures(",
|
||||
):
|
||||
assert token in save, f"{token} 가 저장 한 줄기에서 사라짐"
|
||||
persist = (ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts").read_text(encoding="utf-8")
|
||||
assert "ctx.flushCulvertOptions()" in persist, "B06 조정창 구간값이 저장에서 빠짐"
|
||||
# 차례가 실패하면 그 차례에서 멈추고 이름째 알림(429 ⑨ · 뒤 차례가 옛 정본 위에 얹히지 않게).
|
||||
# 472-6 — 노선 안 바뀐 저장은 한 본문 조각(조정창 구간값 = 세션 `culvertopt`) · 노선 바뀐
|
||||
# 저장은 위 창구 차례 그대로
|
||||
assert 'culvertOptionsPart(stateKey("culvertopt", projectId, routeId))' in save, (
|
||||
"B06 조정창 구간값이 한 본문 저장에서 빠짐"
|
||||
)
|
||||
# 차례가 실패하면 그 차례에서 멈추고 이름째 알림(429 ⑨) · 한 본문은 서버가 차례 이름째 되돌림
|
||||
assert "throw stepFailed(step, error)" in save, "실패한 차례에서 멈춤이 빠짐"
|
||||
assert "STEP_LABEL[error.step]" in save, "한 본문 실패 차례 이름이 빠짐"
|
||||
order = save[save.index("DESIGN_SAVE_ORDER = [") :]
|
||||
order = order[: order.index("]")]
|
||||
names = ["pipes", "structures", "alignment", "culvertOptions", "uphill", "cross"]
|
||||
|
||||
@@ -32,12 +32,15 @@ def test_저장이_면적을_모으기_전에_다시_계산한다() -> None:
|
||||
text = PERSIST.read_text(encoding="utf-8")
|
||||
body = _body(text, "export function crossSaveParts")
|
||||
assert "prepare: () => ctx.reconcileDesigns()" in body, "횡단 보기 몫이 재계산을 안 넘긴다"
|
||||
assert "collectSectionEdits(ctx)" in body[body.index("cross:") :], "면적 수집이 횡단 차례 밖"
|
||||
assert "collectSectionEdits(ctx)" in body[body.index("crossPart:") :], (
|
||||
"면적 수집이 횡단 조각 밖"
|
||||
)
|
||||
shared = _body(SHARED.read_text(encoding="utf-8"), "export async function saveSharedDesign")
|
||||
assert "prepare: crossSave?.prepare" in shared, "한 벌이 재계산을 저장 한 줄기에 안 넘긴다"
|
||||
save = SAVE.read_text(encoding="utf-8")
|
||||
run = _body(save, "export async function saveDesign", 4000)
|
||||
assert run.index("ctx.prepare?.()") < run.index("ctx.cross(confirm)"), (
|
||||
# 472-6 — 노선 안 바뀐 저장 = 한 본문(`saveOneBody`) · 재계산 뒤에 횡단 조각을 모음
|
||||
run = _body(save, "async function saveOneBody", 4000)
|
||||
assert run.index("ctx.prepare?.()") < run.index("ctx.crossPart(confirm)"), (
|
||||
"저장 한 줄기가 재계산보다 면적 수집을 먼저 부른다 — 옛 면적이 실린다"
|
||||
)
|
||||
|
||||
|
||||
@@ -60,17 +60,18 @@ def test_저장_차례(out):
|
||||
"cross",
|
||||
] # 346-3 노선 먼저
|
||||
assert out["saveOk"] is True
|
||||
# 472-6 — 노선 초안 없으면 조각을 모아 한 요청(서버 한 창구 · 전부 아니면 없음) ·
|
||||
# 저장 뒤 조각 정리는 차례대로 · 본문 없는 조각(추가 측점 초안 없음)도 정리는 돎
|
||||
assert out["saveCalls"] == [
|
||||
"overlay:on",
|
||||
"prepare",
|
||||
"pipes",
|
||||
"post:7:false:pipes,structures,alignment,culvertOptions,uphill,cross",
|
||||
"pipes:after",
|
||||
"pipes:screen",
|
||||
"structures",
|
||||
"plusStations",
|
||||
'alignment:7:{"20.000":0.1}',
|
||||
"culvert:aislo:draft:culvertopt:p1:7",
|
||||
"uphill",
|
||||
"cross:false",
|
||||
"structures:after",
|
||||
"plusStations:after",
|
||||
"culvert:after:aislo:draft:culvertopt:p1:7",
|
||||
"cross:after:false",
|
||||
"done:false",
|
||||
"overlay:off",
|
||||
]
|
||||
@@ -78,10 +79,15 @@ def test_저장_차례(out):
|
||||
|
||||
|
||||
def test_구조물_실패는_멈추고_알림_노선없음(out):
|
||||
"""429 ⑨ — 차례가 실패하면 뒤 차례(상단측 · 횡단)를 안 보냄 · 차례 이름째 알림."""
|
||||
assert out["tolerant"] == ["overlay:on", "pipes", "structures", "failed", "overlay:off"]
|
||||
"""429 ⑨ · 472-6 — 서버가 구조물 차례에서 실패 · 되돌림 → 정리 · done 없음 · 차례 이름째."""
|
||||
assert out["tolerant"] == [
|
||||
"overlay:on",
|
||||
"post:7:true:pipes,structures,culvertOptions,uphill,cross",
|
||||
"failed",
|
||||
"overlay:off",
|
||||
]
|
||||
assert out["tolerantOk"] is False
|
||||
assert out["tolerantError"].startswith("구조물 차례에서 멈췄습니다")
|
||||
assert out["tolerantError"].startswith("구조물 차례에서 실패 — 아무것도 저장하지 않았습니다")
|
||||
assert "structures 실패" in out["tolerantError"] # 까닭 그대로
|
||||
assert out["noRouteSave"] is False and out["noRouteCalls"] == []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user