refactor(472 마무리): 종단도 계획고 [저장] 조각을 design_profile_save 로 떼어 냄 — design_profile 708 → 683 줄 · 동작 같음

- design_profile_save.ts 새 파일 — 한 본문 [저장] 조각 · [확정] 직전 저장 · 저장 뒤 서버 선형 넘김
- 높이 캐스케이드 설명을 B05_Profile_UI_Profile_Heights 머리로 옮김
- 팝아웃 시험 목록에 새 파일 · 저장 조각 시험(test_472_6_profile_save) 추가

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 15:33:08 +09:00
co-authored by Claude Opus 5.5
parent cfdcf0aed7
commit 8d577ef330
6 changed files with 171 additions and 48 deletions
+23 -48
View File
@@ -23,11 +23,6 @@ import {
toDesignProfile,
} from "../../B05_Profile/logic/B05_Profile_UI_Profile_Data";
import { showToast } from "@ui/ui_template_elements";
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,
@@ -88,12 +83,12 @@ import * as pagePipeline from "./design_pipeline_page";
import type { ElevationWindowResult } from "@util/common_util_chart_ywindow";
import { frameOf } from "@util/common_util_view_window";
import type { DesignSideBoard, DesignView } from "./design_profile_slots";
import { noticeDroppedEdits } from "./design_save";
import { createProfileOverlays } from "./design_profile_overlays";
import { bindProfileViewport, createSelectionReveal } from "./design_profile_viewport";
import { CROSS_PREVIEW_DEBOUNCE_MS, type DesignProfileContext } from "./design_profile_context";
import { pageRouteId } from "./design_page_route";
import { createProfileFrame } from "./design_profile_frame"; // 판 뼈대 · 종단 CSS(700줄 한계)
import { createProfileSave } from "./design_profile_save"; // 계획고 [저장] 조각(700줄 한계)
export type {
DesignOverlayBoard,
@@ -303,29 +298,26 @@ export function createDesignProfile(ctx: DesignProfileContext) {
const listed = (): AlignmentBase | null =>
listedBase(storedBase, detail, planted ? irregularStations : null);
/** 계획고 [저장] 조각(472-6) — 저장 뒤 서버 선형으로 기준을 갈아 다시 그림. */
const alignmentSave = createProfileSave({
projectId,
routeId: () => routeId,
edits: () => store.edits(),
dirty: () => store.dirty(),
previewing: () => store === preview, // 노선 초안 미리보기 편집은 안 보냄
adopt: (next) => {
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();
},
});
/** 편집 후 다시 그린다 — 길게 누르기(초당 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());
@@ -361,15 +353,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
// 노선 초안 동안은 저장 노선 횡단을 안 고침 — 새 노선 횡단은 계산 묶음 cross(346-2)
const scheduleCrossPreview = (): void => void (store !== preview && crossPreview.schedule());
/**
* 높이 캐스케이드(2026-08-05 사용자 확정): 자리가 모자라면 ① 종단이 먼저 최소까지
* 줄고 → ② 테이블·유토곡선이 **같이**(비례) 각자 최소까지 줄고 → ③ 셋 다 최소면
* 메인 패널 리사이저의 동적 하한이 더 못 내려가게 막는다. 반대로 서브패널을 갑자기
* 펼쳐 자리가 모자라면 메인 패널 높이를 키운다(deficit 처리).
*
* draw()(전체 재구성)와 리사이즈 중 경량 동기화가 **같은 계산**을 쓴다 — 끌 때는
* 이 함수만 프레임마다 돌리고, 무거운 차트·테이블 재구성은 손을 뗀 뒤 한 번만 한다.
*/
/** 높이 캐스케이드 — 규칙은 `_Profile_Heights` 머리(700줄 한계). */
const heights = createHeightCascade({
root,
body,
@@ -642,18 +626,9 @@ export function createDesignProfile(ctx: DesignProfileContext) {
drawSoon();
},
/** [확정] 직전에 호출한다. 편집이 없으면 아무 것도 하지 않는다. */
async save(): Promise<void> {
const part = alignmentSavePart();
if (!part) return;
await part.after?.(
(await saveProfileAlignment(projectId, routeId!, store.edits())) as unknown as Record<
string,
unknown
>,
);
},
save: () => alignmentSave.save(),
/** 한 본문 [저장] 조각(472-6) — 편집 없음 · 노선 초안 미리보기면 null · 저장 뒤 서버 선형으로 다시 그림. */
savePart: () => alignmentSavePart(),
savePart: () => alignmentSave.savePart(),
clear() {
detail = null;
base = null;
+52
View File
@@ -0,0 +1,52 @@
/* =============================================================================
* A00_Common/design/design_profile_save.ts
* 종단도 계획고 편집 저장 — 한 본문 [저장] 조각(472-6) · [확정] 직전 따로 저장.
* 편집 없음 · 노선 초안 미리보기면 안 보냄 · 저장 뒤 서버 선형은 `adopt` 로 종단도에 넘김.
* `design_profile.ts` 에서 떼어 냄(700줄 한계).
* ========================================================================== */
import {
saveProfileAlignment,
type ProfileAlignmentSaveResponse,
} from "../../B05_Profile/B05_Profile_Api_Fetch";
import type {
AlignmentEdits,
ProfileAlignment,
} from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
import type { SavePart } from "./design_save_api";
import { noticeDroppedEdits } from "./design_save";
export function createProfileSave(deps: {
projectId: string;
routeId: () => number | null;
edits: () => AlignmentEdits;
dirty: () => boolean;
/** 노선 초안 미리보기 편집 중 — 안 보냄. */
previewing: () => boolean;
/** 서버가 돌려준 선형(없을 수 있음) — 기준 갈아끼움 · 저장 표시 · 다시 그림. */
adopt: (saved: ProfileAlignment | undefined) => void;
}) {
/** 한 본문 [저장] 조각 — 편집 없음 · 노선 초안 미리보기면 null. */
function savePart(): SavePart | null {
const routeId = deps.routeId();
if (!routeId || !deps.dirty() || deps.previewing()) return null;
return {
body: { route_id: routeId, ...deps.edits() },
after: (response) => {
const saved = response as unknown as ProfileAlignmentSaveResponse;
noticeDroppedEdits(saved);
deps.adopt(saved.profile_alignment as ProfileAlignment | undefined);
},
};
}
/** [확정] 직전에 호출한다. 편집이 없으면 아무 것도 하지 않는다. */
async function save(): Promise<void> {
const part = savePart();
if (!part) return;
const saved = await saveProfileAlignment(deps.projectId, deps.routeId()!, deps.edits());
await part.after?.(saved as unknown as Record<string, unknown>);
}
return { savePart, save };
}