Merge remote-tracking branch 'origin/dev' into sub_laptop_6
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
createProfileEditStore,
|
||||
useProfileEditStore,
|
||||
type ProfileEditStore,
|
||||
} from "../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||
import { readAlignment, toDesignProfile } from "../B05_Profile/B05_Profile_UI_Profile_Data";
|
||||
@@ -28,8 +28,8 @@ export interface GradeEditContext {
|
||||
}
|
||||
|
||||
/**
|
||||
* 계획선 편집(▲/▼) — B05 와 **같은 세션 초안**에 쌓는다(2026-09-12 사용자: B06 에만
|
||||
* 버튼이 없었다). 누르면 그 자리에서 계획선·전 측점 횡단을 다시 풀고(`reconcile`),
|
||||
* 계획선 편집(▲/▼) — B05 종단 패널의 **같은 가게**(노선마다 한 벌)를 쓴다(계획서 24-2a ·
|
||||
* 2026-09-12 사용자: B06 에만 버튼이 없었다). 누르면 그 자리에서 계획선·전 측점 횡단을 다시 풀고(`reconcile`),
|
||||
* 영구저장은 [저장]·[확정]에서만 한다(CLAUDE.md 5장).
|
||||
*/
|
||||
export function createGradeEdit(ctx: GradeEditContext): GradeEditProvider {
|
||||
@@ -53,7 +53,7 @@ export function createGradeEdit(ctx: GradeEditContext): GradeEditProvider {
|
||||
if (!stored) return null; // 선형 저장분이 없는 옛 노선 — 편집할 기준선이 없다.
|
||||
if (!gradeStore || gradeRouteId !== currentRouteId) {
|
||||
gradeRouteId = currentRouteId;
|
||||
gradeStore = createProfileEditStore(currentRouteId, stored.edits, () => undefined);
|
||||
gradeStore = useProfileEditStore(currentRouteId, stored.edits, () => undefined);
|
||||
}
|
||||
const store = gradeStore;
|
||||
const base = toAlignmentBase(stored);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* (CLAUDE.md 5장 데이터 3층: 영구저장은 [저장]·[확정]에서만).
|
||||
* ========================================================================== */
|
||||
|
||||
import { hideLoadingOverlay, showLoadingOverlay, showToast } from "@ui/ui_template_elements";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../A00_Common/b_workflow_nav";
|
||||
import {
|
||||
confirmSections,
|
||||
@@ -17,14 +17,7 @@ import {
|
||||
type SectionDetailResponse,
|
||||
} from "./B06_Section_Api_Fetch";
|
||||
import { invalidateSectionDetail } from "./B06_Section_Section_Store";
|
||||
import { flushPendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft";
|
||||
import { saveProfileAlignment } from "../B05_Profile/B05_Profile_Api_Fetch";
|
||||
import {
|
||||
clearAlignmentDrafts,
|
||||
readAlignmentDraft,
|
||||
} from "../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||
import { flushPendingStructures } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { flushUphillOverrides } from "../B05_Profile/B05_Profile_Api_Fetch";
|
||||
import { saveDesign, type DesignSaveContext } from "../A00_Common/design/design_save";
|
||||
import { buildCrossPatches, type CrossPatchSources } from "./B06_Section_UI_Page_Patches";
|
||||
import { readByKey, readState, writeByKey, writeState } from "../A00_Common/b_page_state";
|
||||
import {
|
||||
@@ -423,102 +416,64 @@ export function collectSectionEdits(ctx: SectionPersistContext): {
|
||||
};
|
||||
}
|
||||
|
||||
/** 계획선 편집 초안이 있으면 종단 정본에 쓰고 초안을 지운다. 없으면 아무 일도 하지 않는다. */
|
||||
async function flushAlignmentDraft(projectId: string, routeId: number | null): Promise<void> {
|
||||
if (routeId === null) return;
|
||||
const draft = readAlignmentDraft(routeId);
|
||||
if (!draft) return;
|
||||
await saveProfileAlignment(projectId, routeId, draft);
|
||||
clearAlignmentDrafts();
|
||||
}
|
||||
|
||||
/** 세션에 쌓인 조정창·구조물 조작을 정본으로 내보낸다 — [저장]·[확정] 공통 앞단. */
|
||||
async function flushPendingEdits(ctx: SectionPersistContext, projectId: string): Promise<void> {
|
||||
// ⚠ 순서는 **B05 [임시저장]과 같아야 한다**(2026-09-12 사용자: 어느 페이지에서 저장해도
|
||||
// 결과가 같아야 한다). 관 목록은 B05 가 **전체 스냅샷**으로, B06 이 **바뀐 것만**(추가·
|
||||
// 삭제·이동·구간값) 담으므로, 스냅샷을 먼저 얹고 그 위에 델타를 적용해야 한다. 반대로
|
||||
// 하면 스냅샷이 B06 편집을 통째로 덮는다.
|
||||
await flushPendingPipes(projectId).catch((error) => {
|
||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||
showToast(`배수관 저장에 실패했습니다.${detail}`, "error");
|
||||
});
|
||||
// 조정창 구간값·추가·삭제·이동은 세션에만 있다 — 정본 payload를 모으기 전에 내보낸다
|
||||
// (CLAUDE.md 5장: 영구저장은 [저장]·[확정]에서만).
|
||||
await ctx.flushCulvertOptions();
|
||||
// B05 3D에서 바꾼 상단측(측구 방향)도 여기서 내보낸다 — 예전에는 B05 [임시저장]에만
|
||||
// 실려, B06에서 저장·확정하면 세션에만 남아 옛 방향이 정본에 그대로 있었다
|
||||
// (2026-09-06). 서버가 종단 정본과 저장된 횡단 설계를 함께 갱신하므로 아래
|
||||
// 횡단 patch 저장보다 **먼저** 나가야 사용자 수정이 위에 얹힌다.
|
||||
await flushUphillOverrides(projectId).catch(() => undefined);
|
||||
// B05에서 만지고 넘어온 구조물 조작분도 여기서 정본에 남긴다. 실패해도 횡단
|
||||
// 저장까지 막지는 않는다 — 미저장분은 세션에 남으므로 다시 시도할 수 있다
|
||||
// (2026-08-29 실측: 타입이 거절되자 sections/save가 아예 나가지 않았다).
|
||||
// 계획선 편집(▲/▼)은 세션 초안에만 있다 — B06 에서 고쳤든 B05 에서 고쳤든 여기서
|
||||
// 종단 정본으로 내보낸다(2026-09-12). 종전에는 B06 이 초안을 **읽기만** 해서, B06 에서
|
||||
// 저장하면 계획선 편집이 다음 진입 때 사라졌다.
|
||||
await flushAlignmentDraft(projectId, ctx.routeId()).catch((error) => {
|
||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||
showToast(`계획선 저장에 실패했습니다.${detail}`, "error");
|
||||
});
|
||||
await flushPendingStructures(projectId).catch((error) => {
|
||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||
showToast(`구조물 저장에 실패했습니다.${detail}`, "error");
|
||||
});
|
||||
/**
|
||||
* B06 의 저장 한 줄기 창구(계획서 24-2a) — 차례는 `saveDesign` 이 정하고(B05 [임시저장]과 같음),
|
||||
* 여기서는 B06 몫만 준다: 저장 앞 전 측점 재계산 · 조정창 구간값 · 횡단 본문 · 뒤처리.
|
||||
*/
|
||||
function sectionSaveContext(ctx: SectionPersistContext, projectId: string): DesignSaveContext {
|
||||
return {
|
||||
projectId,
|
||||
routeId: ctx.routeId,
|
||||
// ⚠ 면적을 모으기 **전에** 전 측점을 다시 계산한다 — 안 그러면 표준단면을 고친 뒤
|
||||
// 안 만진 측점이 옛 면적으로 실려 나간다(2026-09-09 실측).
|
||||
prepare: () => ctx.reconcileDesigns(),
|
||||
// 조정창 구간값은 조정창 저장기가 낸다 — 실패하면 알리고 저장을 멈춘다(예전 그대로).
|
||||
steps: { culvertOptions: () => ctx.flushCulvertOptions() },
|
||||
cross: async (confirm) => {
|
||||
const routeId = ctx.routeId();
|
||||
if (routeId === null) return;
|
||||
const edits = collectSectionEdits(ctx);
|
||||
await (confirm ? confirmSections : saveSections)(
|
||||
projectId,
|
||||
routeId,
|
||||
ctx.standardValues(),
|
||||
edits.crossPatches.length ? edits.crossPatches : undefined,
|
||||
edits.massHaul,
|
||||
);
|
||||
},
|
||||
done: (confirm) => {
|
||||
if (confirm) {
|
||||
showToast(L("B06_Profile_Confirm_Success"), "success");
|
||||
goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[4]);
|
||||
return;
|
||||
}
|
||||
// 저장으로 구조물 초안이 정본이 됐다 — 화면이 들고 있던 상세는 초안 기준으로 벽을
|
||||
// 얹은 사본이라, 그대로 두면 새로 저장된 구조물의 벽이 다음 조회까지 안 붙는다
|
||||
// (2026-09-06 실측: 8+0.0 흙막이가 저장 뒤 화면 사본에만 빠져 있었음).
|
||||
const routeId = ctx.routeId();
|
||||
if (routeId !== null) invalidateSectionDetail(projectId, routeId);
|
||||
showToast(L("B06_Profile_Save_Success"), "success");
|
||||
},
|
||||
failed: (error, confirm) => {
|
||||
if (confirm) {
|
||||
const detail = error instanceof Error ? error.message : L("B06_Profile_Confirm_Failed");
|
||||
showToast(`${L("B06_Profile_Confirm_Failed")} ${detail}`, "error");
|
||||
return;
|
||||
}
|
||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||
showToast(`${L("B06_Profile_Save_Failed")}${detail}`, "error");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 임시 저장 — 저장만 하고 페이지는 그대로 둔다. */
|
||||
export async function saveCurrentSections(ctx: SectionPersistContext): Promise<void> {
|
||||
const routeId = ctx.routeId();
|
||||
if (!ctx.projectId || routeId === null) return;
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
// ⚠ 면적을 모으기 **전에** 전 측점을 다시 계산한다 — 안 그러면 표준단면을 고친 뒤
|
||||
// 안 만진 측점이 옛 면적으로 실려 나간다(2026-09-09 실측).
|
||||
await ctx.reconcileDesigns();
|
||||
await flushPendingEdits(ctx, ctx.projectId);
|
||||
const edits = collectSectionEdits(ctx);
|
||||
await saveSections(
|
||||
ctx.projectId,
|
||||
routeId,
|
||||
ctx.standardValues(),
|
||||
edits.crossPatches.length ? edits.crossPatches : undefined,
|
||||
edits.massHaul,
|
||||
);
|
||||
// 저장으로 구조물 초안이 정본이 됐다 — 화면이 들고 있던 상세는 초안 기준으로 벽을
|
||||
// 얹은 사본이라, 그대로 두면 새로 저장된 구조물의 벽이 다음 조회까지 안 붙는다
|
||||
// (2026-09-06 실측: 8+0.0 흙막이가 저장 뒤 화면 사본에만 빠져 있었음).
|
||||
invalidateSectionDetail(ctx.projectId, routeId);
|
||||
showToast(L("B06_Profile_Save_Success"), "success");
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||
showToast(`${L("B06_Profile_Save_Failed")}${detail}`, "error");
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
if (!ctx.projectId) return;
|
||||
await saveDesign(sectionSaveContext(ctx, ctx.projectId));
|
||||
}
|
||||
|
||||
/** 확정 — 저장 뒤 다음 단계(상세설계)로 넘어간다. */
|
||||
export async function confirmCurrentSections(ctx: SectionPersistContext): Promise<void> {
|
||||
const routeId = ctx.routeId();
|
||||
if (!ctx.projectId || routeId === null) return;
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
await ctx.reconcileDesigns(); // 저장과 같은 이유 — 옛 면적이 정본으로 굳는 것을 막는다
|
||||
await flushPendingEdits(ctx, ctx.projectId);
|
||||
const edits = collectSectionEdits(ctx);
|
||||
await confirmSections(
|
||||
ctx.projectId,
|
||||
routeId,
|
||||
ctx.standardValues(),
|
||||
edits.crossPatches.length ? edits.crossPatches : undefined,
|
||||
edits.massHaul,
|
||||
);
|
||||
showToast(L("B06_Profile_Confirm_Success"), "success");
|
||||
goToWorkflowStage(ctx.projectId, WORKFLOW_STEP_ROUTES[4]);
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : L("B06_Profile_Confirm_Failed");
|
||||
showToast(`${L("B06_Profile_Confirm_Failed")} ${detail}`, "error");
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
if (!ctx.projectId) return;
|
||||
await saveDesign(sectionSaveContext(ctx, ctx.projectId), { confirm: true });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user