Merge remote-tracking branch 'origin/sub_laptop_1' into main_desktop_1
This commit is contained in:
@@ -35,7 +35,17 @@ import {
|
|||||||
type SectionPersistContext,
|
type SectionPersistContext,
|
||||||
} from "./B06_Section_UI_Page_Persist";
|
} from "./B06_Section_UI_Page_Persist";
|
||||||
import { maxToeFitHalfWidth } from "./B06_Section_UI_Cross_Fit";
|
import { maxToeFitHalfWidth } from "./B06_Section_UI_Cross_Fit";
|
||||||
import { readAlignmentDraft } from "../B05_Profile/B05_Profile_UI_Profile_Edit";
|
import {
|
||||||
|
createProfileEditStore,
|
||||||
|
readAlignmentDraft,
|
||||||
|
type ProfileEditStore,
|
||||||
|
} from "../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||||
|
import { readAlignment } from "../B05_Profile/B05_Profile_UI_Profile_Data";
|
||||||
|
import {
|
||||||
|
adjustStation,
|
||||||
|
buildAlignment,
|
||||||
|
toAlignmentBase,
|
||||||
|
} from "../B05_Profile/B05_Profile_UI_Profile_Alignment";
|
||||||
import {
|
import {
|
||||||
readStructurePick,
|
readStructurePick,
|
||||||
writeStructurePick,
|
writeStructurePick,
|
||||||
@@ -481,6 +491,41 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
|||||||
);
|
);
|
||||||
// 좌측 목록이 넘겨 준 구조물을 종단 알약 레인으로 보낸다(표시 통일).
|
// 좌측 목록이 넘겨 준 구조물을 종단 알약 레인으로 보낸다(표시 통일).
|
||||||
structureMarksSink = (structures, types) => sectionView.setStructureMarks(structures, types);
|
structureMarksSink = (structures, types) => sectionView.setStructureMarks(structures, types);
|
||||||
|
/**
|
||||||
|
* 계획선 편집(▲/▼) — B05 와 **같은 세션 초안**에 쌓는다(2026-09-12 사용자: B06 에만
|
||||||
|
* 버튼이 없었다). 누르면 그 자리에서 계획선·전 측점 횡단을 다시 풀고(`reconcileStale…`),
|
||||||
|
* 영구저장은 [저장]·[확정]에서만 한다(CLAUDE.md 5장).
|
||||||
|
*/
|
||||||
|
let gradeStore: ProfileEditStore | null = null;
|
||||||
|
let gradeRouteId: number | null = null;
|
||||||
|
const gradeEditFor = (): ReturnType<
|
||||||
|
NonNullable<Parameters<typeof sectionView.setGradeEdit>[0]>
|
||||||
|
> => {
|
||||||
|
const detail = sectionDetail;
|
||||||
|
if (!detail || currentRouteId === null) return null;
|
||||||
|
const stored = readAlignment(detail.longitudinal);
|
||||||
|
if (!stored) return null; // 선형 저장분이 없는 옛 노선 — 편집할 기준선이 없다.
|
||||||
|
if (!gradeStore || gradeRouteId !== currentRouteId) {
|
||||||
|
gradeRouteId = currentRouteId;
|
||||||
|
gradeStore = createProfileEditStore(currentRouteId, stored.edits, () => undefined);
|
||||||
|
}
|
||||||
|
const store = gradeStore;
|
||||||
|
const base = toAlignmentBase(stored);
|
||||||
|
const alignment = buildAlignment(base, store.edits());
|
||||||
|
return {
|
||||||
|
alignment,
|
||||||
|
stepM: alignment.policy.edit_step_m,
|
||||||
|
onStation: (chainageM, delta) => {
|
||||||
|
store.replace(adjustStation(base, store.edits(), chainageM, delta));
|
||||||
|
// 편집분은 세션 초안에 있으므로 재계산이 그것을 그대로 읽는다(강제로 한 번).
|
||||||
|
void reconcileStaleDesigns({ force: true }).then(() =>
|
||||||
|
sectionView.setGradeEdit(gradeEditFor),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
sectionView.setGradeEdit(gradeEditFor);
|
||||||
|
|
||||||
// 종단 그래프 우클릭 — B05 와 같은 메뉴로 넣고 뺀다(2026-09-12 사용자: B05·B06 은 한
|
// 종단 그래프 우클릭 — B05 와 같은 메뉴로 넣고 뺀다(2026-09-12 사용자: B05·B06 은 한
|
||||||
// 페이지라 같은 자리에서 되어야 한다). 어느 길로 들어와도 좌측 「구조물 배치」와
|
// 페이지라 같은 자리에서 되어야 한다). 어느 길로 들어와도 좌측 「구조물 배치」와
|
||||||
// 같은 함수를 타므로 목록·폼·알약이 함께 선다.
|
// 같은 함수를 타므로 목록·폼·알약이 함께 선다.
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ import {
|
|||||||
} from "./B06_Section_Api_Fetch";
|
} from "./B06_Section_Api_Fetch";
|
||||||
import { invalidateSectionDetail } from "./B06_Section_Section_Store";
|
import { invalidateSectionDetail } from "./B06_Section_Section_Store";
|
||||||
import { flushPendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft";
|
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 { flushPendingStructures } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||||
import { flushUphillOverrides } from "../B05_Profile/B05_Profile_Api_Fetch";
|
import { flushUphillOverrides } from "../B05_Profile/B05_Profile_Api_Fetch";
|
||||||
import { buildCrossPatches, type CrossPatchSources } from "./B06_Section_UI_Page_Patches";
|
import { buildCrossPatches, type CrossPatchSources } from "./B06_Section_UI_Page_Patches";
|
||||||
@@ -418,6 +423,15 @@ 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> {
|
async function flushPendingEdits(ctx: SectionPersistContext, projectId: string): Promise<void> {
|
||||||
// ⚠ 순서는 **B05 [임시저장]과 같아야 한다**(2026-09-12 사용자: 어느 페이지에서 저장해도
|
// ⚠ 순서는 **B05 [임시저장]과 같아야 한다**(2026-09-12 사용자: 어느 페이지에서 저장해도
|
||||||
@@ -439,6 +453,13 @@ async function flushPendingEdits(ctx: SectionPersistContext, projectId: string):
|
|||||||
// B05에서 만지고 넘어온 구조물 조작분도 여기서 정본에 남긴다. 실패해도 횡단
|
// B05에서 만지고 넘어온 구조물 조작분도 여기서 정본에 남긴다. 실패해도 횡단
|
||||||
// 저장까지 막지는 않는다 — 미저장분은 세션에 남으므로 다시 시도할 수 있다
|
// 저장까지 막지는 않는다 — 미저장분은 세션에 남으므로 다시 시도할 수 있다
|
||||||
// (2026-08-29 실측: 타입이 거절되자 sections/save가 아예 나가지 않았다).
|
// (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) => {
|
await flushPendingStructures(projectId).catch((error) => {
|
||||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||||
showToast(`구조물 저장에 실패했습니다.${detail}`, "error");
|
showToast(`구조물 저장에 실패했습니다.${detail}`, "error");
|
||||||
|
|||||||
@@ -45,7 +45,11 @@ import {
|
|||||||
import { CROSS_HEIGHT } from "./B06_Section_UI_Section_Common";
|
import { CROSS_HEIGHT } from "./B06_Section_UI_Section_Common";
|
||||||
import { STRUCTURE_LANE_HEIGHT_PX } from "../B05_Profile/B05_Profile_UI_Structures_Marks";
|
import { STRUCTURE_LANE_HEIGHT_PX } from "../B05_Profile/B05_Profile_UI_Structures_Marks";
|
||||||
import type { SectionStructureEdit } from "./B06_Section_UI_Section_View_Menu";
|
import type { SectionStructureEdit } from "./B06_Section_UI_Section_View_Menu";
|
||||||
import { attachWindowScroll, drawLongitudinalPanel } from "./B06_Section_UI_Section_View_Draw";
|
import {
|
||||||
|
attachWindowScroll,
|
||||||
|
drawLongitudinalPanel,
|
||||||
|
type LongitudinalPanelInput,
|
||||||
|
} from "./B06_Section_UI_Section_View_Draw";
|
||||||
import {
|
import {
|
||||||
type StructureInstance,
|
type StructureInstance,
|
||||||
type StructureType,
|
type StructureType,
|
||||||
@@ -118,6 +122,8 @@ export interface SectionViewController {
|
|||||||
/** 종단 그래프 우클릭으로 구조물을 넣고 빼는 길 — 없으면 메뉴가 안 뜬다
|
/** 종단 그래프 우클릭으로 구조물을 넣고 빼는 길 — 없으면 메뉴가 안 뜬다
|
||||||
* (2026-09-12 B05·B06 일원화). */
|
* (2026-09-12 B05·B06 일원화). */
|
||||||
setStructureEdit: (edit: SectionStructureEdit | null) => void;
|
setStructureEdit: (edit: SectionStructureEdit | null) => void;
|
||||||
|
/** 계획선 편집 ▲/▼ — 그릴 때마다 불러 지금 선형·편집 함수를 받는다(null = 버튼 없음). */
|
||||||
|
setGradeEdit: (provider: (() => LongitudinalPanelInput["grade"]) | null) => void;
|
||||||
dispose: () => void;
|
dispose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +157,7 @@ export function createSectionView(
|
|||||||
let markStructures: ReadonlyArray<StructureInstance> = [];
|
let markStructures: ReadonlyArray<StructureInstance> = [];
|
||||||
let markTypes: ReadonlyArray<StructureType> = [];
|
let markTypes: ReadonlyArray<StructureType> = [];
|
||||||
let structureEdit: SectionStructureEdit | null = null;
|
let structureEdit: SectionStructureEdit | null = null;
|
||||||
|
let gradeEditProvider: (() => LongitudinalPanelInput["grade"]) | null = null;
|
||||||
let renderWidth = 0;
|
let renderWidth = 0;
|
||||||
let resizeTimer = 0;
|
let resizeTimer = 0;
|
||||||
let panelResizeTimer = 0;
|
let panelResizeTimer = 0;
|
||||||
@@ -508,6 +515,7 @@ export function createSectionView(
|
|||||||
naturalSpoilSlope: currentNaturalSpoilSlope,
|
naturalSpoilSlope: currentNaturalSpoilSlope,
|
||||||
selectStation: (stationId) => selectStation(stationId, true),
|
selectStation: (stationId) => selectStation(stationId, true),
|
||||||
setMassBadge: (values) => massBadge.set(values),
|
setMassBadge: (values) => massBadge.set(values),
|
||||||
|
grade: gradeEditProvider?.() ?? null,
|
||||||
});
|
});
|
||||||
// 상단 패널이 sticky라 선택 카드가 그 아래로 숨는다 — 패널 높이만큼 스크롤 여백을 잡아 준다.
|
// 상단 패널이 sticky라 선택 카드가 그 아래로 숨는다 — 패널 높이만큼 스크롤 여백을 잡아 준다.
|
||||||
syncScrollMargin();
|
syncScrollMargin();
|
||||||
@@ -525,11 +533,7 @@ export function createSectionView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
attachWindowScroll(
|
attachWindowScroll(chartWrap, () => updateChartWindow, drawPanel);
|
||||||
chartWrap,
|
|
||||||
() => updateChartWindow,
|
|
||||||
() => drawPanel(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const draw = (): void => {
|
const draw = (): void => {
|
||||||
if (!currentDetail || !Number.isFinite(renderWidth) || renderWidth <= 0) return;
|
if (!currentDetail || !Number.isFinite(renderWidth) || renderWidth <= 0) return;
|
||||||
@@ -663,6 +667,10 @@ export function createSectionView(
|
|||||||
structureEdit = edit;
|
structureEdit = edit;
|
||||||
drawPanel();
|
drawPanel();
|
||||||
},
|
},
|
||||||
|
setGradeEdit(provider) {
|
||||||
|
gradeEditProvider = provider;
|
||||||
|
drawPanel();
|
||||||
|
},
|
||||||
setStructureMarks(structures, types) {
|
setStructureMarks(structures, types) {
|
||||||
markStructures = structures;
|
markStructures = structures;
|
||||||
markTypes = types;
|
markTypes = types;
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export interface LongitudinalChartInput {
|
|||||||
viewportWidth: number;
|
viewportWidth: number;
|
||||||
/** 구조물(비정규) 측점선을 끌어 옮겼다. 안 넘기면 그 선은 못 잡는다. */
|
/** 구조물(비정규) 측점선을 끌어 옮겼다. 안 넘기면 그 선은 못 잡는다. */
|
||||||
onDragStation?: (stationId: string, toChainageM: number) => void;
|
onDragStation?: (stationId: string, toChainageM: number) => void;
|
||||||
|
/** X축·측점 라벨을 바닥에서 이만큼(px) 올린다 — 계획고 편집 ▼ 버튼과 겹치지 않게. */
|
||||||
|
bottomInsetPx?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LongitudinalChartResult {
|
export interface LongitudinalChartResult {
|
||||||
@@ -114,7 +116,7 @@ export function buildLongitudinalChart(input: LongitudinalChartInput): Longitudi
|
|||||||
undefined,
|
undefined,
|
||||||
// 구조물(비정규) 측점선 끌어 옮기기 — B05 와 같은 조작이다(2026-09-12 일원화).
|
// 구조물(비정규) 측점선 끌어 옮기기 — B05 와 같은 조작이다(2026-09-12 일원화).
|
||||||
input.onDragStation,
|
input.onDragStation,
|
||||||
0,
|
input.bottomInsetPx ?? 0,
|
||||||
0,
|
0,
|
||||||
visibleElevationRange(detail, fromM, toM) ?? undefined,
|
visibleElevationRange(detail, fromM, toM) ?? undefined,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import {
|
|||||||
moveMarkById,
|
moveMarkById,
|
||||||
type SectionStructureEdit,
|
type SectionStructureEdit,
|
||||||
} from "./B06_Section_UI_Section_View_Menu";
|
} from "./B06_Section_UI_Section_View_Menu";
|
||||||
|
import { createEditOverlay } from "../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||||
|
import type { ProfileAlignment } from "../B05_Profile/B05_Profile_UI_Profile_Alignment";
|
||||||
|
|
||||||
/** 그래프 한 벌을 세우는 데 필요한 값 — 본체가 재고 고른 것을 그대로 넘긴다. */
|
/** 그래프 한 벌을 세우는 데 필요한 값 — 본체가 재고 고른 것을 그대로 넘긴다. */
|
||||||
export interface LongitudinalPanelInput {
|
export interface LongitudinalPanelInput {
|
||||||
@@ -53,6 +55,14 @@ export interface LongitudinalPanelInput {
|
|||||||
conversion?: EarthworkConversion;
|
conversion?: EarthworkConversion;
|
||||||
naturalSpoilSlope?: number;
|
naturalSpoilSlope?: number;
|
||||||
selectStation: (stationId: string) => void;
|
selectStation: (stationId: string) => void;
|
||||||
|
/** 계획선 편집 — 넘기면 종단 그래프 위에 ▲/▼ 버튼층이 선다(B05 와 같은 부품).
|
||||||
|
* 안 넘기면 버튼이 없고 그래프만 보인다(옛 저장분처럼 선형이 없는 노선). */
|
||||||
|
grade?: {
|
||||||
|
alignment: ProfileAlignment;
|
||||||
|
/** 한 번 누를 때 오르내리는 양(m) — 선형 정책값. */
|
||||||
|
stepM: number;
|
||||||
|
onStation: (chainageM: number, delta: number) => void;
|
||||||
|
} | null;
|
||||||
/** 좌측 상단 누가토량 배지 — 값이 없으면 null 로 지운다. */
|
/** 좌측 상단 누가토량 배지 — 값이 없으면 null 로 지운다. */
|
||||||
setMassBadge: (values: ReturnType<typeof badgeValuesFrom> | null) => void;
|
setMassBadge: (values: ReturnType<typeof badgeValuesFrom> | null) => void;
|
||||||
}
|
}
|
||||||
@@ -91,6 +101,9 @@ export function drawLongitudinalPanel(
|
|||||||
minWidth: chartWidth,
|
minWidth: chartWidth,
|
||||||
scrollLeft: input.keepScrollLeft,
|
scrollLeft: input.keepScrollLeft,
|
||||||
viewportWidth: chartWrap.clientWidth || chartWidth,
|
viewportWidth: chartWrap.clientWidth || chartWidth,
|
||||||
|
// 계획고 편집 ▼ 버튼이 바닥에 붙으므로 X축·측점 라벨을 그만큼 밀어 올린다
|
||||||
|
// (B05 와 같은 값 15px). 버튼이 없으면 0 — 종전 여백 그대로다.
|
||||||
|
bottomInsetPx: input.grade ? 15 : 0,
|
||||||
// 측점선을 끌면 그 구조물이 옮겨 간다 — 관은 예약 이동, 구조물은 정본 이동.
|
// 측점선을 끌면 그 구조물이 옮겨 간다 — 관은 예약 이동, 구조물은 정본 이동.
|
||||||
onDragStation: edit ? moveMark : undefined,
|
onDragStation: edit ? moveMark : undefined,
|
||||||
});
|
});
|
||||||
@@ -130,6 +143,20 @@ export function drawLongitudinalPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
chartWrap.replaceChildren(...nodes);
|
chartWrap.replaceChildren(...nodes);
|
||||||
|
// 계획선 편집 버튼층 — B05 와 **같은 부품**(`createEditOverlay`)이다(2026-09-12 사용자:
|
||||||
|
// B05·B06 은 한 페이지인데 B06 에만 버튼이 없었다). 누른 값은 B05 와 같은 세션 초안에
|
||||||
|
// 쌓이고 [저장]·[확정]에서 종단 정본으로 나간다.
|
||||||
|
if (input.grade) {
|
||||||
|
chartWrap.append(
|
||||||
|
createEditOverlay({
|
||||||
|
alignment: input.grade.alignment,
|
||||||
|
width: chartWidth,
|
||||||
|
x: chart.toX,
|
||||||
|
step: input.grade.stepM,
|
||||||
|
onStation: input.grade.onStation,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
// 종단 그래프 우클릭 — B05 와 같은 메뉴다(가까운 구조물이 있으면 삭제, 없으면 구조물군
|
// 종단 그래프 우클릭 — B05 와 같은 메뉴다(가까운 구조물이 있으면 삭제, 없으면 구조물군
|
||||||
// → 종류 2단 추가). 그래프를 다시 그릴 때마다 붙인다(상태를 안 남긴다).
|
// → 종류 2단 추가). 그래프를 다시 그릴 때마다 붙인다(상태를 안 남긴다).
|
||||||
if (edit) {
|
if (edit) {
|
||||||
|
|||||||
Reference in New Issue
Block a user