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:
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -60,6 +60,15 @@ export interface HeightCascade {
|
||||
scheduleLightSync: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 높이 캐스케이드(2026-08-05 사용자 확정): 자리가 모자라면 ① 종단이 먼저 최소까지
|
||||
* 줄고 → ② 테이블·유토곡선이 **같이**(비례) 각자 최소까지 줄고 → ③ 셋 다 최소면
|
||||
* 메인 패널 리사이저의 동적 하한이 더 못 내려가게 막는다. 반대로 서브패널을 갑자기
|
||||
* 펼쳐 자리가 모자라면 메인 패널 높이를 키운다(deficit 처리).
|
||||
*
|
||||
* 종단도 draw()(전체 재구성)와 리사이즈 중 경량 동기화가 **같은 계산**을 쓴다 — 끌 때는
|
||||
* 이 함수만 프레임마다 돌리고, 무거운 차트·테이블 재구성은 손을 뗀 뒤 한 번만 한다.
|
||||
*/
|
||||
export function createHeightCascade(deps: HeightCascadeDeps): HeightCascade {
|
||||
let lightSyncPending = false;
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/* 472 마무리 — 종단도 계획고 [저장] 조각(`design_profile_save.ts`) 진짜 본문 · 서버 창구 · 알림만 가짜.
|
||||
* 결과 = 표준출력 JSON. 짝 = `test_472_6_profile_save.py`. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const calls = [];
|
||||
const STUBS = {
|
||||
B05_Profile_Api_Fetch: {
|
||||
saveProfileAlignment: async (projectId, routeId, edits) => {
|
||||
calls.push(["put", projectId, routeId, edits]);
|
||||
return { profile_alignment: { base_pvi: [1] }, dropped_edit_chainages: [] };
|
||||
},
|
||||
},
|
||||
design_save: {
|
||||
noticeDroppedEdits: (saved) => calls.push(["notice", saved.dropped_edit_chainages]),
|
||||
},
|
||||
};
|
||||
const file = path.join(ROOT, "A00_Common", "design", "design_profile_save.ts");
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
const req = (request) => STUBS[Object.keys(STUBS).find((name) => request.endsWith(name))] ?? {};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, req);
|
||||
|
||||
(async () => {
|
||||
const state = { routeId: 7, dirty: true, previewing: false };
|
||||
const adopted = [];
|
||||
const edits = { station_offsets: { 10: 0.5 }, curve_radii: {} };
|
||||
const save = box.exports.createProfileSave({
|
||||
projectId: "p",
|
||||
routeId: () => state.routeId,
|
||||
edits: () => edits,
|
||||
dirty: () => state.dirty,
|
||||
previewing: () => state.previewing,
|
||||
adopt: (next) => adopted.push(next ?? null),
|
||||
});
|
||||
const nulls = {};
|
||||
for (const [name, patch] of [
|
||||
["clean", { dirty: false }],
|
||||
["preview", { previewing: true }],
|
||||
["noRoute", { routeId: null }],
|
||||
]) {
|
||||
Object.assign(state, { routeId: 7, dirty: true, previewing: false }, patch);
|
||||
nulls[name] = save.savePart();
|
||||
}
|
||||
Object.assign(state, { routeId: 7, dirty: true, previewing: false });
|
||||
const part = save.savePart();
|
||||
await part.after({ profile_alignment: { base_pvi: [2] }, dropped_edit_chainages: [3] });
|
||||
await save.save();
|
||||
state.dirty = false;
|
||||
await save.save(); // 편집 없음 = 안 보냄
|
||||
console.log(JSON.stringify({ nulls, body: part.body, adopted, calls }));
|
||||
})();
|
||||
@@ -22,6 +22,7 @@ ROOT = os.path.dirname(os.path.dirname(HERE))
|
||||
# 새 창으로 가는 종단 패널의 화면 코드
|
||||
POPOUT_FILES = [
|
||||
"A00_Common/design/design_profile.ts",
|
||||
"A00_Common/design/design_profile_save.ts",
|
||||
"A00_Common/design/design_profile_viewport.ts",
|
||||
"B05_Profile/B05_Profile_UI_Profile_Wheel.ts",
|
||||
"B05_Profile/B05_Profile_UI_Profile_Heights.ts",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""472 마무리 — 종단도 계획고 [저장] 조각을 `design_profile_save.ts` 로 떼어 냄(700줄 한계 · 동작 같음).
|
||||
|
||||
`helper_472_6_profile_save.cjs` 가 진짜 본문을 돌림 — 편집 없음 · 노선 초안 미리보기 · 노선 없음 = 조각 없음 ·
|
||||
조각 본문 = 노선 id + 편집 · 저장 뒤 버린 자리 알림 → 서버 선형 넘김 · [확정] 직전 저장 = 같은 창구 한 번.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_472_6_profile_save.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def test_계획고_저장_조각() -> None:
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_472_6_profile_save.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
r = json.loads(proc.stdout)
|
||||
edits = {"station_offsets": {"10": 0.5}, "curve_radii": {}}
|
||||
assert r["nulls"] == {"clean": None, "preview": None, "noRoute": None}
|
||||
assert r["body"] == {"route_id": 7, **edits}
|
||||
assert r["adopted"] == [{"base_pvi": [2]}, {"base_pvi": [1]}]
|
||||
assert r["calls"] == [["notice", [3]], ["put", "p", 7, edits], ["notice", []]]
|
||||
Reference in New Issue
Block a user