- resetDesignAction 은 design_reset.resetDesign 을 부름 · B06 [초기화]와 같은 함수 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr
185 lines
9.0 KiB
TypeScript
185 lines
9.0 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_Page_Actions.ts
|
|
* B05 페이지의 **버튼 동작** — [경로 계산]·[임시저장]·[초기화].
|
|
*
|
|
* `B05_Profile_UI_Page` 에서 떼어냈다(700줄 제한, 2026-09-02). 화면 조립·로딩 순서는
|
|
* 페이지에 남고, 여기에는 영구저장소를 건드리는 세 갈래만 둔다(CLAUDE.md 5장:
|
|
* 세션에 쌓인 조작은 [저장]·[확정]에서만 정본으로 나간다).
|
|
* ========================================================================== */
|
|
|
|
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
|
import { hideLoadingOverlay, showLoadingOverlay, showToast } from "@ui/ui_template_elements";
|
|
import { resetDesign } from "../A00_Common/design/design_reset";
|
|
import { confirmRoute, solveRoute, type RouteLatestResponse } from "./B05_Profile_Api_Fetch";
|
|
import { saveDesign } from "../A00_Common/design/design_save";
|
|
import {
|
|
invalidateSectionDetail,
|
|
saveCachedCrossPatches,
|
|
} from "../B06_Section/B06_Section_Section_Store";
|
|
import { saveCorridorIfDirty } from "./B05_Profile_UI_Corridor";
|
|
import { circlePoint, routePoint } from "./B05_Profile_UI_Page_Helpers";
|
|
import type { createRoutePanel } from "./B05_Profile_UI_Panel";
|
|
import type { createRouteProfilePanel } from "./B05_Profile_UI_Profile_Panel";
|
|
import type { createStructuresBridge } from "./B05_Profile_UI_Page_Structures";
|
|
import type { createRouteViewer } from "./B05_Profile_UI_Viewer";
|
|
|
|
function L(key: keyof typeof ui_locales): string {
|
|
return ui_locales[key][currentLanguageIndex];
|
|
}
|
|
|
|
/** 버튼 동작이 페이지에서 받아 쓰는 창구 — 값은 바뀌므로 함수로 받는다. */
|
|
export interface PageActionContext {
|
|
projectId: string;
|
|
latest: () => RouteLatestResponse | null;
|
|
/** 확정 지표면 응답 — 모델 id 는 `model_id` 다(2026-09-06 호출 정리). */
|
|
confirmedSurface: () => { model_id: number | null } | null;
|
|
routeReady: () => boolean;
|
|
viewer: () => ReturnType<typeof createRouteViewer>;
|
|
panel: () => ReturnType<typeof createRoutePanel>;
|
|
profilePanel: () => ReturnType<typeof createRouteProfilePanel>;
|
|
bridge: () => ReturnType<typeof createStructuresBridge>;
|
|
/** 상단측(측구 방향) 사용자 변경분 — 키는 누가거리 문자열. */
|
|
uphillOverrides: Map<string, "left" | "right">;
|
|
persistUphillOverrides: () => void;
|
|
loadLatest: (forceFresh?: boolean) => Promise<RouteLatestResponse>;
|
|
renderLatest: (next: RouteLatestResponse) => void;
|
|
restoreSections: (routeId: number) => Promise<void>;
|
|
}
|
|
|
|
/** [경로 계산] — 마커·패널 값으로 노선을 풀고 종횡단까지 다시 받는다. */
|
|
export async function solveRouteAction(ctx: PageActionContext): Promise<void> {
|
|
const latest = ctx.latest();
|
|
const confirmedSurface = ctx.confirmedSurface();
|
|
if (!confirmedSurface || !latest) return;
|
|
const points = ctx.viewer().markers.getPoints();
|
|
if (!points.bp || !points.ep) {
|
|
showToast("BP와 EP를 지형에 배치하세요.", "error");
|
|
return;
|
|
}
|
|
const values = ctx.panel().values();
|
|
showLoadingOverlay();
|
|
try {
|
|
const solved = await solveRoute(ctx.projectId, {
|
|
filter_key: latest.surface_params.source_filter,
|
|
method: latest.surface_params.method,
|
|
smooth: latest.surface_params.smooth,
|
|
surface_model_id: confirmedSurface.model_id ?? undefined,
|
|
algorithm: values.algorithm,
|
|
bp: routePoint(points.bp),
|
|
ep: routePoint(points.ep),
|
|
cp: points.cp.map(routePoint),
|
|
ap: points.ap.map(circlePoint),
|
|
fp: points.fp.map(circlePoint),
|
|
grade_class: values.gradeClass,
|
|
paved: values.paved,
|
|
min_curve_radius_m: values.minCurveRadius,
|
|
max_uphill_grade: values.maxUphillGrade,
|
|
max_downhill_grade: values.maxDownhillGrade,
|
|
min_uphill_grade: values.minUphillGrade,
|
|
min_downhill_grade: values.minDownhillGrade,
|
|
allow_avoid_pass_through: values.allowAvoidPassThrough,
|
|
station_interval_m: values.stationInterval,
|
|
cross_half_width_m: null,
|
|
cross_sample_interval_m: values.crossSampleInterval,
|
|
long_sample_interval_m: values.longSampleInterval,
|
|
terrain_type: values.terrainType,
|
|
design_speed_kph: values.designSpeed,
|
|
max_grade_pct: values.maxGradePct,
|
|
min_vertical_radius_m: values.minVerticalRadius,
|
|
min_tangent_length_m: values.minTangentLength,
|
|
start_elevation_offset_m: values.startElevationOffset,
|
|
end_elevation_offset_m: values.endElevationOffset,
|
|
enforce_pipe_clearance: values.enforcePipeClearance,
|
|
});
|
|
// 새 경로는 측점 구성이 달라지므로 이전 상단측 변경분을 폐기한다(자동 판정 재사용).
|
|
ctx.uphillOverrides.clear();
|
|
ctx.persistUphillOverrides();
|
|
ctx.renderLatest(await ctx.loadLatest(true));
|
|
await ctx.restoreSections(solved.route_id);
|
|
if (solved.cross_section_count === null) {
|
|
showToast("경로는 저장되었지만 종횡단 생성에 실패했습니다.", "error");
|
|
} else if (solved.grade_summary === null) {
|
|
showToast("경로·종횡단은 저장되었지만 계획선 산출에 실패했습니다.", "error");
|
|
} else {
|
|
showToast("최적 경로 계산이 완료되었습니다.", "success");
|
|
}
|
|
} catch (error) {
|
|
showToast(error instanceof Error ? error.message : "경로 계산에 실패했습니다.", "error");
|
|
} finally {
|
|
hideLoadingOverlay();
|
|
}
|
|
}
|
|
|
|
/** [임시저장] — 현재 편집을 영구저장소에 남기되 워크플로 단계·페이지는 그대로 둔다.
|
|
* 종·횡 통합 확정은 B06 [확정]이 담당한다(2026-08-08 워크플로우 재정의).
|
|
* 차례는 저장 한 줄기(`saveDesign`)가 정한다 — B06 [저장]과 같다(계획서 24-2a). */
|
|
export async function tempSaveAction(ctx: PageActionContext): Promise<void> {
|
|
if (!ctx.routeReady()) return;
|
|
const latest = ctx.latest();
|
|
const projectId = ctx.projectId;
|
|
await saveDesign({
|
|
projectId,
|
|
routeId: () => latest?.route?.id ?? null,
|
|
steps: {
|
|
// 세션 초안이 나간 뒤 화면 목록으로 한 번 더 맞춘다(시설 보관소 되붙이기 · 응답으로 화면 갱신).
|
|
pipes: async () => {
|
|
await ctx.profilePanel().drainage.savePipes();
|
|
},
|
|
// 구조물 — 저장 뒤 서버 목록(새 식별자)으로 화면을 맞춘다. 실패는 자체 토스트로 알리고
|
|
// 세션에 남겨 두므로 저장 전체를 멈추지 않는다(2026-08-29 — CLAUDE.md 5장).
|
|
structures: () => ctx.bridge().saveStructuresIfDirty(),
|
|
// 종단 계획선 편집은 화면에서만 계산해 두었으므로 저장 시점에 영속화한다(패널도 다시 그림).
|
|
alignment: () => ctx.profilePanel().save(),
|
|
// 상단측 + 비정규 측점 — 데이터로는 확정 저장하되, 단계 완료 전이는 하지 않는다.
|
|
uphill: async () => {
|
|
await confirmRoute(
|
|
projectId,
|
|
{
|
|
filter_key: latest?.surface_params.source_filter,
|
|
method: latest?.surface_params.method,
|
|
smooth: latest?.surface_params.smooth,
|
|
surface_model_id: ctx.confirmedSurface()?.model_id ?? undefined,
|
|
irregular_stations: ctx
|
|
.bridge()
|
|
.irregularStations()
|
|
.map((station) => ({
|
|
chainage_m: station.chainage_m,
|
|
structure: station.structure,
|
|
})),
|
|
// 상단측(측구 방향) 사용자 변경분 — 종단 정본에 병합되어 B06이 그대로 소비한다.
|
|
uphill_overrides: [...ctx.uphillOverrides.entries()].map(([chainage, side]) => ({
|
|
chainage_m: Number(chainage),
|
|
side,
|
|
})),
|
|
},
|
|
false,
|
|
);
|
|
},
|
|
},
|
|
// B06에서 만져 **캐시에 얹힌** 횡단 수정분을 함께 남긴다 — 안 보내면 바로 아래
|
|
// 캐시 비우기에서 사라진다. 계획선 저장 **뒤에** 보내야 사용자 수정 1세트가
|
|
// 최종본으로 얹힌다(2026-08-24 사용자 지적).
|
|
cross: async () => {
|
|
if (latest?.route?.id != null) await saveCachedCrossPatches(projectId, latest.route.id);
|
|
},
|
|
done: async () => {
|
|
// 서버가 종단 정본의 계획선을 다시 썼다 — 공유 캐시를 비워 B06이 옛 계획선을 못 보게 한다.
|
|
invalidateSectionDetail(projectId);
|
|
ctx.renderLatest(await ctx.loadLatest(true));
|
|
// 임시저장 = 코리도 영구저장 시점(2026-08-23) — 실패해도 임시저장은 성공 처리.
|
|
if (latest?.route?.id) void saveCorridorIfDirty(projectId, latest.route.id);
|
|
showToast(L("B05_Route_TempSave_Success"), "success");
|
|
},
|
|
failed: (error) =>
|
|
showToast(error instanceof Error ? error.message : L("B05_Route_TempSave_Failed"), "error"),
|
|
});
|
|
}
|
|
|
|
/** [초기화] — 두 보기가 같은 함수를 부른다(`design_reset` · 계획서 24-2b). 페이지만 아는
|
|
* 상단측 변경분만 여기서 비운다. */
|
|
export async function resetDesignAction(ctx: PageActionContext): Promise<void> {
|
|
await resetDesign(ctx.projectId, ctx.latest()?.route?.id ?? null, () =>
|
|
ctx.uphillOverrides.clear(),
|
|
);
|
|
}
|