Merge remote-tracking branch 'origin/dev' into sub_laptop_8
# Conflicts: # common_util/common_util_calc_runner.py # resources/tester/test_472_3_calc_registry.py
This commit is contained in:
@@ -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` 와 함께 버린다. */
|
||||
|
||||
@@ -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) — 관경·구체높이가 바뀌면 경고도 다시 본다.
|
||||
@@ -174,8 +173,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;
|
||||
@@ -262,14 +259,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(),
|
||||
@@ -291,7 +282,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;
|
||||
@@ -437,7 +430,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
|
||||
drainagePanel?.setIntervalSpans(routeSpansFromStructures(structures, structureTypes));
|
||||
}
|
||||
|
||||
return {
|
||||
const profile = {
|
||||
root,
|
||||
/** 보기가 바뀌었다 — 종단도는 그대로 두고 판에만 알린다(다시 만들지 않음). */
|
||||
setView(next: DesignView) {
|
||||
@@ -456,8 +449,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(); // 노선 초안 그림은 그대로(갈아끼운 상세)
|
||||
@@ -506,11 +498,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);
|
||||
@@ -598,7 +585,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();
|
||||
@@ -651,8 +638,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` 이 돌려주는 것. */
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
* excavation?: { pipes: [{ diameter_mm }] · walls: [{ height_m · average_thickness_m ·
|
||||
* has_foundation }] } ·
|
||||
* cross_design?: { standard: 표준 횡단(config 위에 저장분 덮은 한 벌) ·
|
||||
* stations: [{ samples · design_elevation_m · options: 파이썬 `compute_cross_design` 키 }] } } }
|
||||
* stations: [{ samples · design_elevation_m · options: 파이썬 `compute_cross_design` 키 }] } ·
|
||||
* berm?: { structures: 구조물 정본 목록 · chainages: [측점 m] } } }
|
||||
* 출력 { results: { [id]: 값 }, available: [id…] }
|
||||
* — 차례는 요청 차례가 아니라 기댐 차례(누가토량 앞에 구조물 면적을 상세에 얹음 ·
|
||||
* server-calc 묶음과 같은 차례).
|
||||
@@ -19,6 +20,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { bermSpansFromStructures, bermSpecAt } from "@util/common_util_berm_spans";
|
||||
import type { BermSpec } from "@util/common_util_cross_berm";
|
||||
import { computeCrossDesign } from "@util/common_util_cross_design";
|
||||
import type { CrossDesignOptions, CrossGroundSample } from "@util/common_util_cross_design";
|
||||
@@ -64,6 +66,10 @@ interface CalcInputs extends FollowInputs {
|
||||
has_foundation?: boolean;
|
||||
}>;
|
||||
};
|
||||
berm?: {
|
||||
structures: Parameters<typeof bermSpansFromStructures>[0];
|
||||
chainages: number[];
|
||||
};
|
||||
cross_design?: {
|
||||
standard: CrossDesignOptions["standard"];
|
||||
stations?: Array<{
|
||||
@@ -181,6 +187,11 @@ function runDesignCalc(run: readonly string[], inputs: CalcInputs) {
|
||||
),
|
||||
})),
|
||||
}),
|
||||
// 소단 구간(C4) — 구조물 정본 → 구간 · 측점마다 제원(겹치면 먼저 놓은 것).
|
||||
"cross.berm_spans": () => {
|
||||
const spans = bermSpansFromStructures(inputs.berm?.structures ?? []);
|
||||
return { spans, at: (inputs.berm?.chainages ?? []).map((c) => bermSpecAt(spans, c)) };
|
||||
},
|
||||
// 측점마다 TS 짝(`computeCrossDesign`) — 못 세우면 그 자리 { error }(파이썬은 ValueError 로 되던짐).
|
||||
"cross.design": () => {
|
||||
const batch = inputs.cross_design;
|
||||
|
||||
@@ -364,11 +364,10 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
site: "browser_now",
|
||||
stage: "cross",
|
||||
ts: `${U}common_util_cross_design.ts#computeCrossDesign`,
|
||||
runner: `py:${B06}B06_Section_Engine_Design.py#compute_cross_design`,
|
||||
until: "472-4",
|
||||
runner: "node:cross.design",
|
||||
pair: `${B06}B06_Section_Engine_Design.py#compute_cross_design`,
|
||||
mirror: ["test_b06_cross_design_mirror", "test_178_2_berm_fill_mirror"],
|
||||
note: "파이썬 짝은 B07 · 추천 · Route_Save · B05 확정도 부름 — 짝 남음",
|
||||
note: "[저장] · [확정] 다시 셈 = Node(472-4 ②) · 파이썬 짝 남음(B07 · 추천 · Route_Save · B05 확정 · 넓힘 찾기) · 알려진 차 = 설계선 점 반올림 동점 1e-4 m",
|
||||
},
|
||||
{
|
||||
id: "cross.areas",
|
||||
@@ -379,11 +378,10 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
site: "browser_now",
|
||||
stage: "cross",
|
||||
ts: `${U}common_util_cross_design_areas.ts#splitCutAreas`,
|
||||
runner: `py:${B06}B06_Section_Engine_Areas.py#_split_cut_areas`,
|
||||
until: "472-4",
|
||||
runner: "node:cross.design",
|
||||
pair: `${B06}B06_Section_Engine_Areas.py#_split_cut_areas`,
|
||||
mirror: ["test_b06_cross_design_mirror"],
|
||||
note: "X3 — 표준 측점 = 파이썬 · 구조물 측점 = Node(structure.layouts)",
|
||||
note: "설계선과 한 셈(cross.design) · X3 한 갈래 — 표준 측점 · 구조물 측점(structure.layouts) 모두 Node",
|
||||
},
|
||||
{
|
||||
id: "cross.enforce",
|
||||
@@ -397,6 +395,7 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
runner: `py:${B06}B06_Section_Router_Design.py#enforce_design_lines`,
|
||||
until: "472-4",
|
||||
pair: `${B06}B06_Section_Router_Design.py#enforce_design_lines`,
|
||||
note: "구간 판정 = 파이썬 짝(Router_Design_Ranges) · 다시 세운 설계 = Node cross.design",
|
||||
mirror: ["test_349_0n_cross_enforce_mirror"],
|
||||
},
|
||||
{
|
||||
@@ -407,12 +406,11 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
outputs: ["소단 구간"],
|
||||
site: "browser_now",
|
||||
stage: null,
|
||||
ts: `${B06}B06_Section_UI_Page_Persist.ts#bermSpansFromStructures`,
|
||||
runner: `py:${B06}B06_Section_Engine_Structures_Wall.py#load_berm_spans`,
|
||||
until: "472-4",
|
||||
ts: `${U}common_util_berm_spans.ts#bermSpansFromStructures`,
|
||||
runner: "node:cross.berm_spans",
|
||||
pair: `${B06}B06_Section_Engine_Structures_Wall.py#load_berm_spans`,
|
||||
mirror: ["test_272_berm_source"],
|
||||
note: "TS 가 화면 파일 안 — common_util 로(472-4 ②)",
|
||||
note: "[저장] 다시 셈 = Node(472-4 ②) · 파이썬 짝은 Node 못 부를 때만",
|
||||
},
|
||||
{
|
||||
id: "cross.berm_slope",
|
||||
@@ -423,9 +421,9 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
site: "browser_now",
|
||||
stage: "cross",
|
||||
ts: `${U}common_util_cross_berm.ts#cutProfilePoints`,
|
||||
runner: `py:${U}common_util_cross_berm.py#cut_profile_points`,
|
||||
until: "472-4",
|
||||
runner: "node:cross.design",
|
||||
pair: `${U}common_util_cross_berm.py#cut_profile_points`,
|
||||
note: "설계선 셈 안(cross.design) · 파이썬 짝 남음(cross.design 과 같은 까닭)",
|
||||
mirror: ["test_178_2_berm_fill_mirror"],
|
||||
},
|
||||
{
|
||||
@@ -440,8 +438,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",
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""횡단 설계선 · 면적 · 소단 구간을 서버 Node 입구로(PLAN 472-4 ② · 등록부 `cross.design` ·
|
||||
`cross.berm_spans`) — 브라우저와 같은 TS(`common_util_cross_design` · `common_util_berm_spans`).
|
||||
|
||||
파이썬은 입력 조립만 — 측점마다 `compute_cross_design` 키를 모아 Node 한 번에 보냄.
|
||||
Node 를 못 부르면 파이썬 짝으로(거울 시험 `test_b06_cross_design_mirror` · `test_272_berm_source` ·
|
||||
알려진 차 = 설계선 점 반올림 동점 1e-4 m — 파이썬 round 짝수 쪽 · TS 위쪽).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from B06_Section.B06_Section_Engine_Design import _resolve_group, compute_cross_design
|
||||
from common_util.common_util_calc_runner import run_calc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 한 측점 입력 = (지반 표본, 계획고, `compute_cross_design` 키 — standard 뺌)
|
||||
DesignJob = tuple[list[dict[str, Any]], float | None, dict[str, Any]]
|
||||
|
||||
|
||||
def full_standard(standard: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""config 기본값 위에 저장분을 덮은 표준 횡단 한 벌 — TS 는 config 를 모름(입력으로 받음)."""
|
||||
out: dict[str, Any] = {}
|
||||
for key in ("soil", "rock", "paved"):
|
||||
g = _resolve_group(key, standard)
|
||||
out[key] = {
|
||||
"road_width_m": g["road_width_m"],
|
||||
"shoulder_left_m": g["shoulder_left_m"],
|
||||
"shoulder_right_m": g["shoulder_right_m"],
|
||||
"ditch": {
|
||||
"top_width_m": g["ditch_top_width_m"],
|
||||
"bottom_width_m": g["ditch_bottom_width_m"],
|
||||
"depth_m": g["ditch_depth_m"],
|
||||
},
|
||||
"ditch_l_type": {"width_m": g["l_ditch_width_m"], "depth_m": g["l_ditch_depth_m"]},
|
||||
"cross_slope_pct": {"min": g["cross_slope_pct"]},
|
||||
"fill_slope_ratio": g["fill_slope_ratio"],
|
||||
"cut_slope_ratio": g["cut_slope_ratio"],
|
||||
"pavement_thickness_m": g["pavement_thickness_m"],
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def _python(job: DesignJob, standard: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
samples, elevation, options = job
|
||||
try:
|
||||
return compute_cross_design(samples, elevation, standard=standard, **options)
|
||||
except (ValueError, KeyError):
|
||||
return None
|
||||
|
||||
|
||||
def compute_cross_designs(
|
||||
jobs: list[DesignJob], standard: dict[str, Any] | None
|
||||
) -> list[dict[str, Any] | None]:
|
||||
"""측점 묶음 횡단 설계 — 차례 그대로 결과 · None = 못 세움(파이썬 ValueError 자리)."""
|
||||
if not jobs:
|
||||
return []
|
||||
stations = [
|
||||
{
|
||||
"samples": samples,
|
||||
"design_elevation_m": elevation,
|
||||
"options": {
|
||||
key: (value._asdict() if key == "berm" and value is not None else value)
|
||||
for key, value in options.items()
|
||||
},
|
||||
}
|
||||
for samples, elevation, options in jobs
|
||||
]
|
||||
results = run_calc(
|
||||
["cross.design"],
|
||||
{"cross_design": {"standard": full_standard(standard), "stations": stations}},
|
||||
)
|
||||
if results is None:
|
||||
logger.warning("Node 횡단 설계 실패 — 파이썬 짝으로 셈(%d 측점)", len(jobs))
|
||||
return [_python(job, standard) for job in jobs]
|
||||
return [None if "error" in row else row for row in results["cross.design"]]
|
||||
|
||||
|
||||
def berm_specs_at(
|
||||
project_root: Path, chainages: list[float]
|
||||
) -> list[dict[str, float] | None] | None:
|
||||
"""측점마다 소단 제원(구조물 정본 구간) · None = 정본을 못 읽음(부르는 쪽이 저장분 그대로)."""
|
||||
from B05_Profile.B05_Profile_Structures_Repository import load_structures
|
||||
from B06_Section.B06_Section_Engine_Structures_Wall import berm_span_at, load_berm_spans
|
||||
|
||||
try:
|
||||
structures = [
|
||||
item.model_dump(include={"type_id", "start_m", "end_m", "chainage_m", "options"})
|
||||
for item in load_structures(str(project_root))[1]
|
||||
]
|
||||
except Exception: # noqa: BLE001 — 정본을 못 읽으면 저장분으로 물러섬
|
||||
logger.exception("B06 소단 구간을 읽지 못했습니다 (저장분 소단을 씀)")
|
||||
return None
|
||||
results = run_calc(
|
||||
["cross.berm_spans"], {"berm": {"structures": structures, "chainages": chainages}}
|
||||
)
|
||||
if results is not None:
|
||||
return results["cross.berm_spans"]["at"]
|
||||
logger.warning("Node 소단 구간 실패 — 파이썬 짝으로 셈")
|
||||
spans = load_berm_spans(project_root)
|
||||
return None if spans is None else [berm_span_at(spans, c) for c in chainages]
|
||||
@@ -118,6 +118,7 @@ def extend_unclosed_sections(
|
||||
sampler: Any,
|
||||
) -> int:
|
||||
"""저장 설계가 미교차인 측점을 넓혀 샘플·설계를 자리에서 갈고 횡단 파일에 남긴다. 넓힌 수."""
|
||||
from B06_Section.B06_Section_Engine_Design_Node import compute_cross_designs
|
||||
from B06_Section.B06_Section_Router_Design import (
|
||||
USER_TOUCHED_KEYS,
|
||||
ford_drop_at,
|
||||
@@ -132,23 +133,21 @@ def extend_unclosed_sections(
|
||||
return 0
|
||||
drops = ford_surface_drops(project_root)
|
||||
cross_dir = project_root / "B06_Section" / "cross_sections"
|
||||
changed = 0
|
||||
done: list[tuple[dict[str, Any], dict[str, Any], float, list[dict[str, Any]], dict, tuple]] = []
|
||||
for section in cross_sections:
|
||||
design = section.get("design")
|
||||
if not isinstance(design, dict) or not design.get("slope_unclosed"):
|
||||
continue
|
||||
chainage = float(section.get("chainage_m", 0.0))
|
||||
|
||||
def compute(samples: list[dict[str, Any]], design=design, chainage=chainage) -> dict:
|
||||
return compute_cross_design(
|
||||
samples,
|
||||
design_elevation_from_longitudinal(longitudinal, chainage),
|
||||
try:
|
||||
elevation = design_elevation_from_longitudinal(longitudinal, chainage)
|
||||
options = dict(
|
||||
ground_type=str(design.get("ground_type") or "ripping_rock"),
|
||||
section_mode=str(design.get("section_mode") or "left_cut"),
|
||||
ditch_side=design.get("ditch_side"),
|
||||
ditch_type=str(design.get("ditch_type") or "standard"),
|
||||
paved=bool(design.get("paved", False)),
|
||||
standard=standard,
|
||||
rock_boundary_offset_m=design.get(
|
||||
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
),
|
||||
@@ -162,6 +161,12 @@ def extend_unclosed_sections(
|
||||
**stored_shoulder_extend(design), # 노견 확장(139-6)
|
||||
**stored_tilt(design), # 물넘이 · 맹암거 자리 측구 없음 · 기울기(286-2 · 287-4)
|
||||
)
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
|
||||
# 넓힘 찾기 = 파이썬 짝(되풀이 셈) · 넓힌 뒤 정본 설계 = Node 한 번(472-4 ② · 아래)
|
||||
def compute(samples: list[dict[str, Any]], z=elevation, opts=options) -> dict:
|
||||
return compute_cross_design(samples, z, standard=standard, **opts)
|
||||
|
||||
try:
|
||||
result = extend_unclosed(
|
||||
@@ -174,7 +179,14 @@ def extend_unclosed_sections(
|
||||
samples, info = result
|
||||
if not info["left_m"] and not info["right_m"]:
|
||||
continue # 첫 걸음부터 지표면 밖 — 샘플이 그대로라 갈 것이 없음
|
||||
recomputed = compute(samples)
|
||||
done.append((section, design, chainage, samples, info, (samples, elevation, options)))
|
||||
results = compute_cross_designs([job for *_rest, job in done], standard)
|
||||
changed = 0
|
||||
for (section, design, chainage, samples, info, _job), recomputed in zip(
|
||||
done, results, strict=True
|
||||
):
|
||||
if recomputed is None:
|
||||
continue
|
||||
for key in ("status", "pavement_suggested", *USER_TOUCHED_KEYS):
|
||||
if design.get(key) is not None:
|
||||
recomputed[key] = design[key]
|
||||
|
||||
@@ -15,6 +15,11 @@ from B06_Section.B06_Section_Engine_Design import (
|
||||
stored_shoulder_extend,
|
||||
stored_tilt,
|
||||
)
|
||||
from B06_Section.B06_Section_Engine_Design_Node import (
|
||||
DesignJob,
|
||||
berm_specs_at,
|
||||
compute_cross_designs,
|
||||
)
|
||||
from B06_Section.B06_Section_Engine_Tilt import tilt_args, tilt_ranges
|
||||
|
||||
# 포장 · 세월교 구간(나눈 파일) — 이 모듈 이름으로 다시 내보냄(부르는 곳 · 시험 갈아 끼움 그대로).
|
||||
@@ -130,6 +135,26 @@ def stored_standard_cross_section(longitudinal_row: dict[str, Any] | None) -> di
|
||||
return standard if isinstance(standard, dict) else None
|
||||
|
||||
|
||||
def apply_designs(
|
||||
jobs: list[tuple[dict[str, Any], dict[str, Any], DesignJob]], standard: dict[str, Any] | None
|
||||
) -> int:
|
||||
"""모은 측점을 Node 한 번(TS 짝 · 472-4 ②)으로 다시 세워 자리에서 갈아 끼움 · 바꾼 수.
|
||||
|
||||
`design` = 승계 원천(사용자 조작값 · 상태) · 못 세운 측점은 저장분 그대로(옛 ValueError 자리).
|
||||
"""
|
||||
results = compute_cross_designs([job for _section, _design, job in jobs], standard)
|
||||
changed = 0
|
||||
for (section, design, _job), recomputed in zip(jobs, results, strict=True):
|
||||
if recomputed is None:
|
||||
continue
|
||||
for key in ("status", "pavement_suggested", *USER_TOUCHED_KEYS):
|
||||
if design.get(key) is not None:
|
||||
recomputed[key] = design[key]
|
||||
section["design"] = recomputed
|
||||
changed += 1
|
||||
return changed
|
||||
|
||||
|
||||
def enforce_pavement_ranges(
|
||||
longitudinal: dict[str, Any],
|
||||
cross_sections: list[dict[str, Any]],
|
||||
@@ -146,7 +171,7 @@ def enforce_pavement_ranges(
|
||||
ranges = pavement_ranges(project_root)
|
||||
tilts = tilt_ranges(project_root)
|
||||
ford_drops = ford_surface_drops(project_root)
|
||||
changed = 0
|
||||
jobs: list[tuple[dict[str, Any], dict[str, Any], DesignJob]] = []
|
||||
for section in cross_sections:
|
||||
design = section.get("design")
|
||||
if not isinstance(design, dict):
|
||||
@@ -157,15 +182,12 @@ def enforce_pavement_ranges(
|
||||
if bool(design.get("paved")) == wanted and stored_tilt(design) == tilt:
|
||||
continue
|
||||
try:
|
||||
recomputed = compute_cross_design(
|
||||
section.get("samples", []),
|
||||
design_elevation_from_longitudinal(longitudinal, chainage),
|
||||
options = dict(
|
||||
ground_type=str(design.get("ground_type") or "ripping_rock"),
|
||||
section_mode=str(design.get("section_mode") or "left_cut"),
|
||||
ditch_side=design.get("ditch_side"),
|
||||
ditch_type=str(design.get("ditch_type") or "standard"),
|
||||
paved=wanted,
|
||||
standard=standard,
|
||||
rock_boundary_offset_m=design.get(
|
||||
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
),
|
||||
@@ -179,14 +201,11 @@ def enforce_pavement_ranges(
|
||||
**stored_shoulder_extend(design), # 노견 확장(139-6)
|
||||
**tilt,
|
||||
)
|
||||
elevation = design_elevation_from_longitudinal(longitudinal, chainage)
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
for key in ("status", "pavement_suggested", *USER_TOUCHED_KEYS):
|
||||
if design.get(key) is not None:
|
||||
recomputed[key] = design[key]
|
||||
section["design"] = recomputed
|
||||
changed += 1
|
||||
return changed
|
||||
jobs.append((section, design, (section.get("samples", []), elevation, options)))
|
||||
return apply_designs(jobs, standard)
|
||||
|
||||
|
||||
def enforce_design_lines(
|
||||
@@ -203,29 +222,23 @@ def enforce_design_lines(
|
||||
확장) + 지금 종단 계획고 · 세월교 월류 하강 · 곡선부 확폭. 사용자 조작값은 승계.
|
||||
소단 제원은 구조물 정본(`structures.json`)의 구간이 원천 — 쪽마다 옮긴 깊이는 측점 patch(194-1).
|
||||
"""
|
||||
from B06_Section.B06_Section_Engine_Structures_Wall import berm_span_at, load_berm_spans
|
||||
|
||||
ford_drops = ford_surface_drops(project_root)
|
||||
tilts = tilt_ranges(project_root)
|
||||
spans = load_berm_spans(project_root)
|
||||
changed = 0
|
||||
for section in cross_sections:
|
||||
design = section.get("design")
|
||||
if not isinstance(design, dict):
|
||||
continue
|
||||
chainage = float(section.get("chainage_m", 0.0))
|
||||
if spans is not None: # 구간을 지웠으면 소단 없음 · 못 읽으면 저장분 그대로
|
||||
design = {**design, "berm": berm_span_at(spans, chainage)}
|
||||
targets = [section for section in cross_sections if isinstance(section.get("design"), dict)]
|
||||
chainages = [float(section.get("chainage_m", 0.0)) for section in targets]
|
||||
berms = berm_specs_at(project_root, chainages) # Node 소단 구간(C4) · None = 못 읽음
|
||||
jobs: list[tuple[dict[str, Any], dict[str, Any], DesignJob]] = []
|
||||
for index, (section, chainage) in enumerate(zip(targets, chainages, strict=True)):
|
||||
design = section["design"]
|
||||
if berms is not None: # 구간을 지웠으면 소단 없음 · 못 읽으면 저장분 그대로
|
||||
design = {**design, "berm": berms[index]}
|
||||
try:
|
||||
recomputed = compute_cross_design(
|
||||
section.get("samples", []),
|
||||
design_elevation_from_longitudinal(longitudinal, chainage),
|
||||
options = dict(
|
||||
ground_type=str(design.get("ground_type") or "ripping_rock"),
|
||||
section_mode=str(design.get("section_mode") or "left_cut"),
|
||||
ditch_side=design.get("ditch_side"),
|
||||
ditch_type=str(design.get("ditch_type") or "standard"),
|
||||
paved=bool(design.get("paved", False)),
|
||||
standard=standard,
|
||||
rock_boundary_offset_m=design.get(
|
||||
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
),
|
||||
@@ -239,14 +252,11 @@ def enforce_design_lines(
|
||||
**stored_shoulder_extend(design),
|
||||
**tilt_args(chainage, tilts),
|
||||
)
|
||||
elevation = design_elevation_from_longitudinal(longitudinal, chainage)
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
for key in ("status", "pavement_suggested", *USER_TOUCHED_KEYS):
|
||||
if design.get(key) is not None:
|
||||
recomputed[key] = design[key]
|
||||
section["design"] = recomputed
|
||||
changed += 1
|
||||
return changed
|
||||
jobs.append((section, design, (section.get("samples", []), elevation, options)))
|
||||
return apply_designs(jobs, standard)
|
||||
|
||||
|
||||
def enforce_ford_surface_drops(
|
||||
@@ -262,7 +272,7 @@ def enforce_ford_surface_drops(
|
||||
월류 높이를 지우면 같은 경로로 원래 계획고로 되돌아온다. 사용자 조작값은 승계한다.
|
||||
"""
|
||||
drops = ford_surface_drops(project_root)
|
||||
changed = 0
|
||||
jobs: list[tuple[dict[str, Any], dict[str, Any], DesignJob]] = []
|
||||
for section in cross_sections:
|
||||
design = section.get("design")
|
||||
if not isinstance(design, dict):
|
||||
@@ -273,15 +283,12 @@ def enforce_ford_surface_drops(
|
||||
if abs(wanted - stored) < 1e-6:
|
||||
continue
|
||||
try:
|
||||
recomputed = compute_cross_design(
|
||||
section.get("samples", []),
|
||||
design_elevation_from_longitudinal(longitudinal, chainage),
|
||||
options = dict(
|
||||
ground_type=str(design.get("ground_type") or "ripping_rock"),
|
||||
section_mode=str(design.get("section_mode") or "left_cut"),
|
||||
ditch_side=design.get("ditch_side"),
|
||||
ditch_type=str(design.get("ditch_type") or "standard"),
|
||||
paved=bool(design.get("paved", False)),
|
||||
standard=standard,
|
||||
rock_boundary_offset_m=design.get(
|
||||
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
),
|
||||
@@ -295,14 +302,11 @@ def enforce_ford_surface_drops(
|
||||
**stored_shoulder_extend(design), # 노견 확장(139-6)
|
||||
**stored_tilt(design),
|
||||
)
|
||||
elevation = design_elevation_from_longitudinal(longitudinal, chainage)
|
||||
except (ValueError, KeyError):
|
||||
continue
|
||||
for key in ("status", "pavement_suggested", *USER_TOUCHED_KEYS):
|
||||
if design.get(key) is not None:
|
||||
recomputed[key] = design[key]
|
||||
section["design"] = recomputed
|
||||
changed += 1
|
||||
return changed
|
||||
jobs.append((section, design, (section.get("samples", []), elevation, options)))
|
||||
return apply_designs(jobs, standard)
|
||||
|
||||
|
||||
def default_section_modes(longitudinal: dict[str, Any]) -> dict[float, str]:
|
||||
|
||||
@@ -25,12 +25,6 @@ import {
|
||||
writeState,
|
||||
} from "../A00_Common/b_page_state";
|
||||
import { kindChangedChainages } from "./B06_Section_Api_Culvert_Options";
|
||||
import {
|
||||
BERM_DEFAULT_DEPTH_M,
|
||||
BERM_DEFAULT_INTERVAL_M,
|
||||
BERM_DEFAULT_SLOPE_DEG,
|
||||
BERM_DEFAULT_WIDTH_M,
|
||||
} from "@util/common_util_cross_berm";
|
||||
import {
|
||||
crossDesignChoices,
|
||||
readBermDepths,
|
||||
@@ -43,7 +37,15 @@ import type { StandardCrossSection } from "./B06_Section_Api_Fetch";
|
||||
import type { RockBoundaryControl } from "./B06_Section_UI_Section_View";
|
||||
import type { CutSlopeControl } from "./B06_Section_UI_Cross_CutSlope";
|
||||
import { balloonOffsetsPayload } from "@util/common_util_mass_haul_balance_view";
|
||||
import { catalogRow } from "../common_util/common_util_structure_catalog";
|
||||
// 소단 구간(C4) — common_util 로 옮김 · 부르는 곳 그대로(다시 내보냄).
|
||||
import type { BermSpan } from "@util/common_util_berm_spans";
|
||||
export {
|
||||
bermSpansFromStructures,
|
||||
bermSpecAt,
|
||||
isBermType,
|
||||
type BermSessionSpec,
|
||||
type BermSpan,
|
||||
} from "@util/common_util_berm_spans";
|
||||
import { readKeyed, writeKeyed, type KeyedState } from "./B06_Section_Station_Keys";
|
||||
|
||||
/**
|
||||
@@ -107,27 +109,6 @@ function persistKeyed(name: KeyedState, storageKey: string, values: Map<string,
|
||||
else writeByKey(storageKey, JSON.stringify(Object.fromEntries(values)));
|
||||
}
|
||||
|
||||
/** 측점 하나의 소단 제원 — 서버 payload 와 같은 이름을 쓴다(그대로 실어 보낸다). */
|
||||
export interface BermSessionSpec {
|
||||
width_m: number;
|
||||
interval_m: number;
|
||||
slope_deg: number;
|
||||
/** 노면 끝 아래 깊이(m · ≥ 0 · 178-2). */
|
||||
depth_m: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자가 놓은 소단 **한 구간** — 종단 범위 + 제원.
|
||||
*
|
||||
* 사용자는 측점 하나가 아니라 **구간**에 놓는다(2026-09-07 확정: 「길이 + 기준측점 전·후」).
|
||||
* 그래서 세션에는 구간 목록으로 두고, 측점별 제원은 읽는 자리에서 편다 — 구간을 측점으로
|
||||
* 펴서 저장하면 나중에 「어디부터 어디까지 놓았나」를 되짚을 수 없다.
|
||||
*/
|
||||
export interface BermSpan extends BermSessionSpec {
|
||||
start_m: number;
|
||||
end_m: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션에 쌓인 소단 구간 목록. 없거나 손상되면 빈 목록.
|
||||
*
|
||||
@@ -143,76 +124,6 @@ export function writeBermSpans(projectId: string, routeId: number, spans: BermSp
|
||||
writeState("berm", spans, projectId, routeId);
|
||||
}
|
||||
|
||||
/** 소단 = 표 형상 berm(323-4 · 파이썬 `load_berm_spans` 짝) — 종류 키로 안 가름. */
|
||||
export const isBermType = (typeId: string): boolean => catalogRow(typeId)?.shape === "berm";
|
||||
|
||||
/**
|
||||
* 구조물 목록에서 **소단 구간**을 뽑는다 — 사용자는 「구조물 배치」에서 놓는다
|
||||
* (2026-09-07 사용자 확정: 별도 폼이 아니라 다른 옹벽·기슭막이와 같은 자리).
|
||||
*
|
||||
* 세션 열쇠 `berm` 은 그 결과를 담는 **사본**이다. 재계산(브라우저·서버)이 측점마다 값을
|
||||
* 읽어야 하는데 구조물 목록은 비동기로 오므로, 목록이 바뀔 때마다 여기서 펴 두고 계산은
|
||||
* 그 사본만 본다.
|
||||
*/
|
||||
export function bermSpansFromStructures(
|
||||
structures: ReadonlyArray<{
|
||||
type_id: string;
|
||||
start_m?: number | null;
|
||||
end_m?: number | null;
|
||||
chainage_m?: number | null;
|
||||
options?: Record<string, string | number>;
|
||||
}>,
|
||||
): BermSpan[] {
|
||||
const spans: BermSpan[] = [];
|
||||
for (const item of structures) {
|
||||
if (!isBermType(item.type_id)) continue;
|
||||
const anchor = item.chainage_m ?? item.start_m ?? null;
|
||||
const start = item.start_m ?? anchor;
|
||||
const end = item.end_m ?? anchor;
|
||||
if (start === null || end === null) continue;
|
||||
const options = item.options ?? {};
|
||||
const number = (key: string, fallback: number): number => {
|
||||
const parsed = Number(options[key]);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
||||
};
|
||||
const width = number("width_m", BERM_DEFAULT_WIDTH_M);
|
||||
const interval = number("interval_m", BERM_DEFAULT_INTERVAL_M);
|
||||
if (width <= 0) continue; // 폭이 0이면 계단이 없다(간격은 성토 한 단에 안 씀 · 178-2)
|
||||
// 깊이 = 노면 끝 아래 · 음수는 0(도로보다 높을 수 없음) · 빈 값은 기본 1.0
|
||||
const rawDepth = Number(options.depth_m);
|
||||
const depth =
|
||||
options.depth_m === undefined || options.depth_m === "" || !Number.isFinite(rawDepth)
|
||||
? BERM_DEFAULT_DEPTH_M
|
||||
: Math.max(rawDepth, 0);
|
||||
spans.push({
|
||||
start_m: Math.min(start, end),
|
||||
end_m: Math.max(start, end),
|
||||
width_m: width,
|
||||
interval_m: interval,
|
||||
slope_deg: number("slope_deg", BERM_DEFAULT_SLOPE_DEG),
|
||||
depth_m: depth,
|
||||
});
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
/** 그 측점을 덮는 소단 제원 — 없으면 null. 겹치면 먼저 놓은 것이 이긴다. */
|
||||
export function bermSpecAt(spans: BermSpan[], chainageM: number): BermSessionSpec | null {
|
||||
const found = spans.find(
|
||||
(span) =>
|
||||
chainageM >= Math.min(span.start_m, span.end_m) - 1e-6 &&
|
||||
chainageM <= Math.max(span.start_m, span.end_m) + 1e-6,
|
||||
);
|
||||
return found
|
||||
? {
|
||||
width_m: found.width_m,
|
||||
interval_m: found.interval_m,
|
||||
slope_deg: found.slope_deg,
|
||||
depth_m: found.depth_m ?? BERM_DEFAULT_DEPTH_M,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
/** 암 경계선 오프셋 저장소 — 값(Map)과 조정창 제어기를 함께 낸다. */
|
||||
export interface RockBoundaryStore {
|
||||
/** 측점키(누가거리 2자리) → 오프셋(m). `buildCrossPatches` 가 그대로 읽는다. */
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
① 짝(ts ↔ py · 거울 시험):
|
||||
|
||||
- `Engine_Design.py` ↔ `common_util_cross_design.ts` · `Engine_Areas.py` ↔ `common_util_cross_design_areas.ts` · `test_b06_cross_design_mirror.py`
|
||||
- [저장] · [확정] 다시 셈(`Router_Design` enforce · 넓힘 뒤 설계 · 소단 구간)은 Node(`Engine_Design_Node.py` → design-calc `cross.design` · `cross.berm_spans`) · 파이썬 짝은 B07 · 추천 · Route_Save · Node 못 부를 때
|
||||
- `Engine_Culvert.py` ↔ `common_util_culvert_sets.ts` · `logic/UI_Cross_Culvert_Const.ts` · `test_b06_culvert_sets_mirror.py` · `test_b06_pipe_wall_height_mirror.py`
|
||||
- `Engine_Structures_Wall.py` ↔ `common_util_structure_walls.ts` · `test_b06_structure_walls_mirror.py`
|
||||
- 지형 표본 = `common_util_surface_sampler` 하나(`Engine_SampleExtend` 도)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/* =============================================================================
|
||||
* common_util_berm_spans.ts
|
||||
* 소단 구간 — 구조물 목록(표 형상 berm)에서 구간 · 측점마다 제원(PLAN 472-4 C4 · 화면 파일
|
||||
* `B06_Section_UI_Page_Persist.ts` 에서 옮김 · 그쪽이 다시 내보냄).
|
||||
* 브라우저(시설 줄 · 다시 셈)와 서버 Node 입구(`design_calc_node` · 등록부 `cross.berm_spans`)가 같은 셈.
|
||||
* 파이썬 짝 = `B06_Section_Engine_Structures_Wall.py` `load_berm_spans` · `berm_span_at`
|
||||
* (Node 를 못 부를 때만 · 거울 시험 `test_272_berm_source.py`).
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
BERM_DEFAULT_DEPTH_M,
|
||||
BERM_DEFAULT_INTERVAL_M,
|
||||
BERM_DEFAULT_SLOPE_DEG,
|
||||
BERM_DEFAULT_WIDTH_M,
|
||||
} from "./common_util_cross_berm";
|
||||
import { catalogRow } from "./common_util_structure_catalog";
|
||||
|
||||
/** 측점 하나의 소단 제원 — 서버 payload 와 같은 이름을 쓴다(그대로 실어 보낸다). */
|
||||
export interface BermSessionSpec {
|
||||
width_m: number;
|
||||
interval_m: number;
|
||||
slope_deg: number;
|
||||
/** 노면 끝 아래 깊이(m · ≥ 0 · 178-2). */
|
||||
depth_m: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자가 놓은 소단 **한 구간** — 종단 범위 + 제원.
|
||||
*
|
||||
* 사용자는 측점 하나가 아니라 **구간**에 놓는다(2026-09-07 확정: 「길이 + 기준측점 전·후」).
|
||||
* 그래서 세션에는 구간 목록으로 두고, 측점별 제원은 읽는 자리에서 편다 — 구간을 측점으로
|
||||
* 펴서 저장하면 나중에 「어디부터 어디까지 놓았나」를 되짚을 수 없다.
|
||||
*/
|
||||
export interface BermSpan extends BermSessionSpec {
|
||||
start_m: number;
|
||||
end_m: number;
|
||||
}
|
||||
|
||||
/** 소단 = 표 형상 berm(323-4 · 파이썬 `load_berm_spans` 짝) — 종류 키로 안 가름. */
|
||||
export const isBermType = (typeId: string): boolean => catalogRow(typeId)?.shape === "berm";
|
||||
|
||||
/**
|
||||
* 구조물 목록에서 **소단 구간**을 뽑는다 — 사용자는 「구조물 배치」에서 놓는다
|
||||
* (2026-09-07 사용자 확정: 별도 폼이 아니라 다른 옹벽·기슭막이와 같은 자리).
|
||||
*
|
||||
* 세션 열쇠 `berm` 은 그 결과를 담는 **사본**이다. 재계산(브라우저·서버)이 측점마다 값을
|
||||
* 읽어야 하는데 구조물 목록은 비동기로 오므로, 목록이 바뀔 때마다 여기서 펴 두고 계산은
|
||||
* 그 사본만 본다.
|
||||
*/
|
||||
export function bermSpansFromStructures(
|
||||
structures: ReadonlyArray<{
|
||||
type_id: string;
|
||||
start_m?: number | null;
|
||||
end_m?: number | null;
|
||||
chainage_m?: number | null;
|
||||
options?: Record<string, string | number>;
|
||||
}>,
|
||||
): BermSpan[] {
|
||||
const spans: BermSpan[] = [];
|
||||
for (const item of structures) {
|
||||
if (!isBermType(item.type_id)) continue;
|
||||
const anchor = item.chainage_m ?? item.start_m ?? null;
|
||||
const start = item.start_m ?? anchor;
|
||||
const end = item.end_m ?? anchor;
|
||||
if (start === null || end === null) continue;
|
||||
const options = item.options ?? {};
|
||||
const number = (key: string, fallback: number): number => {
|
||||
const parsed = Number(options[key]);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
||||
};
|
||||
const width = number("width_m", BERM_DEFAULT_WIDTH_M);
|
||||
const interval = number("interval_m", BERM_DEFAULT_INTERVAL_M);
|
||||
if (width <= 0) continue; // 폭이 0이면 계단이 없다(간격은 성토 한 단에 안 씀 · 178-2)
|
||||
// 깊이 = 노면 끝 아래 · 음수는 0(도로보다 높을 수 없음) · 빈 값은 기본 1.0
|
||||
const rawDepth = Number(options.depth_m);
|
||||
const depth =
|
||||
options.depth_m === undefined || options.depth_m === "" || !Number.isFinite(rawDepth)
|
||||
? BERM_DEFAULT_DEPTH_M
|
||||
: Math.max(rawDepth, 0);
|
||||
spans.push({
|
||||
start_m: Math.min(start, end),
|
||||
end_m: Math.max(start, end),
|
||||
width_m: width,
|
||||
interval_m: interval,
|
||||
slope_deg: number("slope_deg", BERM_DEFAULT_SLOPE_DEG),
|
||||
depth_m: depth,
|
||||
});
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
/** 그 측점을 덮는 소단 제원 — 없으면 null. 겹치면 먼저 놓은 것이 이긴다. */
|
||||
export function bermSpecAt(spans: BermSpan[], chainageM: number): BermSessionSpec | null {
|
||||
const found = spans.find(
|
||||
(span) =>
|
||||
chainageM >= Math.min(span.start_m, span.end_m) - 1e-6 &&
|
||||
chainageM <= Math.max(span.start_m, span.end_m) + 1e-6,
|
||||
);
|
||||
return found
|
||||
? {
|
||||
width_m: found.width_m,
|
||||
interval_m: found.interval_m,
|
||||
slope_deg: found.slope_deg,
|
||||
depth_m: found.depth_m ?? BERM_DEFAULT_DEPTH_M,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -27,6 +27,8 @@ NODE_IDS = frozenset(
|
||||
"profile.seat",
|
||||
"facility.plus_follow",
|
||||
"facility.follow",
|
||||
"cross.design",
|
||||
"cross.berm_spans",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -258,3 +258,7 @@
|
||||
|
||||
1. b05/b06의 조작과 데이터 흐름 프론트에서 계산하는지 백엔드에서 계산하는지를 브레인 참고문서를 하나 만들어서 코더들이 b05/b06을 수정할때 참고하게하자.
|
||||
2. 재정리를 당연히 해야함. 단계적으로 계획서를 세워서 진행하고 데이터 저장의 관점을 보면 보안과 사용자 편의성을 위해 충돌이남. 그래서 로직들은 프론트와 백엔드에서 동작하게 하는 로직을 동시에 구성하고 최종적으로 하나의관리코드에서 선택적으로 할수 있게 반영하는건 어때? 물런 코드가 하나면 유지보수에 좋을테니 더 좋고 어때?
|
||||
|
||||
## 요구 274 (10-19 · 사용자 · 472-7 물음 답)
|
||||
|
||||
1. 측구는 변경이 되었으니 사용자가 이전에 선택한 값른 의미가 없어짐.
|
||||
|
||||
@@ -8,7 +8,7 @@ source: ["docs/raw/조사/2026-10-19_472_구조다시세움_목록.md", "PLAN.md
|
||||
|
||||
# 계산 목록 — TS · 파이썬 짝 · Node 묶음 · 실행 자리
|
||||
|
||||
- Node 묶음 — sc = server-calc · co = corridor · ex = b07-extent · 다시 지음 `npm run -s build:server-calc|corridor|b07-extent`.
|
||||
- Node 묶음 — sc = server-calc · co = corridor · ex = b07-extent · dc = design-calc(등록부 입구 `A00_Common/design/logic/design_calc_node.ts` · 파이썬 `common_util_calc_runner.run_calc`) · 다시 지음 `npm run -s build:server-calc|corridor|b07-extent|design-calc`.
|
||||
- 실행 자리 — 짝 = 브라우저 즉시 TS + [저장] 정본 파이썬 · ② = 같은 TS 를 브라우저 · Node 둘 다 · 브라우저 = 입력 도구 · 표시만 · 서버 = 무거운 GIS · 추천.
|
||||
- 줄 더함 · 바꿈 = 새 계산 · 짝 · 묶음이 바뀐 커밋에서.
|
||||
|
||||
@@ -34,11 +34,11 @@ source: ["docs/raw/조사/2026-10-19_472_구조다시세움_목록.md", "PLAN.md
|
||||
| P6 | 옮긴 시설 꼭짓점 세움 | `logic/B05_Profile_Grade_Seat.ts:32` · `common_util_structure_station.ts:356` | `B05_Profile_Router_Confirm.py:220 · 306` | — | 짝 · `test_427_1_follow_seat` · `test_459_route_seat_mirror` |
|
||||
| P7 | 직선화 · 자유지정 · 쉬프트 · 시종점 | `logic/B05_Profile_UI_Profile_Straighten.ts` · `logic/design_profile_free.ts` · `logic/B05_Profile_UI_Profile_Pivot.ts` · `logic/B05_Profile_EndOffsets.ts` | — | — | 브라우저(편집값만 저장 → P2) |
|
||||
| P8 | 종단 경고(기울기 · 법정 L · 최소 R · 피복) | `B05_Profile_UI_Panel_Fields.ts` · `logic/B05_Profile_UI_Profile_Guard.ts` · `B05_Profile_UI_Profile_MinCover.ts` · 경고 글 `logic/B05_Profile_UI_Profile_Alignment.ts` buildAlignment | `B05_Profile_Engine_Grade.py` · `B05_Profile_PageSettings.py` · 경고 글 `B05_Profile_Engine_Grade_Alignment.py` build_alignment | — | 짝 · [저장] · [확정] = `B06_Section_Server_Calc_Prebuild.py` refresh_alignment 가 경고만 다시 씀(기하 그대로) · `test_442_2_grade_limit_mirror` · `test_472_save_warnings` |
|
||||
| C1 | 횡단 설계선 | `common_util_cross_design.ts:267` · `logic/B06_Section_Cross_Stage.ts:93` · `B06_Section_Cross_Refresh.ts:330` | `B06_Section_Engine_Design.py:195` | ex | 짝 · `test_b06_cross_design_mirror` |
|
||||
| C2 | 단면적(절 · 성 · 측구 · 층따기) | `common_util_cross_design_areas.ts` | `B06_Section_Engine_Areas.py` | sc · ex | 짝 · 정본 = 표준 측점 파이썬 · 구조물 측점 Node |
|
||||
| C3 | 확정 보정(포장 · 세월교 · 기울임) | `logic/B06_Section_Cross_Enforce.ts:40` | `B06_Section_Router_Design.py` · `B06_Section_Engine_Tilt.py` | — | 짝 · `test_349_0n_cross_enforce_mirror` |
|
||||
| C4 | 소단 구간(구조물 → 구간) | `B06_Section_UI_Page_Persist.ts:157`(시설 줄이 부름) | `B06_Section_Engine_Structures_Wall.py:124` | — | 짝 · `test_272_berm_source` |
|
||||
| C5 | 소단 사면 점 | `common_util_cross_berm.ts` | `common_util_cross_berm.py` | sc · co · ex | 짝 · `test_178_2_berm_fill_mirror` |
|
||||
| C1 | 횡단 설계선 | `common_util_cross_design.ts:267` · `logic/B06_Section_Cross_Stage.ts:93` · `B06_Section_Cross_Refresh.ts:330` | `B06_Section_Engine_Design.py:195`(B07 · 추천 · Route_Save · B05 확정 · 넓힘 찾기) | dc · ex | ② · [저장] · [확정] 다시 셈 = Node(`B06_Section_Engine_Design_Node.py` 측점 묶음 · 파이썬은 입력 조립) · `test_b06_cross_design_mirror` · `test_472_1_golden` 설계선 점 1e-4 |
|
||||
| C2 | 단면적(절 · 성 · 측구 · 층따기) | `common_util_cross_design_areas.ts` | `B06_Section_Engine_Areas.py` | dc · sc · ex | ② · C1 과 한 셈 · 정본 = 표준 · 구조물 측점 모두 Node |
|
||||
| C3 | 확정 보정(포장 · 세월교 · 기울임) | `logic/B06_Section_Cross_Enforce.ts:40` | `B06_Section_Router_Design_Ranges.py` · `B06_Section_Engine_Tilt.py` | dc(다시 세운 설계) | 구간 판정 짝 · `test_349_0n_cross_enforce_mirror` |
|
||||
| C4 | 소단 구간(구조물 → 구간) | `common_util_berm_spans.ts`(시설 줄 · 다시 셈이 부름) | `B06_Section_Engine_Structures_Wall.py:124`(Node 못 부를 때만) | dc | ② · `test_272_berm_source` · `test_472_3_calc_runner` |
|
||||
| C5 | 소단 사면 점 | `common_util_cross_berm.ts` | `common_util_cross_berm.py` | dc · sc · co · ex | C1 과 한 셈 · `test_178_2_berm_fill_mirror` |
|
||||
| C6 | 맹암거 얹기 · 기울임 | `common_util_structure_underdrain.ts` | `B06_Section_Engine_Tilt.py` | — | 짝 · 거울 시험 없음 |
|
||||
| C7 | 반폭 맞춤 · 성토 5 m 경고 | `logic/B06_Section_UI_Cross_Fit.ts` · `logic/B06_Section_UI_Cross_FillSlope_Warn.ts` | — | ex(Fit) | 브라우저 |
|
||||
| F1 | 시설 따라감(노선 바뀜) | `common_util_route_follow.ts:291` · `logic/B05_Profile_Pipeline_Follow.ts` · `logic/B05_Profile_Structure_Follow.ts` | `common_util_route_follow.py:120` | — | 짝 · `test_410_follow_xy` · `test_344_2_route_pairs` |
|
||||
@@ -64,7 +64,7 @@ source: ["docs/raw/조사/2026-10-19_472_구조다시세움_목록.md", "PLAN.md
|
||||
|---|---|---|---|---|
|
||||
| X1 | 단면 모양 기본값 | `"left_cut"`(`B06_Section_Cross_Refresh.ts:438`) | `default_section_modes`(`B06_Section_Router_Design.py:402`) | 사용자 협의 · 손대지 않음 |
|
||||
| X2 | 지반 표본 출처 · 입력 선 | 격자 `.bin` · `/route/plan` planned 선 | npz `sample_xy` · 저장 경로 폴리라인 | 노선 초안 없는 차례 = 정본 표본 유지로 막음 |
|
||||
| X3 | 단면적 두 갈래 | 전부 TS | 표준 측점 파이썬 · 구조물 측점 Node | 1e-4 ㎡ 차 |
|
||||
| X3 | 단면적 두 갈래 | 전부 TS | 모두 Node(472-4 ②) | 풀림 · 남은 차 = 설계선 점 반올림 동점 1e-4 m(파이썬 round 짝수 쪽 · TS 위쪽) |
|
||||
| X4 | 평면 발자국 | TS PlanFootprint | B07 파이썬 PlanSlope | 거울 시험 |
|
||||
| X5 | 경고 기준표 | `B05_Profile_UI_Panel_Fields.ts` 안 표 | `FOREST_ROAD_PROFILE_CRITERIA` | 거울 시험 · 자료 두 벌 |
|
||||
| X6 | 노선 바뀜 넘기는 칸 | `B06_Section_Station_Keys.ts:46` 근처 | `B05_Profile_Route_Save.py:275-279` CARRY_INPUT_KEYS · ROUTE_CARRY_KEYS | 손으로 맞춘 목록 둘 · 거울 없음 |
|
||||
|
||||
@@ -30,7 +30,7 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "docs/raw/
|
||||
```
|
||||
사용자 조작(끌기 · 단추 · 칸)
|
||||
▼
|
||||
① 화면 메모리 — 편집기 · 종단 EditState · 배수판 · 보기 구조물 · B06 조정 맵 · 시설 줄
|
||||
① 화면 메모리 — 편집기 · 배수판 · 보기 구조물 · B06 조정 맵 · 시설 줄
|
||||
② 상세 가게 — Section_Store cache(페이지 수명) · 정본 사본 canon(세션)
|
||||
③ 설계 가게 design_store — stations · profile · footprint 는 메모리 우선(멈춘 뒤 1 s 세션)
|
||||
④ 세션 sessionStorage — aislo:{통}:{이름}:{프로젝트}[:{노선}] · draft · result · pref(localStorage)
|
||||
@@ -47,7 +47,7 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "docs/raw/
|
||||
|
||||
| 층 | 위치(파일:줄) | 역할 · 비우는 때 |
|
||||
|---|---|---|
|
||||
| ① 화면 메모리 | `B05_Profile_UI_RouteEdit.ts:124-145` · `B05_Profile_UI_Profile_Edit.ts:137` · `B05_Profile_UI_Drainage_Panel.ts:123 · 142` · `B05_Profile_UI_Page_Structures.ts:78` · `B06_Section_UI_Page_Structures_Panel.ts:175-176` · `B06_Section_UI_Page_Link_Session.ts:47` · `design_facility_lane.ts:95-105` | 조작 즉시 · 페이지 dispose 로 버림 |
|
||||
| ① 화면 메모리 | `B05_Profile_UI_RouteEdit.ts:124-145` · `B05_Profile_UI_Drainage_Panel.ts:123 · 142` · `B05_Profile_UI_Page_Structures.ts:78` · `B06_Section_UI_Page_Structures_Panel.ts:175-176` · `B06_Section_UI_Page_Link_Session.ts:47` · `design_facility_lane.ts:95-105` | 조작 즉시 · 페이지 dispose 로 버림 |
|
||||
| ② 상세 가게 | `B06_Section_Section_Store.ts:68` cache · `:69` pending · `:117` loadSectionDetail · `:431` peekSectionDetail | 상세 횡단(살아 있는 객체) · 떠남 `forgetSectionMemory`(`:424`) · 정본 바뀜 `invalidateSectionDetail`(`:403`) |
|
||||
| ② 정본 사본 | `B06_Section_Section_Canon.ts:36` keepCanon · `:59` sessionDetail · `:72` savedDesigns · `:84` forgetCanon | 서버 정본 설계 · 자리 · 표본 · invalidate 때 비움 |
|
||||
| ③ 설계 가게 | `design_store.ts:226` useDesignStore · `:151` 메모리 칸 · `:160` 1 s | 읽고 쓰는 창구 · 쓰면 듣는 쪽 부름 |
|
||||
|
||||
@@ -35,10 +35,10 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "docs/raw/
|
||||
|
||||
| 조작 | 처음 놓이는 곳 | 브라우저 즉시 셈(모듈) | 서버 뒤 셈(API) | [저장]·[확정] 때 | 사본 | 위치(파일:줄) |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 계획고 ▲▼ | 종단 메모리 `EditState.current` → 세션 `alignment-draft` | 종단선 · 묶음 profile → cross(전 측점 횡단 · 면적) → 누가토량 · 알약 | 유토 배분(400 ms 뒤 · Node · `_derived/haul`) | `PUT /route/profile-alignment`(편집 델타) · 파이썬 종단 다시 셈 · 횡단 차례 Node 면적 · 누가토량 | 8 | `design_profile.ts:311-344` · `B05_Profile_UI_Profile_Edit.ts:124-137 · 389-400` · `logic/B05_Profile_UI_Profile_Alignment.ts:566-603` · `B05_Profile_Router.py:416-500` → `B05_Profile_Engine_Grade_Profile.py:537-575` |
|
||||
| 계획고 ▲▼ | 가게 `alignmentDraft` → 세션 `alignment-draft`(지금 값 = 초안 ?? 가게 `alignmentSaved` · 모듈 사본 없음) | 종단선 · 묶음 profile → cross(전 측점 횡단 · 면적) → 누가토량 · 알약 | 유토 배분(400 ms 뒤 · Node · `_derived/haul`) | `PUT /route/profile-alignment`(편집 델타) · 파이썬 종단 다시 셈 · 횡단 차례 Node 면적 · 누가토량 | 8 | `design_profile.ts:311-344` · `B05_Profile_UI_Profile_Edit.ts:113-258` · `logic/B05_Profile_UI_Profile_Alignment.ts:566-603` · `B05_Profile_Router.py:416-500` → `B05_Profile_Engine_Grade_Profile.py:537-575` |
|
||||
| 직선화 | ▲▼ 와 같음(R = L/Δ 로 적음) | ▲▼ 와 같음 · 실시간 띠 | 같음 | 같음 | 8 | `B05_Profile_UI_Profile_Panel_Tools.ts:133-158` · `logic/B05_Profile_UI_Profile_Straighten.ts` · `logic/B05_Profile_Profile_CurveLaw.ts:21` |
|
||||
| 자유지정 | 그리기 자체 되돌리기 → 끝나면 ▲▼ 와 같음 | 같음 | 같음 | 같음 | 8 | `logic/design_profile_free.ts:127-170 · 224-313` |
|
||||
| 쉬프트(직선 구간 올림 · 내림) | ▲▼ 와 같음 | 같음 · 붉은 강조 | 같음 | 같음 | 8 | `B05_Profile_UI_Profile_Tools.ts:111-114 · 216-217` |
|
||||
| 종단 R(L 칸) | `curve_radii` → `alignment-draft` | 같음 | 같음 | 같음 · 서버는 R 로 받음 | 8 | `B05_Profile_UI_Profile_Table.ts:360-413` · `logic/B05_Profile_UI_Profile_Alignment.ts:685-695` |
|
||||
| 시 · 종점 계획고 | `alignment-draft`(끝 측점 ▲▼) | 같음 | 같음 | 같음 | 8 | `B05_Profile_UI_Panel_Settings.ts:113-133` · `logic/B05_Profile_EndOffsets.ts:43` |
|
||||
| 페이지 설정(설계속도 · 지형 · 기준 · 최소고 강제) | 세션 `pagesettings` + result `profile-enforce-min-cover` | 최대 기울기만 종단에 얹음 · 횡단 안 셈 | — | `PUT /route/page-settings`(설정 차례) · 서버는 경고만 다시(기하 그대로) | 4 | `B05_Profile_UI_Panel.ts:130-142` · `B05_Profile_Api_PageSettings.ts:47-67` · `B05_Profile_UI_Page_Latest.ts:94-111` · `B05_Profile_PageSettings.py:79-107` |
|
||||
| 페이지 설정(설계속도 · 지형 · 기준 · 최소고 강제) | 가게 `pageSettings` → 세션 `pagesettings`(최소고 강제 = 편집 가드가 이 칸을 바로 읽음) | 최대 기울기만 종단에 얹음 · 횡단 안 셈 · ↶ ↷ 뒤 가게가 폼 · 기울기 한도 · 시작 측점을 다시 부름 | — | `PUT /route/page-settings`(설정 차례) · 서버는 경고만 다시(기하 그대로) | 4 | `B05_Profile_UI_Panel.ts:130-142` · `B05_Profile_Api_PageSettings.ts:47-85` · `B05_Profile_UI_Page_Latest.ts:92-128` · `design_profile.ts:645` · `design_view_profile.ts:355` · `B05_Profile_PageSettings.py:79-107` |
|
||||
|
||||
@@ -51,7 +51,6 @@ source: ["CLAUDE.md §5", "docs/raw/조사/2026-10-19_465_요구272_값안변함
|
||||
| `pipe_points.json` 한 [저장]에 두 번 PUT(관 · 관 옵션) | `B06_Section_Api_Culvert_Options.ts:67-95` | 코드 확인 |
|
||||
| 노선 바뀐 [저장]의 관 옵션 차례 = 옛 노선 열쇠 | `design_save.ts:187-188` | 추정 |
|
||||
| ↶ ↷ 가 세션을 직접 되씀 — B06 조정 메모리 맵 · B05 상단측 맵이 옛 값일 수 있음 | `design_store.ts:231-235` | 추정 |
|
||||
| 페이지 설정 ↶ = 세션만 · 폼 · 기울기 한도 안 얹음 · `profile-enforce-min-cover` = result 통 | `b_page_state.ts:226` | 추정 |
|
||||
| 스냅 켬 · 간격 = 되돌리기 밖 | `B05_Profile_UI_RouteEdit_SnapStep.ts:74-78` | 추정 |
|
||||
| `flushDesignStore` 부름 없음 — 멈춘 뒤 1 s 안 새로고침 = 가게 세션 사본 잃음(다시 셈) | `design_store.ts:179` | 코드 확인 |
|
||||
| 「[]」 — 되돌리기 = 값 · 떠남 확인 = 빈 칸 | `B05_Profile_UI_Profile_History.ts:50` · `design_leave.ts:41-42` | 코드 확인 |
|
||||
|
||||
@@ -40,7 +40,6 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "A00_Common
|
||||
| `section-detail` | `:232` | 상세 서버 정본 사본(브라우저 셈 안 실음) | `invalidateSectionDetail` |
|
||||
| `routeedit-initial` | `:247` | 추천 노선 | 추천 다시 셈 |
|
||||
| `design-stations` · `design-profile` · `design-footprint` | `:249-251` | 가게 메모리 칸의 세션 사본(멈춘 뒤 1 s) | 가게 칸 지움 |
|
||||
| `profile-enforce-min-cover` | `:226` | 최소고 강제(사용자 값인데 result 통) | 결과 비우기 |
|
||||
|
||||
## pref(localStorage)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "docs/raw/
|
||||
| 측점 목록 · 열쇠 | DB `cross_sections` 행 · 종단 파일 stations · 서버 `generate_sections` | 묶음 stations · 가게 `stations`(메모리) → 세션 `design-stations` · 상세 cache · 세션 `section-detail` · canon · `design_pipeline_page.ts` savedStations · `B06_Section_Station_Keys.ts:92` 목록 | TS `logic/design_pipeline_stations.ts` · `logic/B05_Profile_Sections_Stations.ts:276` ↔ 파이썬 `B05_Profile_Engine_Sections_Core.py:449` · 열쇠 `common_util_station_key.ts:32` ↔ `.py:32` · `test_344_3_pipeline_stations_mirror` · `test_344_3_station_key_mirror` | 가게 `stations` 알림 → 갈아끼움(구독 둘 · 같은 목록이면 안 함) · 조용한 차례 끝 = 상세 측점(`design_pipeline_page_publish.ts` shown) · [저장] 뒤 비움 |
|
||||
| 측점 자리 · 축 | 서버 측점 틀(저장 경로 폴리라인) | 브라우저 측점 틀(`/route/plan` planned 선) · 상세 cache frame | TS `logic/B05_Profile_UI_RouteEdit_Frame.ts:61` ↔ 파이썬 `B05_Profile_Engine_Sections_Core.py:449-519`(같은 식 · 입력 선 다름 · mm 차) · `test_b05_routeedit_frame_mirror` | 노선 초안 없는 차례 = 정본 자리 · 축 유지(`logic/B06_Section_Station_Swap.ts:238-247`) |
|
||||
| 지반 표본 | 서버 `sample_xy`(npz · 저장 노선 선) → `cross_*.json` samples | 지형 격자(`B05_Profile_Api_TerrainGrid.ts:29-31` · IndexedDB) · 묶음 ground · 상세 cache samples | TS `common_util_surface_sampler.ts:65` ↔ 파이썬 `common_util_surface_sampler.py:210` · `test_344_1_surface_sampler_mirror` | 갈아끼움 — 노선 초안 없음 = 같은 자리(0.01 m · 축 1e-3) 앞 횡단 → 그 자리 옛 횡단 → 저장 정본 표본 · 노선 초안 = 새 · 옮긴 측점만 브라우저 표본 |
|
||||
| 계획고 편집(▲▼ · 직선화 · 자유 · 쉬프트 · 시종점) | 종단 파일 `profile_alignment.edits` | `EditState` current · saved(`B05_Profile_UI_Profile_Edit.ts:137`) · 세션 `alignment-draft`(노선 · 초안 노선 0) · 묶음 입력 · 가게 `profile` → `design-profile` · 상세 `longitudinal` | TS `logic/B05_Profile_UI_Profile_Alignment.ts:401` ↔ 파이썬 `B05_Profile_Engine_Grade_Alignment.py:494` · `test_426_profile_straight` · `test_432_1_curve_law` | 커밋마다 세션 · [저장] = 서버 다시 셈 |
|
||||
| 계획고 편집(▲▼ · 직선화 · 자유 · 쉬프트 · 시종점) | 종단 파일 `profile_alignment.edits` | ★ 가게 `alignmentDraft`(세션 `alignment-draft`) · 가게 `alignmentSaved`(서버 저장분 · 메모리 · `B05_Profile_UI_Profile_Edit.ts:127 · 235`)(노선 · 초안 노선 0) · 묶음 입력 · 가게 `profile` → `design-profile` · 상세 `longitudinal` | TS `logic/B05_Profile_UI_Profile_Alignment.ts:401` ↔ 파이썬 `B05_Profile_Engine_Grade_Alignment.py:494` · `test_426_profile_straight` · `test_432_1_curve_law` | 커밋마다 세션 · [저장] = 서버 다시 셈 |
|
||||
| 종단 R | 같은 `edits.curve_radii` | 위와 같음 · 칸은 L · 저장은 R | TS `logic/B05_Profile_Profile_CurveLaw.ts` ↔ `B05_Profile_Engine_Grade_CurveLaw.py` · `test_432_1_curve_law` | 위와 같음 · R = L/Δ 로 적어 Δ 바뀌면 L 흐름 |
|
||||
| 계획선 꼭짓점(시설 측점) | 종단 파일 base PVI | 묶음 planline · 종단도 | TS `common_util_structure_station.ts:356` followMovedNodes ↔ 파이썬 `B05_Profile_Router_Confirm.py:306` `_follow_moved_stations` · `:220` `_seat_born_nodes` · `test_427_1_follow_seat` · `test_459_route_seat_mirror` | 짝 있는 옮긴 측점 = 꼭짓점 따라감 · 짝 없는 새 측점 = 안 세움 |
|
||||
| 횡단 입력(카드 · 조정) | DB `cross_sections.design` 패치 칸 | 세션 칸(`crossdesign` · 조정 맵 등) · B06 조정 메모리 맵(`B06_Section_UI_Page_Link_Session.ts:47`) | 입력 도구(셈 없음) | 조작마다 세션 · [저장] 횡단 패치 |
|
||||
@@ -28,7 +28,7 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "docs/raw/
|
||||
| 관 | `pipe_points.json` | 배수판 둘(`B05_Profile_UI_Drainage_Panel.ts:123 · 142`) · `B06_Section_UI_Page_Structures_Panel.ts:176` · 시설 줄 `:101` · `design_view_profile.ts:116` · savedPipes · `design_pipe_ids.ts:24` · 세션 `pipes` · `pipe-ids` · `culvertopt` · `kindreset` · `pipe-option-queue` · cache culvert · 유역 pkl | 관 길이 TS `design_structure_pipe_calc.ts` · `B06_Section_Structure_Layouts.ts:95`(Node 한 벌) · `test_b06_pipe_length` | 시설 줄 알림 · 배수 다시 나눔 응답 · [저장] 뒤 다시 받음 |
|
||||
| 추가 측점 | `plus_stations.json` | `B05_Profile_Api_PlusStations.ts:14 · 74 · 76` · 세션 `plus-stations` | 따라감 TS `followPlusDraft` ↔ 파이썬 `B05_Profile_Plus_Stations.py:82` · `test_459_route_seat_mirror` | 노선 초안 follow 뒤 · [저장] |
|
||||
| 소단 구간 · 깊이 | 구간 = `structures.json` · 깊이 = DB 패치 | 세션 `berm`(시설 줄만 씀) · `bermdepth` · cache `design.berm` · 초안 노선 `bermAt`(`design_shared.ts:248`) | TS `B06_Section_UI_Page_Persist.ts:157` bermSpansFromStructures ↔ 파이썬 `B06_Section_Engine_Structures_Wall.py:124` load_berm_spans · `test_272_berm_source` | 구조물 바뀜 → 시설 줄 `syncBerm`(같으면 안 씀) → cross 한 번 · 사본 없으면 저장분 |
|
||||
| 페이지 설정 | DB 단계 2 설정 · 종단 정책 | 세션 `pagesettings` · result `profile-enforce-min-cover` · 종단도 기울기 한도 · 폼 | 정책 파이썬 기준표 ↔ TS `B05_Profile_UI_Panel_Fields.ts` · `test_442_2_grade_limit_mirror` | 열 때 두 보기가 얹음 · [저장] 설정 차례 |
|
||||
| 페이지 설정 | DB 단계 2 설정 · 종단 정책 | ★ 가게 `pageSettings`(세션 `pagesettings`) — 폼 · 종단도 기울기 한도 · 시작 측점이 듣고 최소고 강제는 바로 읽음 | 정책 파이썬 기준표 ↔ TS `B05_Profile_UI_Panel_Fields.ts` · `test_442_2_grade_limit_mirror` | 열 때 두 보기가 얹음 · [저장] 설정 차례 |
|
||||
| 표준 횡단 | 종단 행 `standard_cross_section` | 세션 `std-cross` · `std-cross-default` · `section-context` | 서버 설계선 다시 | [전체 반영] · [저장] |
|
||||
| 3D | corridor 파일(Node) | `B05_Profile_UI_Corridor.ts:45` cache | Node `B05_Profile_Corridor_Prebuild.py` | 노선 초안 동안 낡음 표시 · [저장] 뒤 |
|
||||
| 되돌리기 사진첩 | 없음(메모리 · 탭 수명) | 사진 n 장 = 세션 draft 칸 글 | — | 조작 뒤 `record()` · 같은 사진이면 걸음 아님 |
|
||||
|
||||
@@ -24,6 +24,7 @@ globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStora
|
||||
const REAL = new Set(
|
||||
[
|
||||
"A00_Common/b_page_state.ts",
|
||||
"A00_Common/design/design_store.ts", // 페이지 설정 값 주인 = 가게 칸(472-5)
|
||||
"B05_Profile/B05_Profile_Api_Fetch.ts",
|
||||
"B05_Profile/B05_Profile_Api_PageSettings.ts",
|
||||
"B05_Profile/B05_Profile_UI_Page_Latest.ts",
|
||||
@@ -56,18 +57,20 @@ const settings = load(path.join(ROOT, "B05_Profile", "B05_Profile_Api_PageSettin
|
||||
const state = load(path.join(ROOT, "A00_Common", "b_page_state.ts"));
|
||||
const P = "P463";
|
||||
|
||||
/** 가짜 종단도 — 받은 기준을 적음. */
|
||||
/** 가짜 종단도 — 받은 기준을 적음. 최소고 강제는 편집 가드가 페이지 설정 한 칸을 바로 읽음(472-5) —
|
||||
* 화면은 늘 캐시에 둔 노선을 넘기므로(`writeRouteLatestCache` 뒤) 받은 노선을 캐시에 두고 읽음. */
|
||||
function apply(latest) {
|
||||
const got = {};
|
||||
if (latest) state.writeState("latest", latest, P);
|
||||
latestApi.applyPageSettings(
|
||||
{
|
||||
setGradeLimit: (pct) => (got.grade = pct),
|
||||
setEnforceMinCover: (on) => (got.enforce = on),
|
||||
setStationDisplay: (next) => (got.display = next),
|
||||
},
|
||||
P,
|
||||
latest,
|
||||
);
|
||||
if (Object.keys(got).length) got.enforce = settings.enforceMinCoverOf(P);
|
||||
return got;
|
||||
}
|
||||
const latestOf = (params) => ({ project_id: P, surface_params: {}, route_params: params });
|
||||
|
||||
@@ -78,6 +78,18 @@ async function run() {
|
||||
route_params: { options: { grade_class: "trunk", design_speed_kph: 20 }, max_grade_pct: 15 },
|
||||
};
|
||||
const api = load(path.join(ROOT, "B05_Profile", "B05_Profile_Api_PageSettings.ts"), {
|
||||
// 값 주인 = 설계 가게 칸(472-5) — 같은 세션 흉내 위에
|
||||
"../A00_Common/design/design_store": {
|
||||
useDesignStore: (p) => ({
|
||||
get: () => (session.has(`pagesettings:${p}`) ? session.get(`pagesettings:${p}`) : null),
|
||||
set: (_key, value) =>
|
||||
value === null
|
||||
? session.delete(`pagesettings:${p}`)
|
||||
: session.set(`pagesettings:${p}`, value),
|
||||
}),
|
||||
draftProjectId: () => "p1",
|
||||
},
|
||||
"./B05_Profile_UI_Profile_History": { onPageHistory: () => () => undefined },
|
||||
"../A00_Common/b_page_state": {
|
||||
readState: (name, p) => (session.has(`${name}:${p}`) ? session.get(`${name}:${p}`) : null),
|
||||
writeState: (name, value, p) => session.set(`${name}:${p}`, value),
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/* 472-4 C6 거울 헬퍼 — 맹암거 얹기 TS 짝(vite ssr) · test_472_4_underdrain_mirror.py 가 JSON 으로 받음. */
|
||||
export {
|
||||
attachUnderdrainSpecs,
|
||||
underdrainSpecsFrom,
|
||||
} from "../../common_util/common_util_structure_underdrain";
|
||||
@@ -0,0 +1,119 @@
|
||||
/* 472-5 5-C 헬퍼 — 종단 편집 · 페이지 설정 값 주인 = 설계 가게 칸 하나. 진짜 `b_page_state` · `design_store` ·
|
||||
* `_Profile_Edit` · `_Api_PageSettings` 를 Node 로(세션 = 메모리 · 되돌리기 = 세션 칸 되쓰기 + 훅 부름 흉내).
|
||||
* node helper_472_5_value_owner.cjs (짝 = `test_472_5_value_owner.py`)
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||||
|
||||
function memoryStorage() {
|
||||
const map = new Map();
|
||||
return {
|
||||
getItem: (k) => (map.has(k) ? map.get(k) : null),
|
||||
setItem: (k, v) => map.set(k, String(v)),
|
||||
removeItem: (k) => map.delete(k),
|
||||
key: (i) => [...map.keys()][i] ?? null,
|
||||
get length() {
|
||||
return map.size;
|
||||
},
|
||||
};
|
||||
}
|
||||
globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStorage() };
|
||||
|
||||
const REAL = new Set(
|
||||
[
|
||||
"A00_Common/b_page_state.ts",
|
||||
"A00_Common/design/design_store.ts",
|
||||
"B05_Profile/B05_Profile_UI_Profile_Edit.ts",
|
||||
"B05_Profile/B05_Profile_Api_PageSettings.ts",
|
||||
"B05_Profile/logic/B05_Profile_UI_Profile_Alignment.ts",
|
||||
].map((rel) => path.join(ROOT, rel)),
|
||||
);
|
||||
const historyHooks = [];
|
||||
const STUBS = {
|
||||
B05_Profile_UI_Profile_History: { onPageHistory: (entry) => historyHooks.push(entry) },
|
||||
B05_Profile_Api_Fetch: {
|
||||
readRouteLatestCache: (p) => state().readState("latest", p),
|
||||
writeRouteLatestCache: (p, value) => state().writeState("latest", value, p),
|
||||
savePageSettings: async () => ({}),
|
||||
},
|
||||
};
|
||||
const anything = () =>
|
||||
new Proxy({}, { get: (_target, key) => (key === "__esModule" ? false : () => undefined) });
|
||||
const cache = new Map();
|
||||
function load(file) {
|
||||
if (cache.has(file)) return cache.get(file);
|
||||
const box = { exports: {} };
|
||||
cache.set(file, box.exports);
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const req = (request) => {
|
||||
const stub = STUBS[path.basename(request)];
|
||||
if (stub) return stub;
|
||||
if (request === "@ui/ui_template_locale") return { ui_locales: {}, currentLanguageIndex: 0 };
|
||||
if (!request.startsWith(".")) return anything();
|
||||
const target = path.resolve(path.dirname(file), request);
|
||||
const full = fs.existsSync(`${target}.ts`) ? `${target}.ts` : target;
|
||||
return REAL.has(full) ? load(full) : anything();
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, req);
|
||||
return box.exports;
|
||||
}
|
||||
const state = () => load(path.join(ROOT, "A00_Common", "b_page_state.ts"));
|
||||
|
||||
const P = "P4725";
|
||||
const ROUTE = 7;
|
||||
const store = load(path.join(ROOT, "A00_Common", "design", "design_store.ts"));
|
||||
store.pinDraftProject(P);
|
||||
const edit = load(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Edit.ts"));
|
||||
const settings = load(path.join(ROOT, "B05_Profile", "B05_Profile_Api_PageSettings.ts"));
|
||||
const out = {};
|
||||
|
||||
// ① 두 보기 = 창구 둘 · 같은 노선 — ↶ 가 세션 칸을 되쓰면 둘 다 그 값(모듈 사본이 옛 값을 쥐지 않음)
|
||||
const saved = { station_offsets: {}, curve_radii: { "40.000": 30 } };
|
||||
const a = edit.createProfileEditStore(ROUTE, saved, () => undefined);
|
||||
const b = edit.createProfileEditStore(ROUTE, saved, () => undefined);
|
||||
const x = { station_offsets: { "20.000": 0.5 }, curve_radii: { "40.000": 30 } };
|
||||
const y = { station_offsets: { "20.000": 0.2 }, curve_radii: { "40.000": 30 } };
|
||||
a.replace(x);
|
||||
state().writeState("alignment-draft", y, P, ROUTE); // 되돌리기 = 세션 되쓰기
|
||||
out.undo = {
|
||||
a: a.edits(),
|
||||
b: b.edits(),
|
||||
aDirty: a.dirty(),
|
||||
resync: a.resync?.() ?? "없음",
|
||||
again: a.resync?.() ?? "없음",
|
||||
};
|
||||
// ② [저장] 뒤 ↶ 로 초안 칸이 비면 = 저장한 값(저장 전 서버 값으로 안 돌아감)
|
||||
a.replace(x);
|
||||
a.markSaved();
|
||||
state().writeState("alignment-draft", null, P, ROUTE);
|
||||
out.afterSave = { edits: b.edits(), dirty: b.dirty(), canRestore: b.canRestoreSaved() };
|
||||
|
||||
// ③ 페이지 설정 — ↶ 훅이 가게로 다시 알림(폼 · 기울기 한도 · 시작 측점이 들음 · N6)
|
||||
const heard = [];
|
||||
store.useDesignStore(P, null).subscribe("pageSettings", (value) => heard.push(value));
|
||||
const draft = {
|
||||
design_speed_kph: 30,
|
||||
terrain_type: "normal",
|
||||
max_grade_pct: 12,
|
||||
min_vertical_radius_m: null,
|
||||
min_tangent_length_m: null,
|
||||
enforce_pipe_clearance: true,
|
||||
station_display: { station: 0, cumulative: 0 },
|
||||
};
|
||||
settings.writePageSettings(P, draft);
|
||||
state().writeState("pagesettings", null, P); // 되돌리기 = 세션 되쓰기
|
||||
historyHooks.forEach((entry) => entry.restore?.("undo"));
|
||||
out.settingsHeard = [...heard];
|
||||
|
||||
// ④ 최소고 강제 = 페이지 설정 한 칸(초안 ?? 저장 노선) · 사본 세션 칸 없음(N7)
|
||||
state().writeState("latest", { project_id: P, route_params: { enforce_pipe_clearance: true } }, P);
|
||||
out.enforceSaved = settings.enforceMinCoverOf?.(P) ?? "없음"; // 옛 코드 = 창구 없음
|
||||
settings.writePageSettings(P, { ...draft, enforce_pipe_clearance: false });
|
||||
out.enforceDraft = settings.enforceMinCoverOf?.(P) ?? "없음";
|
||||
// 등록부에 없으면 열쇠가 안 섬(null) — 옛 코드는 결과 통 열쇠가 섬
|
||||
out.copyKey = state().stateKey("profile-enforce-min-cover", P) ?? "none";
|
||||
console.log(JSON.stringify(out));
|
||||
@@ -42,8 +42,11 @@ def test_글은_재계산에서도_남는다(monkeypatch):
|
||||
monkeypatch.setattr(design_mod, "ford_surface_drops", lambda root: [])
|
||||
monkeypatch.setattr(design_mod, "design_elevation_from_longitudinal", lambda lon, ch: 100.0)
|
||||
monkeypatch.setattr(design_mod, "curve_widening_args", lambda section: {})
|
||||
# 472-4 ② — [저장] 다시 셈은 측점 묶음(Node) 한 번 · 그 자리를 갈아 끼움
|
||||
monkeypatch.setattr(
|
||||
design_mod, "compute_cross_design", lambda samples, elev, **kw: {"paved": kw["paved"]}
|
||||
design_mod,
|
||||
"compute_cross_designs",
|
||||
lambda jobs, standard: [{"paved": kw["paved"]} for _s, _z, kw in jobs],
|
||||
)
|
||||
sections = [{"chainage_m": 40.0, "design": {"paved": False, "structure_note": "메모"}}]
|
||||
assert design_mod.enforce_pavement_ranges({}, sections, Path("."), None) == 1
|
||||
|
||||
@@ -202,7 +202,8 @@ def test_좌측_패널_칸_구간_깊이_나름():
|
||||
"""옛 구조물 옵션 depth_m → 세션 구간 → 측점 제원(기본값) · 서버 payload(같은 꼴).
|
||||
|
||||
좌측 칸은 194-1 에서 숨김 — 옮기기는 횡단도 ▲▼(`test_194_1_berm_depth`)."""
|
||||
source = (ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts").read_text(encoding="utf-8")
|
||||
# 소단 구간 셈은 472-4 C4 에서 `common_util_berm_spans.ts` 로 옮김
|
||||
source = (ROOT / "common_util" / "common_util_berm_spans.ts").read_text(encoding="utf-8")
|
||||
assert "options.depth_m" in source and "Math.max(rawDepth, 0)" in source
|
||||
refresh = (ROOT / "B06_Section" / "B06_Section_Cross_Refresh.ts").read_text(encoding="utf-8")
|
||||
assert "depthM: spec.depth_m" in refresh
|
||||
|
||||
@@ -59,8 +59,11 @@ def test_CAD_칸은_재계산에서도_남는다(monkeypatch):
|
||||
monkeypatch.setattr(design_mod, "ford_surface_drops", lambda root: [])
|
||||
monkeypatch.setattr(design_mod, "design_elevation_from_longitudinal", lambda lon, ch: 100.0)
|
||||
monkeypatch.setattr(design_mod, "curve_widening_args", lambda section: {})
|
||||
# 472-4 ② — [저장] 다시 셈은 측점 묶음(Node) 한 번 · 그 자리를 갈아 끼움
|
||||
monkeypatch.setattr(
|
||||
design_mod, "compute_cross_design", lambda samples, elev, **kw: {"paved": kw["paved"]}
|
||||
design_mod,
|
||||
"compute_cross_designs",
|
||||
lambda jobs, standard: [{"paved": kw["paved"]} for _s, _z, kw in jobs],
|
||||
)
|
||||
sections = [{"chainage_m": 40.0, "design": {"paved": False, "structure_note_cad": CAD}}]
|
||||
assert design_mod.enforce_pavement_ranges({}, sections, Path("."), None) == 1
|
||||
|
||||
@@ -19,6 +19,7 @@ X3 면적 두 갈래(구조물 = 관 351 하나 · 두 길 다 Node TS 로 셈)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -131,6 +132,25 @@ def test_두_길_사이_알려진_차만(got, pair) -> None:
|
||||
assert not gone, f"안 나타난 알려진 차 {gone} — KNOWN 에서 지움"
|
||||
|
||||
|
||||
def test_저장_설계선_점_정본과_1e4_안(tmp_path) -> None:
|
||||
"""[저장] 다시 셈 설계선(472-4 ② = Node TS) — 정본과 점 수 · offset 같음 · 고도 1e-4 m 안.
|
||||
|
||||
알려진 차 = 반올림 동점(파이썬 round 짝수 쪽 · TS 위쪽) 13 점(브레인 10-19 승인).
|
||||
"""
|
||||
detail = copy.deepcopy(paths.read("detail.json"))
|
||||
before = {s["chainage_m"]: s["design"]["design_line"] for s in detail["cross_sections"]}
|
||||
sections, _ = paths._save_tail(detail, paths.stage_project(tmp_path / "project"))
|
||||
moved = 0
|
||||
for section in sections:
|
||||
old, new = before[section["chainage_m"]], section["design"]["design_line"]
|
||||
assert [p["offset_m"] for p in new] == [p["offset_m"] for p in old], section["chainage_m"]
|
||||
for a, b in zip(old, new, strict=True):
|
||||
gap = abs(a["elevation_m"] - b["elevation_m"])
|
||||
assert gap <= 1e-4 + 1e-9, (section["chainage_m"], a, b)
|
||||
moved += gap > 0
|
||||
assert moved == 13, f"반올림 동점 점 수 {moved} — 알려진 차(13)와 다름"
|
||||
|
||||
|
||||
def test_화면_참값() -> None:
|
||||
stored = GOLDEN["stored"]
|
||||
assert stored["final_cumulative_m3"] == -17570.7
|
||||
|
||||
@@ -43,6 +43,8 @@ NODE_IDS = {
|
||||
"profile.seat",
|
||||
"facility.plus_follow",
|
||||
"facility.follow",
|
||||
"cross.design",
|
||||
"cross.berm_spans",
|
||||
}
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
@@ -199,7 +201,7 @@ def test_id_마다_실행기_있음(loaded) -> None:
|
||||
if row["site"] == "browser_only":
|
||||
ok = kind == "browser" and _has_name(ref)
|
||||
elif kind == "node":
|
||||
ok = ref == row["id"] and ref in NODE_IDS
|
||||
ok = ref in NODE_IDS # 한 셈에서 나오는 값(면적 · 소단 사면)은 그 셈의 id
|
||||
else:
|
||||
ok = kind == "py" and _has_name(ref)
|
||||
ok = ok and ("until" not in row or kind == "py")
|
||||
@@ -208,8 +210,10 @@ def test_id_마다_실행기_있음(loaded) -> None:
|
||||
if not ok:
|
||||
bad.append(row["id"])
|
||||
assert bad == []
|
||||
node_rows = {row["id"] for row in loaded["registry"] if row["runner"].startswith("node:")}
|
||||
assert node_rows == NODE_IDS
|
||||
node_runs = {
|
||||
row["runner"][5:] for row in loaded["registry"] if row["runner"].startswith("node:")
|
||||
}
|
||||
assert node_runs == NODE_IDS
|
||||
|
||||
|
||||
def test_묶음_STAGE_DEPS_등록부_입력과_같음(loaded) -> None:
|
||||
|
||||
@@ -68,3 +68,36 @@ def test_실행기_값_server_calc_와_같음() -> None:
|
||||
def test_Node_실행기_아닌_id_는_ValueError() -> None:
|
||||
with pytest.raises(ValueError, match="stations.list"):
|
||||
runner.run_calc(["masshaul.cumulative", "stations.list"], {})
|
||||
|
||||
|
||||
# ── 472-4 ② — 횡단 설계선 · 소단 구간 Node 길 ──────────────────────────────────
|
||||
|
||||
|
||||
def test_소단_구간_Node_와_파이썬_짝_같음(tmp_path, monkeypatch) -> None:
|
||||
from test_272_berm_source import STRUCTURE_CASES, _project
|
||||
|
||||
from B06_Section import B06_Section_Engine_Design_Node as node_mod
|
||||
from B06_Section.B06_Section_Engine_Structures_Wall import berm_span_at, load_berm_spans
|
||||
|
||||
calls = []
|
||||
real = node_mod.run_calc
|
||||
monkeypatch.setattr(
|
||||
node_mod, "run_calc", lambda ids, inputs: calls.append(ids) or real(ids, inputs)
|
||||
)
|
||||
chainages = [0.0, 5.0, 50.0, 65.0, 80.0, 100.0, 150.0, 250.0]
|
||||
for index, structures in enumerate(STRUCTURE_CASES):
|
||||
root = _project(tmp_path / f"c{index}", structures)
|
||||
got = node_mod.berm_specs_at(root, chainages)
|
||||
spans = load_berm_spans(root)
|
||||
want = [berm_span_at(spans, c) for c in chainages]
|
||||
assert got == [pytest.approx(w) if w else w for w in want], index
|
||||
assert len(calls) == len(STRUCTURE_CASES) # 파이썬 물러섬 아님
|
||||
|
||||
|
||||
def test_횡단_설계_못_세운_측점은_None() -> None:
|
||||
from B06_Section.B06_Section_Engine_Design_Node import compute_cross_designs
|
||||
|
||||
ground = [{"offset_m": o, "elevation_m": 100.0 + 0.3 * o} for o in range(-20, 21)]
|
||||
options = {"ground_type": "soil", "section_mode": "left_cut"}
|
||||
got = compute_cross_designs([(ground, 100.0, options), (ground, None, options)], None)
|
||||
assert got[0]["cut_area_m2"] > 0 and got[1] is None
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""PLAN 472-4 ② C6 (요구 273) — 맹암거 얹기 거울: TS `common_util_structure_underdrain` ↔ 파이썬
|
||||
`B06_Section_Engine_Tilt.underdrains_from` · `attach_underdrain_specs`.
|
||||
|
||||
못박는 것
|
||||
- 구조물 목록 → 제원(높이 · 경사 · 빈 값 · 0 · 음수 · 글 · 맹암거 아닌 종류) 두 쪽 같음.
|
||||
- 기준 측점(±0.02 m) 얹기 · 얹은 수 같음.
|
||||
- 알려진 차 하나 — 옛 `underdrain` 이 남은 측점: TS 는 걷음 · 파이썬은 둠
|
||||
(서버는 파일에서 새로 읽어 옛 값 없음 · Node 로 옮길 때 한 벌).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_472_4_underdrain_mirror.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance
|
||||
from B06_Section.B06_Section_Engine_Tilt import attach_underdrain_specs, underdrains_from
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "vite").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def _drain(at: float, **options) -> dict:
|
||||
return {
|
||||
"type_id": "cross_underdrain",
|
||||
"placement": "point",
|
||||
"chainage_m": at,
|
||||
"options": options,
|
||||
}
|
||||
|
||||
|
||||
STRUCTURES = [
|
||||
_drain(40.0, culvert_height_m=1.2, culvert_slope_pct=3),
|
||||
_drain(80.0, culvert_height_m="", culvert_slope_pct="2.5"),
|
||||
_drain(120.0, culvert_height_m=0, culvert_slope_pct=-1),
|
||||
_drain(160.0),
|
||||
{"type_id": "berm", "placement": "interval", "start_m": 0.0, "end_m": 50.0, "options": {}},
|
||||
]
|
||||
SECTIONS = [
|
||||
{"chainage_m": 40.01},
|
||||
{"chainage_m": 79.99},
|
||||
{"chainage_m": 120.0},
|
||||
{"chainage_m": 160.03}, # 0.02 밖
|
||||
{"chainage_m": 200.0, "underdrain": {"chainage_m": 200.0}}, # 옛 값 — 알려진 차
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ts(tmp_path_factory) -> dict:
|
||||
work = tmp_path_factory.mktemp("u4724")
|
||||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
[
|
||||
"node",
|
||||
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
"../resources/tester/helper_472_4_underdrain_entry.ts",
|
||||
"--outDir",
|
||||
str(work),
|
||||
"--logLevel",
|
||||
"error",
|
||||
],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
timeout=300,
|
||||
)
|
||||
bundle = work / "bundle.mjs"
|
||||
shutil.copy(work / "helper_472_4_underdrain_entry.js", bundle)
|
||||
job = json.dumps({"structures": STRUCTURES, "sections": SECTIONS})
|
||||
script = (
|
||||
f"const m = await import({json.dumps(bundle.as_uri())});"
|
||||
f"const job = {job};"
|
||||
"const specs = m.underdrainSpecsFrom(job.structures);"
|
||||
"const count = m.attachUnderdrainSpecs(job.sections, specs);"
|
||||
"console.log(JSON.stringify({specs, count, sections: job.sections}));"
|
||||
)
|
||||
out = subprocess.run( # noqa: S603
|
||||
["node", "--input-type=module", "-e", script],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
return json.loads(out.stdout)
|
||||
|
||||
|
||||
def _python() -> tuple[list[dict], int, list[dict]]:
|
||||
specs = underdrains_from([StructureInstance(**item) for item in STRUCTURES])
|
||||
sections = copy.deepcopy(SECTIONS)
|
||||
return specs, attach_underdrain_specs(sections, specs), sections
|
||||
|
||||
|
||||
def test_제원_같음(ts) -> None:
|
||||
specs, _count, _sections = _python()
|
||||
assert ts["specs"] == specs
|
||||
|
||||
|
||||
def test_얹기_같음_알려진_차_하나(ts) -> None:
|
||||
_specs, count, sections = _python()
|
||||
assert ts["count"] == count == 3
|
||||
assert ts["sections"][:4] == sections[:4]
|
||||
# 알려진 차 — 옛 underdrain: TS 걷음 · 파이썬 둠
|
||||
assert "underdrain" not in ts["sections"][4] and "underdrain" in sections[4]
|
||||
@@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""472-5 5-C — 종단 편집 · 페이지 설정 값 주인 = 설계 가게 칸 하나.
|
||||
|
||||
옛 흠: 종단 편집은 모듈 사본(EditState)이 지금 값을 쥐어 ↶ 뒤 다른 창구가 옛 값 · [저장] 뒤 ↶ 가 저장 전
|
||||
서버 값으로 돌아감 · 페이지 설정 ↶ 는 세션 칸만 돌아오고 폼 · 기울기 한도는 안 들음(N6) · 최소고 강제는
|
||||
결과 통 사본 칸(N7).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = ROOT / "resources" / "tester" / "helper_472_5_value_owner.cjs"
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def got() -> dict:
|
||||
run = subprocess.run(
|
||||
["node", str(HELPER)], capture_output=True, text=True, encoding="utf-8", timeout=120
|
||||
)
|
||||
assert run.returncode == 0, run.stderr
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
Y = {"station_offsets": {"20.000": 0.2}, "curve_radii": {"40.000": 30}}
|
||||
X = {"station_offsets": {"20.000": 0.5}, "curve_radii": {"40.000": 30}}
|
||||
|
||||
|
||||
def test_되돌리면_두_창구가_같은_값(got):
|
||||
assert got["undo"]["a"] == Y
|
||||
assert got["undo"]["b"] == Y
|
||||
assert got["undo"]["aDirty"] is True
|
||||
assert got["undo"]["resync"] is True # 다시 셈
|
||||
assert got["undo"]["again"] is False # 같은 값 = 다시 셈 예약 없음
|
||||
|
||||
|
||||
def test_저장_뒤_되돌려도_저장한_값(got):
|
||||
assert got["afterSave"] == {"edits": X, "dirty": False, "canRestore": False}
|
||||
|
||||
|
||||
def test_페이지_설정_되돌리면_가게가_알림(got):
|
||||
assert got["settingsHeard"][-1] is None # ↶ 로 초안이 빔 → 듣는 쪽이 저장 설정으로 다시 얹음
|
||||
|
||||
|
||||
def test_최소고_강제는_페이지_설정_한_칸(got):
|
||||
assert got["enforceSaved"] is True
|
||||
assert got["enforceDraft"] is False
|
||||
assert got["copyKey"] == "none"
|
||||
@@ -76,10 +76,9 @@ def test_source_guard_sits_on_the_single_apply_path():
|
||||
방향키가 그냥 지나갔다. 판정 로직을 `_Profile_MinCover.blocksMinCover` 로 옮기고
|
||||
`_Profile_Panel.applyEdits` 한 곳에서만 부른다 — 화면 쪽에는 사본을 두지 않는다.
|
||||
"""
|
||||
assert (
|
||||
"if (blocksMinCover(base, alignment, candidate, enforceMinCover, minCoverTargets)) return false;"
|
||||
in PANEL
|
||||
)
|
||||
# 강제 여부 = 페이지 설정 한 칸을 그 자리에서 읽음(472-5 · 보기마다 사본 없음)
|
||||
assert "const enforce = enforceMinCoverOf(projectId);" in PANEL
|
||||
assert "if (blocksMinCover(base, alignment, candidate, enforce, minCoverTargets)) return false;" in PANEL
|
||||
assert PANEL.count("blocksMinCover(") == 1 # 부르는 자리는 한 곳뿐.
|
||||
assert "export function blocksMinCover(" in GUARD
|
||||
assert "blocksMinCover" not in RENDER # 화면 모듈에 사본이 남지 않았다.
|
||||
|
||||
@@ -31,6 +31,8 @@ TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||||
|
||||
# 값이 소수점 아래에서 갈리는지 보려면 자릿수를 그대로 비교해야 한다. 두 쪽 다 반올림해
|
||||
# 내보내므로 완전 일치를 기대하되, 부동소수 마지막 자리만 어긋나는 것은 허용한다.
|
||||
# 알려진 차(472-4 ②) — 반올림 동점(…5)에서 파이썬 round 는 짝수 쪽 · TS 는 위쪽이라 설계선 점이
|
||||
# 1e-4 m 갈릴 수 있음(이 시험 입력엔 없음 · [저장] 정본은 Node TS 쪽 · test_472_1_golden).
|
||||
_TOLERANCE = 1e-9
|
||||
|
||||
|
||||
|
||||
@@ -146,8 +146,11 @@ def test_enforce_turns_paved_on_and_off(monkeypatch):
|
||||
monkeypatch.setattr(design_mod, "ford_surface_drops", lambda root: [])
|
||||
monkeypatch.setattr(design_mod, "design_elevation_from_longitudinal", lambda lon, ch: 100.0)
|
||||
monkeypatch.setattr(design_mod, "curve_widening_args", lambda section: {})
|
||||
# 472-4 ② — [저장] 다시 셈은 측점 묶음(Node) 한 번 · 그 자리를 갈아 끼움
|
||||
monkeypatch.setattr(
|
||||
design_mod, "compute_cross_design", lambda samples, elev, **kw: {"paved": kw["paved"]}
|
||||
design_mod,
|
||||
"compute_cross_designs",
|
||||
lambda jobs, standard: [{"paved": kw["paved"]} for _s, _z, kw in jobs],
|
||||
)
|
||||
sections = [
|
||||
{"chainage_m": 20.0, "design": {"paved": False}}, # 범위 안 · 끔 → 켬
|
||||
|
||||
@@ -80,7 +80,12 @@ def _install_fakes(monkeypatch) -> dict:
|
||||
}
|
||||
|
||||
monkeypatch.setattr(design_mod, "pavement_ranges", lambda root: [(0.0, 200.0)])
|
||||
monkeypatch.setattr(design_mod, "compute_cross_design", fake_compute)
|
||||
# 472-4 ② — [저장] 다시 셈은 측점 묶음(Node) 한 번 · 그 자리를 갈아 끼움
|
||||
monkeypatch.setattr(
|
||||
design_mod,
|
||||
"compute_cross_designs",
|
||||
lambda jobs, standard: [fake_compute(s, z, standard=standard, **kw) for s, z, kw in jobs],
|
||||
)
|
||||
monkeypatch.setattr(design_mod, "design_elevation_from_longitudinal", lambda lon, ch: 100.0)
|
||||
monkeypatch.setattr(design_mod, "curve_widening_args", lambda section: {})
|
||||
return seen
|
||||
|
||||
@@ -120,7 +120,12 @@ def _fake_engine(monkeypatch):
|
||||
return {"paved": True, "cut_area_m2": 1.0, "fill_area_m2": 2.0}
|
||||
|
||||
monkeypatch.setattr(design_mod, "pavement_ranges", lambda root: [(0.0, 200.0)])
|
||||
monkeypatch.setattr(design_mod, "compute_cross_design", fake_compute)
|
||||
# 472-4 ② — [저장] 다시 셈은 측점 묶음(Node) 한 번 · 그 자리를 갈아 끼움
|
||||
monkeypatch.setattr(
|
||||
design_mod,
|
||||
"compute_cross_designs",
|
||||
lambda jobs, standard: [fake_compute(s, z, standard=standard, **kw) for s, z, kw in jobs],
|
||||
)
|
||||
monkeypatch.setattr(design_mod, "design_elevation_from_longitudinal", lambda lon, ch: 100.0)
|
||||
monkeypatch.setattr(design_mod, "curve_widening_args", lambda section: {})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user