Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -6,9 +6,12 @@
|
||||
* 서버로(자동저장 없음). 서버가 stage 2 params 에 얹고 종단 경고 기준을 갈아 끼운다(기하 그대로).
|
||||
* 시작 측점은 B05 표시 전용 — 측점 열쇠 · 다른 페이지 라벨에 안 씀.
|
||||
* 시 · 종점 계획고 조정은 여기 없음 — 첫 · 끝 측점 계획고 편집(`alignment` 차례)이 정본.
|
||||
* 값 주인 = 설계 가게 `pageSettings` 칸(472-5) — 폼 · 기울기 한도 · 최소고 강제가 가게를 듣고 읽음 ·
|
||||
* ↶ ↷ 가 세션을 되쓰면 가게가 듣는 쪽을 다시 부름(N6) · 최소고 강제 사본 칸 없음(N7).
|
||||
* ========================================================================== */
|
||||
|
||||
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { draftProjectId, useDesignStore } from "../A00_Common/design/design_store";
|
||||
import { onPageHistory } from "./B05_Profile_UI_Profile_History";
|
||||
import {
|
||||
readRouteLatestCache,
|
||||
savePageSettings,
|
||||
@@ -42,10 +45,25 @@ export const panelValuesOfDraft = (draft: PageSettingsDraft): Partial<RoutePanel
|
||||
});
|
||||
|
||||
export const readPageSettings = (projectId: string): PageSettingsDraft | null =>
|
||||
readState<PageSettingsDraft>("pagesettings", projectId);
|
||||
useDesignStore(projectId, null).get("pageSettings");
|
||||
|
||||
export const writePageSettings = (projectId: string, value: PageSettingsDraft): void =>
|
||||
writeState("pagesettings", value, projectId);
|
||||
export const writePageSettings = (projectId: string, value: PageSettingsDraft | null): void =>
|
||||
useDesignStore(projectId, null).set("pageSettings", value);
|
||||
|
||||
/** 횡단배수 최소고를 편집에서 강제할지 — 초안이 있으면 초안 · 없으면 저장 노선 설정(기본 해제). */
|
||||
export function enforceMinCoverOf(projectId: string): boolean {
|
||||
const draft = readPageSettings(projectId);
|
||||
if (draft) return draft.enforce_pipe_clearance === true;
|
||||
return readRouteLatestCache(projectId)?.route_params?.enforce_pipe_clearance === true;
|
||||
}
|
||||
|
||||
// 되돌리기 · 다시하기는 세션을 바로 되씀 — 가게로 다시 알려 폼 · 기울기 한도 · 시작 측점이 따라옴(N6)
|
||||
onPageHistory({
|
||||
restore: () => {
|
||||
const projectId = draftProjectId();
|
||||
writePageSettings(projectId, readPageSettings(projectId));
|
||||
},
|
||||
});
|
||||
|
||||
/** [저장] `settings` 차례 — 초안이 없으면 안 보냄 · 성공하면 최신 응답 캐시에 얹고 초안을 비움
|
||||
* (실패 = 던짐 · 초안 그대로 다음 [저장]에서 다시). */
|
||||
@@ -63,5 +81,5 @@ export async function flushPageSettings(projectId: string): Promise<void> {
|
||||
};
|
||||
writeRouteLatestCache(projectId, latest);
|
||||
}
|
||||
clearState("pagesettings", projectId);
|
||||
writePageSettings(projectId, null);
|
||||
}
|
||||
|
||||
@@ -89,10 +89,27 @@ export function panelValuesOf(next: RouteLatestResponse): Partial<RoutePanelValu
|
||||
};
|
||||
}
|
||||
|
||||
/** 페이지 설정 한 칸(463 · 요구 271-5) — 종단도 기울기 상한 · 최소고 강제 · 시작 측점 = 세션 초안(448) ?? 저장 노선
|
||||
/** 폼의 페이지 설정 칸만 — 초안 ?? 저장 노선(빈 기준값 = 빈 칸 · 강제 = 해제) · ↶ ↷ 뒤 폼에 다시 얹음(N6 · 472-5). */
|
||||
export function pageSettingsPanelValues(projectId: string): Partial<RoutePanelValues> | null {
|
||||
const latest = readRouteLatestCache(projectId);
|
||||
const draft = readPageSettings(projectId);
|
||||
if (!latest) return draft ? panelValuesOfDraft(draft) : null;
|
||||
const all = panelValuesOf(latest);
|
||||
return {
|
||||
designSpeed: all.designSpeed,
|
||||
terrainType: all.terrainType,
|
||||
maxGradePct: all.maxGradePct ?? null,
|
||||
minVerticalRadius: all.minVerticalRadius ?? null,
|
||||
minTangentLength: all.minTangentLength ?? null,
|
||||
enforcePipeClearance: all.enforcePipeClearance === true,
|
||||
stationDisplay: all.stationDisplay ?? { station: 0, cumulative: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
/** 페이지 설정 한 칸(463 · 요구 271-5) — 종단도 기울기 상한 · 시작 측점 = 세션 초안(448) ?? 저장 노선
|
||||
* 설정(빈 칸 = 좌측 패널 기본). 어느 보기로 서도 종단도를 그린 뒤 얹음 · B05 패널은 세션에 쓰고 고친 즉시 얹음. */
|
||||
export function applyPageSettings(
|
||||
profile: Pick<DesignProfile, "setGradeLimit" | "setEnforceMinCover" | "setStationDisplay">,
|
||||
profile: Pick<DesignProfile, "setGradeLimit" | "setStationDisplay">,
|
||||
projectId: string,
|
||||
latest: RouteLatestResponse | null = readRouteLatestCache(projectId),
|
||||
): void {
|
||||
@@ -106,6 +123,5 @@ export function applyPageSettings(
|
||||
profile.setGradeLimit(
|
||||
gradeLimitOf(speed, values.terrainType ?? "special", values.maxGradePct ?? null),
|
||||
);
|
||||
profile.setEnforceMinCover(values.enforcePipeClearance === true);
|
||||
profile.setStationDisplay(values.stationDisplay ?? { station: 0, cumulative: 0 });
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export interface ProfileEditStore {
|
||||
/** 서버 저장분 곡선반경 — ▲▼ 가 자동 R 로 보고 지울 기준(433-13). */
|
||||
savedRadii(): Readonly<Record<string, number>>;
|
||||
replace(next: AlignmentEdits): void;
|
||||
/** 서버 저장이 끝났음을 표시한다. 편집 델타는 그대로 두고 초안만 지운다.
|
||||
/** 서버 저장이 끝났음을 표시한다 — 초안을 저장분으로 옮기고 초안을 지운다.
|
||||
* 이때의 값이 [초기화]의 기준점이 된다. */
|
||||
markSaved(): void;
|
||||
/** 마지막 [저장] 시점으로 되돌린다 — 그 뒤의 편집만 버린다(2026-09-03 사용자 지시). */
|
||||
@@ -87,6 +87,8 @@ export interface ProfileEditStore {
|
||||
dirty(): boolean;
|
||||
/** 자동 선형 대비 편집 델타가 하나라도 있는가. */
|
||||
edited(): boolean;
|
||||
/** ↶ ↷ 가 세션을 되쓴 뒤 — 이 창구가 마지막으로 본 편집과 다른가(다시 셈 여부). */
|
||||
resync(): boolean;
|
||||
}
|
||||
|
||||
/** 초안 읽기 · 쓰기는 설계 가게(`design_store` 의 `alignmentDraft`) 한 창구로만. */
|
||||
@@ -111,124 +113,84 @@ export function readAlignmentDraft(routeId: number | null): AlignmentEdits | nul
|
||||
* 초기화 직후에는 멀쩡하다가 새로고침 한 번에 최대 6.0m 어긋남).
|
||||
*/
|
||||
export function clearAlignmentDrafts(): void {
|
||||
shared.clear();
|
||||
// 등록부 칸(`alignment-draft`) — 노선을 안 주면 그 프로젝트의 모든 노선 칸을 쓸어 낸다.
|
||||
clearState("alignment-draft", draftProjectId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 노선마다 **한 벌**인 편집 상태 — 두 보기의 종단도(`design_profile`)가 같은 벌을
|
||||
* 쓴다(계획서 24-2a). 예전에는 두 화면이 가게를 따로 만들어 같은 초안 키를 각자 고쳤다 —
|
||||
* 한쪽이 메모리에 쥔 옛 값으로 다시 쓰면 다른 쪽 편집이 덮였다.
|
||||
* 노선마다 편집 값 주인 = 설계 가게 두 칸(472-5) — 두 보기의 종단도(`design_profile`)가 같은 칸을 읽는다.
|
||||
* `alignmentDraft` = 미저장 편집(세션 · 되돌리기가 사진 찍음) · 있으면 미저장
|
||||
* `alignmentSaved` = 서버 저장분(메모리 · 종단도가 상세를 받을 때 · [저장]이 옮김) — [초기화]의 기준점 ·
|
||||
* 측점 원복 · 전체 원복이 자동 곡선반경을 되살리는 기준(2026-08-23 · 2026-09-03 사용자 지시)
|
||||
* 지금 편집 = 초안 ?? 저장분 — 모듈 사본이 없어 ↶ ↷ 가 세션을 되써도 두 보기가 같은 값을 본다.
|
||||
*/
|
||||
interface EditState {
|
||||
current: AlignmentEdits;
|
||||
unsaved: boolean;
|
||||
/**
|
||||
* [초기화]의 기준점 — **마지막 [저장] 시점의 편집분**이다(2026-09-03 사용자 지시).
|
||||
* 처음에는 서버 저장분이고, [저장]을 누를 때마다 그때 값으로 옮겨 간다. 초기 자동
|
||||
* 선형으로 되돌리는 것(`resetAll`)과는 다르다 — 저장한 작업까지 지우지 않는다.
|
||||
*/
|
||||
savedBaseline: AlignmentEdits;
|
||||
/** 서버 저장분 — 측점 원복 · 전체 원복이 자동 곡선반경을 되살리는 기준. */
|
||||
saved: AlignmentEdits;
|
||||
}
|
||||
|
||||
const shared = new Map<number, EditState>();
|
||||
|
||||
/** 서버 저장분과 남은 초안으로 상태를 세운다(초안이 있으면 그것이 이기고 미저장). */
|
||||
function freshState(routeId: number | null, saved: AlignmentEdits): EditState {
|
||||
const draft = readAlignmentDraft(routeId);
|
||||
return { current: draft ?? saved, unsaved: draft !== null, savedBaseline: saved, saved };
|
||||
}
|
||||
const savedOf = (routeId: number | null): AlignmentEdits =>
|
||||
(routeId === null ? null : draftOf(routeId).get("alignmentSaved")) ?? emptyEdits();
|
||||
|
||||
/** [저장]이 보낼 계획고 초안 — 미저장 편집이 없으면 null. */
|
||||
export function pendingAlignment(routeId: number): AlignmentEdits | null {
|
||||
const state = shared.get(routeId);
|
||||
if (state) return state.unsaved ? state.current : null;
|
||||
return readAlignmentDraft(routeId);
|
||||
}
|
||||
|
||||
/** 초안을 다른 노선 칸에 둔다 — 노선 저장 뒤 다른 차례가 실패(433-12 · 다음 [저장]이 새 노선으로 보냄). */
|
||||
export function writeAlignmentDraft(routeId: number, edits: AlignmentEdits): void {
|
||||
draftOf(routeId).set("alignmentDraft", edits);
|
||||
const state = shared.get(routeId);
|
||||
if (state) Object.assign(state, { current: edits, unsaved: true });
|
||||
}
|
||||
|
||||
/** 계획고가 정본에 나갔다 — 그 노선 상태를 저장됨으로 두고 초안을 지운다. */
|
||||
/** 계획고가 정본에 나갔다 — 그 노선 초안을 저장분으로 옮기고 초안을 지운다. */
|
||||
export function markAlignmentSaved(routeId: number): void {
|
||||
const state = shared.get(routeId);
|
||||
if (state) {
|
||||
state.unsaved = false;
|
||||
state.savedBaseline = state.current;
|
||||
}
|
||||
draftOf(routeId).set("alignmentDraft", null);
|
||||
const store = draftOf(routeId);
|
||||
const draft = store.get("alignmentDraft");
|
||||
if (draft) store.set("alignmentSaved", draft);
|
||||
store.set("alignmentDraft", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 편집 델타 보관소.
|
||||
* 편집 창구.
|
||||
*
|
||||
* 초기값은 **서버에 저장된 편집분**이고, 세션 초안이 남아 있으면(= 확정 없이
|
||||
* 새로고침한 경우) 초안을 우선 채택하고 미저장 상태로 표시한다. routeId가 바뀌면
|
||||
* 초안 키도 바뀌어 이전 노선의 편집이 새 노선에 잘못 얹히지 않는다.
|
||||
*
|
||||
* 부를 때마다 그 노선의 한 벌을 **초안 · 저장분으로 다시 세운다**(되돌리기가 초안을 바꾼 뒤
|
||||
* 다시 읽는 길). 같은 노선이면 어느 화면이 불러도 같은 한 벌이다.
|
||||
* `saved` = 서버에 저장된 편집분(종단도가 상세를 받을 때) — 주면 그 노선 저장분 칸을 갈아 끼움 · 안 주면
|
||||
* 칸 그대로. 세션 초안이 남아 있으면(= 확정 없이 새로고침한 경우) 초안이 이기고 미저장이다. routeId가
|
||||
* 바뀌면 칸도 바뀌어 이전 노선의 편집이 새 노선에 잘못 얹히지 않는다.
|
||||
*
|
||||
* routeId가 아직 없으면 **초안을 두지 않는다**(2026-09-04). 노선 없는 키(`:none`)에
|
||||
* 쌓인 초안은 어느 노선에나 되붙어 초기화로도 떨어지지 않았다.
|
||||
*/
|
||||
export function createProfileEditStore(
|
||||
routeId: number | null,
|
||||
saved: AlignmentEdits,
|
||||
saved: AlignmentEdits | undefined,
|
||||
onChange: () => void,
|
||||
): ProfileEditStore {
|
||||
const fresh = freshState(routeId, saved);
|
||||
// 이미 선 한 벌은 **그 자리에서** 다시 세운다 — 바꿔 끼우면 붙어 있던 쪽이 옛 벌을 쥔다.
|
||||
const state = routeId === null ? fresh : Object.assign(shared.get(routeId) ?? fresh, fresh);
|
||||
if (routeId !== null) shared.set(routeId, state);
|
||||
return handleOf(routeId, state, onChange);
|
||||
}
|
||||
|
||||
function handleOf(
|
||||
routeId: number | null,
|
||||
state: EditState,
|
||||
onChange: () => void,
|
||||
): ProfileEditStore {
|
||||
const draft = routeId === null ? null : draftOf(routeId);
|
||||
|
||||
function dropDraft(): void {
|
||||
draft?.set("alignmentDraft", null);
|
||||
}
|
||||
const cells = routeId === null ? localCells(saved) : storeCells(routeId, saved);
|
||||
const editsNow = (): AlignmentEdits => cells.draft() ?? cells.saved();
|
||||
/** 이 창구가 마지막으로 본 편집 — ↶ ↷ 가 세션을 되쓴 뒤 바뀌었는지(`resync`). */
|
||||
let seen = JSON.stringify(editsNow());
|
||||
|
||||
function commit(next: AlignmentEdits): void {
|
||||
state.current = next;
|
||||
state.unsaved = true;
|
||||
draft?.set("alignmentDraft", state.current);
|
||||
seen = JSON.stringify(next);
|
||||
cells.setDraft(next);
|
||||
onChange();
|
||||
}
|
||||
|
||||
return {
|
||||
edits: () => state.current,
|
||||
savedRadii: () => state.saved.curve_radii ?? {},
|
||||
edits: editsNow,
|
||||
savedRadii: () => cells.saved().curve_radii ?? {},
|
||||
replace: commit,
|
||||
markSaved() {
|
||||
state.unsaved = false;
|
||||
state.savedBaseline = state.current;
|
||||
dropDraft();
|
||||
const draft = cells.draft();
|
||||
if (draft) cells.setSaved(draft);
|
||||
cells.setDraft(null);
|
||||
},
|
||||
restoreSaved() {
|
||||
state.current = state.savedBaseline;
|
||||
state.unsaved = false;
|
||||
dropDraft();
|
||||
seen = JSON.stringify(cells.saved());
|
||||
cells.setDraft(null);
|
||||
onChange();
|
||||
},
|
||||
canRestoreSaved: () => JSON.stringify(state.current) !== JSON.stringify(state.savedBaseline),
|
||||
canRestoreSaved: () => JSON.stringify(editsNow()) !== JSON.stringify(cells.saved()),
|
||||
resetStation(chainageM, moveTo) {
|
||||
const current = editsNow();
|
||||
const key = chainageKey(chainageM);
|
||||
const stationOffsets = { ...state.current.station_offsets };
|
||||
const curveRadii = { ...state.current.curve_radii };
|
||||
const stationOffsets = { ...current.station_offsets };
|
||||
const curveRadii = { ...current.curve_radii };
|
||||
if (moveTo !== undefined) {
|
||||
const to = chainageKey(moveTo);
|
||||
if (stationOffsets[key] !== undefined) stationOffsets[to] = stationOffsets[key];
|
||||
@@ -240,7 +202,7 @@ function handleOf(
|
||||
delete stationOffsets[key];
|
||||
// 그 측점의 곡선반경은 저장분(자동 계산이 역산해 둔 값)으로 되돌린다 — 지우면
|
||||
// 프론트가 기본 R로 다시 그려 계획고가 서버 정본과 어긋난다(2026-08-23).
|
||||
const savedRadius = state.saved.curve_radii?.[key];
|
||||
const savedRadius = cells.saved().curve_radii?.[key];
|
||||
if (savedRadius === undefined) delete curveRadii[key];
|
||||
else curveRadii[key] = savedRadius;
|
||||
commit({ station_offsets: stationOffsets, curve_radii: curveRadii });
|
||||
@@ -249,10 +211,47 @@ function handleOf(
|
||||
// 지우는 것은 **사용자 편집 측점**이다. 자동 계획선이 역산해 저장한 곡선반경
|
||||
// (`curve_radii`)까지 비우면 호가 달라져 화면 계획고가 정본과 달라진다 —
|
||||
// 세월교 지점에서 0.22m 낮게 나와 최소고 경고가 잘못 떴다(2026-08-23 진단).
|
||||
commit({ ...emptyEdits(), curve_radii: { ...(state.saved.curve_radii ?? {}) } });
|
||||
commit({ ...emptyEdits(), curve_radii: { ...(cells.saved().curve_radii ?? {}) } });
|
||||
},
|
||||
dirty: () => state.unsaved,
|
||||
edited: () => hasEdits(state.current),
|
||||
dirty: () => cells.draft() !== null,
|
||||
edited: () => hasEdits(editsNow()),
|
||||
resync() {
|
||||
const now = JSON.stringify(editsNow());
|
||||
const changed = now !== seen;
|
||||
seen = now;
|
||||
return changed;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
interface EditCells {
|
||||
draft(): AlignmentEdits | null;
|
||||
setDraft(value: AlignmentEdits | null): void;
|
||||
saved(): AlignmentEdits;
|
||||
setSaved(value: AlignmentEdits): void;
|
||||
}
|
||||
|
||||
/** 노선 칸 = 설계 가게 두 칸. */
|
||||
function storeCells(routeId: number, saved: AlignmentEdits | undefined): EditCells {
|
||||
const store = draftOf(routeId);
|
||||
if (saved) store.set("alignmentSaved", saved);
|
||||
return {
|
||||
draft: () => store.get("alignmentDraft"),
|
||||
setDraft: (value) => store.set("alignmentDraft", value),
|
||||
saved: () => savedOf(routeId),
|
||||
setSaved: (value) => store.set("alignmentSaved", value),
|
||||
};
|
||||
}
|
||||
|
||||
/** 노선이 아직 없음 — 초안을 세션에 두지 않음(창구 안 메모리만 · 2026-09-04). */
|
||||
function localCells(saved: AlignmentEdits | undefined): EditCells {
|
||||
let draft: AlignmentEdits | null = null;
|
||||
let base = saved ?? emptyEdits();
|
||||
return {
|
||||
draft: () => draft,
|
||||
setDraft: (value) => void (draft = value),
|
||||
saved: () => base,
|
||||
setSaved: (value) => void (base = value),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user