Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1

This commit is contained in:
2026-10-19 10:52:12 +09:00
34 changed files with 1334 additions and 165 deletions
+7 -2
View File
@@ -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 · 저장 한 줄기가 그 차례에서 멈춤 ·
+14 -2
View File
@@ -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),
};
}
+22 -7
View File
@@ -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);
},
};
}
+16
View File
@@ -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),
};
}
+13 -11
View File
@@ -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,
/** 진입·새로고침 — 타입 레지스트리와 구조물 정본을 받아 화면을 채운다. */