Merge remote-tracking branch 'origin/dev' into sub_laptop_3

This commit is contained in:
2026-10-19 09:43:39 +09:00
20 changed files with 499 additions and 133 deletions
-2
View File
@@ -218,8 +218,6 @@ export const STATE_REGISTRY = {
/* ── ④ 계산 결과 ─────────────────────────────────────────────────────── */
/** 노선·종단 최신 응답. 페이지를 오갈 때 이 값으로 먼저 그린다. */
latest: { bucket: "result", scope: "project", legacy: (p) => `b05:latest:${p}` },
/** 종단도 최소 흙덮기 강제 여부 — 종단 보기 「페이지 설정」 값을 두 보기가 같이 본다(24-2c). */
"profile-enforce-min-cover": { bucket: "result", scope: "project" },
/** 종횡단 설정값(config 상수 + 프로젝트 임도 종류 + 노선 id). B05·B06 이 같은 값을
* 받아 쓰므로 화면을 오갈 때마다 다시 묻지 않는다(2026-09-06 호출 정리).
* 노선이 바뀌면 `latest` 와 함께 버린다. */
+17 -24
View File
@@ -12,7 +12,6 @@
* ========================================================================== */
import { routeSpansFromStructures } from "../../B05_Profile/B05_Profile_UI_Drainage_Spans";
import { readState, writeState } from "../b_page_state";
import type { SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
import type {
StructureInstance,
@@ -44,6 +43,9 @@ import {
toAlignmentBase,
} from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
import { createProfileEditStore } from "../../B05_Profile/B05_Profile_UI_Profile_Edit";
import { enforceMinCoverOf } from "../../B05_Profile/B05_Profile_Api_PageSettings";
import { applyPageSettings } from "../../B05_Profile/B05_Profile_UI_Page_Latest";
import { useDesignStore } from "./design_store";
import {
endOffsetsOf,
withEndOffset,
@@ -118,9 +120,6 @@ export function createDesignProfile(ctx: DesignProfileContext) {
});
/** 횡단배수 최소 계획고 대상(배수관·BOX암거) — 관 목록이 바뀔 때 갱신한다. */
let minCoverTargets: MinCoverPoint[] = [];
/** 최소고를 편집에서 강제할지 — 종단 보기 「페이지 설정」 체크박스를 따라온다(기본 해제).
* 두 보기가 같은 값을 보도록 세션 한 칸에 비춰 둔다. */
let enforceMinCover = readState<boolean>("profile-enforce-min-cover", projectId) === true;
/** 관 목록이 바뀌었다 — 최소 흙덮기 판정 대상을 다시 잡는다(판 · 보기 어느 쪽에서 와도). */
const takePipes = (pipes: ReadonlyArray<MinCoverPipe>): void => {
// 횡단배수 최소 계획고(2026-08-23) — 관경·구체높이가 바뀌면 경고도 다시 본다.
@@ -176,8 +175,6 @@ export function createDesignProfile(ctx: DesignProfileContext) {
let store = createProfileEditStore(null, emptyEdits(), () => rebuild());
/** 계산 묶음이 노선 초안으로 셈한 종단을 그리는 동안의 편집 한 벌 — 노선 0 칸(alignment-draft · 346-1). */
let preview: typeof store | null = null;
/** 서버 저장분 — 되돌리기 복원이 초안을 다시 읽을 때 기준으로 쓴다. */
let savedEdits: AlignmentEdits = emptyEdits();
let redrawPending = false;
let lastWidth = 0;
let lastHeight = 0;
@@ -264,14 +261,8 @@ export function createDesignProfile(ctx: DesignProfileContext) {
callbacks?.onStructureMove?.(station.chainage_m, toChainageM, station),
// 판이 없는 보기는 관 자리를 되돌리기에 태우지 않는다.
drainage: drainagePanel ?? { pipeChainages: () => [], setPipeChainages: () => undefined },
restore: () => {
// 세션이 정본 — 편집 초안을 다시 읽음 · 노선 초안 동안은 노선 0 칸 · 계획고가 그대로면 다시 셈 예약 없음(349-0q)
const [was, draft] = [JSON.stringify(store.edits()), store === preview];
const id = draft ? pagePipeline.DRAFT_ROUTE_ID : routeId;
store = createProfileEditStore(id, draft ? store.edits() : savedEdits, () => rebuild());
if (draft) preview = store;
void (JSON.stringify(store.edits()) !== was ? rebuild() : draw());
},
// 세션이 정본 — 가게가 편집 초안을 다시 읽음(노선 초안 동안은 노선 0 칸) · 계획고가 그대로면 다시 셈 예약 없음(349-0q)
restore: () => void (store.resync() ? rebuild() : draw()),
restoreSaved: () => store.restoreSaved(),
canRestoreSaved: () => store.canRestoreSaved(),
refresh: () => renderBalance(),
@@ -293,7 +284,9 @@ export function createDesignProfile(ctx: DesignProfileContext) {
return false;
}
// 최소고 가드 — 편집 경로가 전부 여기로 모인다(2026-09-02 직선화·쉬프트·틸팅·방향키 누락 수정).
if (blocksMinCover(base, alignment, candidate, enforceMinCover, minCoverTargets)) return false;
// 최소고 강제 = 페이지 설정 한 칸(초안 ?? 저장 노선 · 472-5) — 보기마다 사본 없음
const enforce = enforceMinCoverOf(projectId);
if (blocksMinCover(base, alignment, candidate, enforce, minCoverTargets)) return false;
store.replace(next);
history.record();
return true;
@@ -441,7 +434,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
drainagePanel?.setIntervalSpans(routeSpansFromStructures(structures, structureTypes));
}
return {
const profile = {
root,
/** 보기가 바뀌었다 — 종단도는 그대로 두고 판에만 알린다(다시 만들지 않음). */
setView(next: DesignView) {
@@ -460,8 +453,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
// 서버 저장분을 기준으로 삼되, 남아 있는 세션 초안이 있으면 그쪽을 우선한다.
if (routeChanged || (store !== preview && !store.dirty())) {
routeId = nextRouteId ?? routeId;
savedEdits = stored?.edits ?? emptyEdits();
store = createProfileEditStore(routeId, savedEdits, () => rebuild());
store = createProfileEditStore(routeId, stored?.edits ?? emptyEdits(), () => rebuild());
}
storedBase = stored ? toAlignmentBase(stored) : null;
if (store !== preview) base = listed(); // 노선 초안 그림은 그대로(갈아끼운 상세)
@@ -510,11 +502,6 @@ export function createDesignProfile(ctx: DesignProfileContext) {
draw();
revealPick();
},
/** 횡단배수 최소고를 편집에서 강제할지(사이드 체크박스). 경고 표시는 무관하게 유지된다. */
setEnforceMinCover(enforce: boolean) {
enforceMinCover = enforce;
writeState("profile-enforce-min-cover", enforce, projectId);
},
/** 판 없이 관 목록을 넘긴다 — 판이 없는 보기에서 최소 흙덮기를 판정하는 길. */
setMinCoverPipes(pipes: ReadonlyArray<MinCoverPipe>) {
takePipes(pipes);
@@ -603,7 +590,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
rebuild(),
);
if (draft) return void ((base = nextBase), rebuild());
if (store === preview) store = createProfileEditStore(routeId, savedEdits, () => rebuild());
if (store === preview) store = createProfileEditStore(routeId, undefined, () => rebuild());
[preview, base] = [null, listed()];
alignment = base && buildAlignment(base, store.edits()); // 그리기만 · 다시 셈 예약 없음(349-0l)
drawSoon();
@@ -656,8 +643,14 @@ export function createDesignProfile(ctx: DesignProfileContext) {
drag.dispose();
crossPreview.dispose();
unbindViewport();
unbindSettings();
},
};
// 페이지 설정 = 가게 한 칸 — ↶ ↷ · 다른 보기가 고친 기울기 한도 · 시작 측점을 다시 얹음(N6 · 472-5)
const unbindSettings = useDesignStore(projectId, null).subscribe("pageSettings", () =>
applyPageSettings(profile, projectId),
);
return profile;
}
/** 종단도 한 벌 — `createDesignProfile` 이 돌려주는 것. */
+23 -2
View File
@@ -12,7 +12,9 @@
*
* `alignmentDraft` 는 등록부 `alignment-draft`(노선 칸)다 — 옛 키(`b05-profile-alignment-draft:{노선}`)
* 값은 안 받는다(387-1). B05 되돌리기(`_Profile_History`)도 이 칸을 사진 찍는다(24-2c).
* 부르는 쪽이 프로젝트를 모르면(`null`) 지금 연 프로젝트 칸을 쓴다.
* 부르는 쪽이 프로젝트를 모르면(`null`) 지금 연 프로젝트 칸을 쓴다. `alignmentSaved` = 그 노선 서버
* 저장분(메모리만 · 같은 프로젝트 규칙 · 472-5) — 지금 편집 = 초안 ?? 저장분(`_Profile_Edit`).
* `pageSettings` = 등록부 `pagesettings`(페이지 설정 초안 · 448) — 폼 · 기울기 한도 · 최소고 강제가 읽음.
*
* 계산 묶음 칸(344-4 · 모양 = `design_pipeline_types.ts`) — `routeDraft` 는 초안(세션 즉시) ·
* `stations` · `profile` · `footprint` 는 **메모리 우선**(결과가 커서 끌기마다 세션에 쓰지 않음 ·
@@ -24,6 +26,7 @@ import type { StateName } from "../b_page_state";
import { CURRENT_PROJECT_ID_KEY } from "../../config/config_frontend";
import type { StructureInstance } from "../../B05_Profile/B05_Profile_Api_Structures";
import type { DetailPipeInput } from "../../B04_PreProcess/B04_PreProcess_Api_Fetch";
import type { PageSettingsBody } from "../../B05_Profile/B05_Profile_Api_Fetch";
import type { AlignmentEdits } from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
import type { SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
import type { ProfileAlignment } from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
@@ -48,6 +51,10 @@ export interface DesignStoreValues {
pipes: DetailPipeInput[] | null;
/** 미저장 계획고 편집(▲▼ · 곡선반경). 노선이 있어야 한다. */
alignmentDraft: AlignmentEdits | null;
/** 그 노선 서버 저장분 계획고 편집 — 초안이 없을 때의 지금 값 · [초기화] 기준점(메모리만). */
alignmentSaved: AlignmentEdits | null;
/** 미저장 페이지 설정(설계속도 · 지형 · 기준값 · 최소고 강제 · 시작 측점). */
pageSettings: PageSettingsBody | null;
/** 종횡단 상세 — 세션 사본. 화면이 함께 쥐는 객체는 `B06_Section_Section_Store`. */
sectionDetail: SectionDetailResponse | null;
/** 고른 측점 · 구조물. */
@@ -120,6 +127,16 @@ function slotOf<K extends DesignStoreKey>(
write: (value) => writeState("alignment-draft", value, project, routeId),
};
}
if (key === "alignmentSaved") {
if (routeId === null) return { id: null, read: () => null, write: () => undefined };
const id = `alignment-saved:${draftProjectId(projectId)}:${routeId}`;
return {
id,
read: () => (savedAlignments.get(id) as V | undefined) ?? null,
write: (value) =>
void (value === null ? savedAlignments.delete(id) : savedAlignments.set(id, value)),
};
}
if (key in MEMORY_SLOTS) return memorySlot<V>(key as MemoryKey, projectId);
// 등록부 이름 — 가게 이름과 캐시 이름이 다른 것만 적는다.
const name = (
@@ -130,8 +147,9 @@ function slotOf<K extends DesignStoreKey>(
sectionDetail: "section-detail",
selection: "selection",
routeDraft: "route-draft",
pageSettings: "pagesettings",
} as const
)[key as Exclude<DesignStoreKey, "alignmentDraft" | MemoryKey>];
)[key as Exclude<DesignStoreKey, "alignmentDraft" | "alignmentSaved" | MemoryKey>];
const routeScoped = name === "section-detail";
if (!projectId || (routeScoped && routeId === null)) {
return { id: null, read: () => null, write: () => undefined };
@@ -144,6 +162,9 @@ function slotOf<K extends DesignStoreKey>(
};
}
/** 노선마다 서버 저장분 계획고 편집(`alignmentSaved`) — 종단도가 상세를 받을 때 채움. */
const savedAlignments = new Map<string, unknown>();
/** 메모리 우선 칸 → 등록부 이름(null = 메모리만). */
const MEMORY_SLOTS = {
stations: "design-stations",
+9 -2
View File
@@ -68,6 +68,7 @@ import "../../B05_Profile/B05_Profile_UI_Style_Drainage.css";
import "../../B05_Profile/B05_Profile_UI_Style_Structures.css";
import { pipeIdAt } from "./design_pipe_ids";
import { useDesignStore } from "./design_store";
import { pageSettingsPanelValues } from "../../B05_Profile/B05_Profile_UI_Page_Latest";
import { setPlusStations } from "../../common_util/common_util_structure_station";
import {
loadSavedPlusStations,
@@ -346,10 +347,16 @@ export async function mountProfileView(
function markStale(): void {
if (state.restoring || !state.routeReady) return;
profilePanel.setGradeLimit(panel.gradeLimitPct());
// 횡단배수 최소고 강제 체크는 재계산 없이 편집 가드에만 즉시 반영된다(2026-09-01).
profilePanel.setEnforceMinCover(panel.values().enforcePipeClearance);
// 횡단배수 최소고 강제 체크는 페이지 설정 한 칸(가게)을 편집 가드가 바로 읽음(2026-09-01 · 472-5).
sections.redrawStationLines();
}
// 페이지 설정 = 가게 한 칸 — ↶ ↷ 가 되쓴 값을 폼에 다시 얹음(N6 · 472-5)
host.onDispose(
designStore.subscribe("pageSettings", () => {
const values = pageSettingsPanelValues(activeProjectId);
if (values) panel.restore(values);
}),
);
viewer.markers.onChange(markStale);
viewer.markers.onStationSelectionChange((stationId) => {
@@ -439,8 +439,8 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
runner: `py:${B06}B06_Section_Engine_Tilt.py#attach_underdrains`,
until: "472-4",
pair: `${B06}B06_Section_Engine_Tilt.py#attach_underdrains`,
mirror: [],
note: "거울 시험 없음 — 옮기기 전 먼저",
mirror: ["test_472_4_underdrain_mirror"],
note: "얹기 = GET 상세(파이썬) · 알려진 차 = 옛 underdrain 걷음 TS 만 · 기울임은 cross.design 안",
},
{
id: "cross.fit",