Merge remote-tracking branch 'origin/sub_laptop_4' into sub_laptop_7
This commit is contained in:
@@ -7,9 +7,12 @@
|
||||
* 하나뿐이었다). 다른 조작값과 같은 규칙으로 세션에 쌓고 저장 앞단에서 함께 내보낸다.
|
||||
*
|
||||
* 자동저장은 하지 않는다(CLAUDE.md 5장).
|
||||
*
|
||||
* 관 목록의 주인은 설계 가게(`design_store` 의 `pipes`) 하나다(계획서 24-2a) — B05 배수유역도 ·
|
||||
* B06 구조물 목록 모두 이 창구로 읽고 쓴다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { readState, writeState } from "../A00_Common/b_page_state";
|
||||
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";
|
||||
|
||||
@@ -17,11 +20,11 @@ import type { DetailPipeInput } from "../B04_PreProcess/B04_PreProcess_Api_Fetch
|
||||
export type PendingPipes = DetailPipeInput[];
|
||||
|
||||
export function readPendingPipes(projectId: string): PendingPipes | null {
|
||||
return readState<PendingPipes>("pipes", projectId);
|
||||
return useDesignStore(projectId, null).get("pipes");
|
||||
}
|
||||
|
||||
export function writePendingPipes(projectId: string, next: PendingPipes | null): void {
|
||||
writeState("pipes", next, projectId);
|
||||
useDesignStore(projectId, null).set("pipes", next);
|
||||
}
|
||||
|
||||
/** 초안이 있으면 정본에 쓰고 비운다. 없으면 아무 일도 하지 않는다. */
|
||||
|
||||
@@ -23,9 +23,7 @@ import {
|
||||
solveRoute,
|
||||
type RouteLatestResponse,
|
||||
} from "./B05_Profile_Api_Fetch";
|
||||
import { flushCulvertOptions } from "../B06_Section/B06_Section_Api_Culvert_Options";
|
||||
import { flushPendingPipes } from "./B05_Profile_Api_Pipes_Draft";
|
||||
import { stateKey } from "../A00_Common/b_page_state";
|
||||
import { saveDesign } from "../A00_Common/design/design_save";
|
||||
import {
|
||||
invalidateSectionDetail,
|
||||
saveCachedCrossPatches,
|
||||
@@ -127,74 +125,68 @@ export async function solveRouteAction(ctx: PageActionContext): Promise<void> {
|
||||
}
|
||||
|
||||
/** [임시저장] — 현재 편집을 영구저장소에 남기되 워크플로 단계·페이지는 그대로 둔다.
|
||||
* 종·횡 통합 확정은 B06 [확정]이 담당한다(2026-08-08 워크플로우 재정의). */
|
||||
* 종·횡 통합 확정은 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;
|
||||
// 관 매설 지점을 B04와 같은 저장소에 남긴다 — 저장 실패가 임시저장을 막지는 않는다.
|
||||
// 세션 초안(B06 저장도 같은 창구를 쓴다)을 먼저 내보내고, 화면 목록으로 한 번 더 맞춘다.
|
||||
await flushPendingPipes(projectId).catch(() => undefined);
|
||||
await ctx
|
||||
.profilePanel()
|
||||
.drainage.savePipes()
|
||||
.catch(() => 0);
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
// 종단 계획선 편집은 화면에서만 계산해 두었으므로 저장 시점에 영속화한다.
|
||||
await ctx.profilePanel().save();
|
||||
// 구조물도 조작분이 세션에만 있다(2026-08-29 — CLAUDE.md 5장). 실패는 자체 토스트로
|
||||
// 알리고 세션에 남겨 두므로, 여기서 저장 전체를 멈추지 않는다.
|
||||
await ctx.bridge().saveStructuresIfDirty();
|
||||
// 비정규 측점·상단측 변경분까지 데이터로는 확정 저장하되, 단계 완료 전이는 하지 않는다.
|
||||
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,
|
||||
})),
|
||||
await saveDesign({
|
||||
projectId,
|
||||
routeId: () => latest?.route?.id ?? null,
|
||||
steps: {
|
||||
// 세션 초안이 나간 뒤 화면 목록으로 한 번 더 맞춘다(시설 보관소 되붙이기 · 응답으로 화면 갱신).
|
||||
pipes: async () => {
|
||||
await ctx.profilePanel().drainage.savePipes();
|
||||
},
|
||||
false,
|
||||
);
|
||||
// B06 조정창에서 만진 배수관 구간값도 세션에만 있다 — 함께 내보낸다
|
||||
// (CLAUDE.md 5장: 영구저장은 [저장]·[확정]에서만). 실패해도 저장은 진행한다.
|
||||
// 관 위치는 두 페이지가 같은 스냅샷(`pipes`)으로만 옮김 — 옛 이동 예약은 안 보냄.
|
||||
if (latest?.route?.id != null) {
|
||||
await flushCulvertOptions(
|
||||
projectId,
|
||||
stateKey("culvertopt", projectId, latest.route.id),
|
||||
).catch(() => undefined);
|
||||
}
|
||||
// 구조물 — 저장 뒤 서버 목록(새 식별자)으로 화면을 맞춘다. 실패는 자체 토스트로 알리고
|
||||
// 세션에 남겨 두므로 저장 전체를 멈추지 않는다(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 사용자 지적).
|
||||
if (latest?.route?.id != null) {
|
||||
await saveCachedCrossPatches(projectId, latest.route.id);
|
||||
}
|
||||
// 서버가 종단 정본의 계획선을 다시 썼다 — 공유 캐시를 비워 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");
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : L("B05_Route_TempSave_Failed"), "error");
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
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"),
|
||||
});
|
||||
}
|
||||
|
||||
/** [초기화] — 사용자 편집 전부 폐기, 계획노선 CSV 기본값으로 B05·B06 재계산 후 재진입.
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
|
||||
import type { AlignmentEdits, ProfileAlignment } from "./B05_Profile_UI_Profile_Alignment";
|
||||
import { chainageKey, emptyEdits, hasEdits } from "./B05_Profile_UI_Profile_Alignment";
|
||||
import { ALIGNMENT_DRAFT_KEY_PREFIX, useDesignStore } from "../A00_Common/design/design_store";
|
||||
|
||||
const DRAFT_KEY_PREFIX = "b05-profile-alignment-draft";
|
||||
/** 버튼 폭(21px)에 여유를 더한 값 — 이보다 가까우면 겹친 것으로 본다. */
|
||||
const BUTTON_CLEARANCE_PX = 23;
|
||||
const BUTTON_HALF_PX = 10;
|
||||
@@ -79,6 +79,9 @@ export interface ProfileEditStore {
|
||||
edited(): boolean;
|
||||
}
|
||||
|
||||
/** 초안 읽기 · 쓰기는 설계 가게(`design_store` 의 `alignmentDraft`) 한 창구로만. */
|
||||
const draftOf = (routeId: number) => useDesignStore(null, routeId);
|
||||
|
||||
/**
|
||||
* 노선의 미저장 편집 초안을 읽는다(B06 공용).
|
||||
*
|
||||
@@ -87,7 +90,7 @@ export interface ProfileEditStore {
|
||||
* 쓰면 초안 편집분이 B06에서 되돌아간 것처럼 보인다.
|
||||
*/
|
||||
export function readAlignmentDraft(routeId: number | null): AlignmentEdits | null {
|
||||
return routeId === null ? null : readDraft(`${DRAFT_KEY_PREFIX}:${routeId}`);
|
||||
return routeId === null ? null : draftOf(routeId).get("alignmentDraft");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,13 +99,15 @@ export function readAlignmentDraft(routeId: number | null): AlignmentEdits | nul
|
||||
* 초기화는 서버 값을 초기 스냅샷으로 되돌리는데, 이 초안이 남아 있으면 화면이 그 위에
|
||||
* 옛 편집 델타를 다시 얹어 계획선이 측점에서 원지반선과 만나지 않는다(2026-09-04 실측:
|
||||
* 초기화 직후에는 멀쩡하다가 새로고침 한 번에 최대 6.0m 어긋남).
|
||||
* 노선 번호를 모르는 옛 초안까지 쓸어야 해 여기서만 키를 훑는다.
|
||||
*/
|
||||
export function clearAlignmentDrafts(): void {
|
||||
shared.clear();
|
||||
try {
|
||||
const keys: string[] = [];
|
||||
for (let index = 0; index < sessionStorage.length; index += 1) {
|
||||
const key = sessionStorage.key(index);
|
||||
if (key?.startsWith(`${DRAFT_KEY_PREFIX}:`)) keys.push(key);
|
||||
if (key?.startsWith(`${ALIGNMENT_DRAFT_KEY_PREFIX}:`)) keys.push(key);
|
||||
}
|
||||
keys.forEach((key) => sessionStorage.removeItem(key));
|
||||
} catch {
|
||||
@@ -110,18 +115,47 @@ export function clearAlignmentDrafts(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function readDraft(storageKey: string): AlignmentEdits | null {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(storageKey);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw) as Partial<AlignmentEdits>;
|
||||
return {
|
||||
station_offsets: parsed.station_offsets ?? {},
|
||||
curve_radii: parsed.curve_radii ?? {},
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
/**
|
||||
* 노선마다 **한 벌**인 편집 상태 — B05 종단 패널이 주인이고 B06 계획선 편집은 같은 벌을
|
||||
* 쓴다(계획서 24-2a). 예전에는 두 화면이 가게를 따로 만들어 같은 초안 키를 각자 고쳤다 —
|
||||
* 한쪽이 메모리에 쥔 옛 값으로 다시 쓰면 다른 쪽 편집이 덮였다.
|
||||
*/
|
||||
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 };
|
||||
}
|
||||
|
||||
/** [저장]이 보낼 계획고 초안 — 미저장 편집이 없으면 null. */
|
||||
export function pendingAlignment(routeId: number): AlignmentEdits | null {
|
||||
const state = shared.get(routeId);
|
||||
if (state) return state.unsaved ? state.current : null;
|
||||
return readAlignmentDraft(routeId);
|
||||
}
|
||||
|
||||
/** 계획고가 정본에 나갔다 — 그 노선 상태를 저장됨으로 두고 초안을 지운다. */
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,6 +165,9 @@ function readDraft(storageKey: string): AlignmentEdits | null {
|
||||
* 새로고침한 경우) 초안을 우선 채택하고 미저장 상태로 표시한다. routeId가 바뀌면
|
||||
* 초안 키도 바뀌어 이전 노선의 편집이 새 노선에 잘못 얹히지 않는다.
|
||||
*
|
||||
* 부를 때마다 그 노선의 한 벌을 **초안 · 저장분으로 다시 세운다**(되돌리기가 초안을 바꾼 뒤
|
||||
* 다시 읽는 길). 이미 선 한 벌에 붙기만 하려면 `useProfileEditStore`.
|
||||
*
|
||||
* routeId가 아직 없으면 **초안을 두지 않는다**(2026-09-04). 노선 없는 키(`:none`)에
|
||||
* 쌓인 초안은 어느 노선에나 되붙어 초기화로도 떨어지지 않았다.
|
||||
*/
|
||||
@@ -139,56 +176,62 @@ export function createProfileEditStore(
|
||||
saved: AlignmentEdits,
|
||||
onChange: () => void,
|
||||
): ProfileEditStore {
|
||||
const storageKey = routeId === null ? null : `${DRAFT_KEY_PREFIX}:${routeId}`;
|
||||
const draft = storageKey ? readDraft(storageKey) : null;
|
||||
let current = draft ?? saved;
|
||||
let unsaved = draft !== null;
|
||||
/**
|
||||
* [초기화]의 기준점 — **마지막 [저장] 시점의 편집분**이다(2026-09-03 사용자 지시).
|
||||
* 처음에는 서버 저장분이고, [저장]을 누를 때마다 그때 값으로 옮겨 간다. 초기 자동
|
||||
* 선형으로 되돌리는 것(`resetAll`)과는 다르다 — 저장한 작업까지 지우지 않는다.
|
||||
*/
|
||||
let savedBaseline = saved;
|
||||
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);
|
||||
}
|
||||
|
||||
/** 그 노선에 이미 선 한 벌에 붙는다 — 없으면 `createProfileEditStore` 와 같다(B06 계획선 편집). */
|
||||
export function useProfileEditStore(
|
||||
routeId: number,
|
||||
saved: AlignmentEdits,
|
||||
onChange: () => void,
|
||||
): ProfileEditStore {
|
||||
const state = shared.get(routeId);
|
||||
return state
|
||||
? handleOf(routeId, state, onChange)
|
||||
: createProfileEditStore(routeId, saved, onChange);
|
||||
}
|
||||
|
||||
function handleOf(
|
||||
routeId: number | null,
|
||||
state: EditState,
|
||||
onChange: () => void,
|
||||
): ProfileEditStore {
|
||||
const draft = routeId === null ? null : draftOf(routeId);
|
||||
|
||||
function dropDraft(): void {
|
||||
if (!storageKey) return;
|
||||
try {
|
||||
sessionStorage.removeItem(storageKey);
|
||||
} catch {
|
||||
// 세션 저장소를 못 쓰는 환경에서도 편집 자체는 계속 동작해야 한다.
|
||||
}
|
||||
draft?.set("alignmentDraft", null);
|
||||
}
|
||||
|
||||
function commit(next: AlignmentEdits): void {
|
||||
current = next;
|
||||
unsaved = true;
|
||||
try {
|
||||
if (storageKey) sessionStorage.setItem(storageKey, JSON.stringify(current));
|
||||
} catch {
|
||||
// 초안 보관 실패는 편집을 막지 않는다.
|
||||
}
|
||||
state.current = next;
|
||||
state.unsaved = true;
|
||||
draft?.set("alignmentDraft", state.current);
|
||||
onChange();
|
||||
}
|
||||
|
||||
return {
|
||||
edits: () => current,
|
||||
edits: () => state.current,
|
||||
replace: commit,
|
||||
markSaved() {
|
||||
unsaved = false;
|
||||
savedBaseline = current;
|
||||
state.unsaved = false;
|
||||
state.savedBaseline = state.current;
|
||||
dropDraft();
|
||||
},
|
||||
restoreSaved() {
|
||||
current = savedBaseline;
|
||||
unsaved = false;
|
||||
state.current = state.savedBaseline;
|
||||
state.unsaved = false;
|
||||
dropDraft();
|
||||
onChange();
|
||||
},
|
||||
canRestoreSaved: () => JSON.stringify(current) !== JSON.stringify(savedBaseline),
|
||||
canRestoreSaved: () => JSON.stringify(state.current) !== JSON.stringify(state.savedBaseline),
|
||||
resetStation(chainageM, moveTo) {
|
||||
const key = chainageKey(chainageM);
|
||||
const stationOffsets = { ...current.station_offsets };
|
||||
const curveRadii = { ...current.curve_radii };
|
||||
const stationOffsets = { ...state.current.station_offsets };
|
||||
const curveRadii = { ...state.current.curve_radii };
|
||||
if (moveTo !== undefined) {
|
||||
const to = chainageKey(moveTo);
|
||||
if (stationOffsets[key] !== undefined) stationOffsets[to] = stationOffsets[key];
|
||||
@@ -200,7 +243,7 @@ export function createProfileEditStore(
|
||||
delete stationOffsets[key];
|
||||
// 그 측점의 곡선반경은 저장분(자동 계산이 역산해 둔 값)으로 되돌린다 — 지우면
|
||||
// 프론트가 기본 R로 다시 그려 계획고가 서버 정본과 어긋난다(2026-08-23).
|
||||
const savedRadius = saved.curve_radii?.[key];
|
||||
const savedRadius = state.saved.curve_radii?.[key];
|
||||
if (savedRadius === undefined) delete curveRadii[key];
|
||||
else curveRadii[key] = savedRadius;
|
||||
commit({ station_offsets: stationOffsets, curve_radii: curveRadii });
|
||||
@@ -209,10 +252,10 @@ export function createProfileEditStore(
|
||||
// 지우는 것은 **사용자 편집 측점**이다. 자동 계획선이 역산해 저장한 곡선반경
|
||||
// (`curve_radii`)까지 비우면 호가 달라져 화면 계획고가 정본과 달라진다 —
|
||||
// 세월교 지점에서 0.22m 낮게 나와 최소고 경고가 잘못 떴다(2026-08-23 진단).
|
||||
commit({ ...emptyEdits(), curve_radii: { ...(saved.curve_radii ?? {}) } });
|
||||
commit({ ...emptyEdits(), curve_radii: { ...(state.saved.curve_radii ?? {}) } });
|
||||
},
|
||||
dirty: () => unsaved,
|
||||
edited: () => hasEdits(current),
|
||||
dirty: () => state.unsaved,
|
||||
edited: () => hasEdits(state.current),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user