diff --git a/A00_Common/b_page_state.ts b/A00_Common/b_page_state.ts index 38c3a5045..93edcdb56 100644 --- a/A00_Common/b_page_state.ts +++ b/A00_Common/b_page_state.ts @@ -235,9 +235,6 @@ export const STATE_REGISTRY = { "design-stations": { bucket: "result", scope: "project" }, "design-profile": { bucket: "result", scope: "project" }, "design-footprint": { bucket: "result", scope: "project" }, - /** 소단 사본 — 구조물(소단)에서 셈한 구간 목록(`design_facility_lane` 만 씀 · 고침은 `structures` 칸). - * 다시 셈에 실어 보냄 · 안 실으면 계획선을 고치는 순간 계단이 사라짐(3-9 · 477-4). */ - berm: { bucket: "result", scope: "route" }, } as const satisfies Record; export type StateName = keyof typeof STATE_REGISTRY; diff --git a/A00_Common/design/design_calc_server.ts b/A00_Common/design/design_calc_server.ts index c0fc466a2..ad4f12f6a 100644 --- a/A00_Common/design/design_calc_server.ts +++ b/A00_Common/design/design_calc_server.ts @@ -142,18 +142,12 @@ export interface CalcRequest { pick: (results: Record) => unknown; } -/** F7 구조물 면적 · C4 소단 구간 — 등록부 `node:` 입력 그대로. */ +/** F7 구조물 면적 — 등록부 `node:` 입력 그대로. */ export const areasRequest = (rows: unknown[]): CalcRequest => ({ run: ["structure.layouts"], inputs: { detail: { cross_sections: rows } }, pick: (results) => (results["structure.layouts"] as { areas: unknown }).areas, }); -export const bermRequest = (structures: unknown[]): CalcRequest => ({ - run: ["cross.berm_spans"], - inputs: { berm: { structures, chainages: [] } }, - pick: (results) => (results["cross.berm_spans"] as { spans: unknown }).spans, -}); - export const trialRequest = (id: string, args: unknown[]): CalcRequest => ({ run: [id], inputs: { args: { [id]: args } }, diff --git a/A00_Common/design/design_facility_lane.ts b/A00_Common/design/design_facility_lane.ts index 5c9bf6970..95c234d69 100644 --- a/A00_Common/design/design_facility_lane.ts +++ b/A00_Common/design/design_facility_lane.ts @@ -8,20 +8,10 @@ * 입력 = 관 알림(배수유역도 · B06 관 목록) · 구조물(보기 고침 · 저장분) · 종류 표 · 측점 간격 · 추가 측점(가게) * 관 id 장부 = 관 고정 id 가 쥔 세션 id 를 그 자리로(411-B) · 헛 id 정리(411-E) — 어느 보기에서 고쳐도 * 계획고 편집 옮김(사라진 측점 → 새 측점 · 213-2) = 사용자 고침만 · 노선 따라감(`follow`)은 안 옮김(계산 묶음 몫 · 349-0d) - * 소단 사본(세션 `berm`) = 구조물 소단 구간 — 쓰는 자리 여기 하나(B05 만 열어도 · 같으면 안 씀 · 쓰면 cross 한 번 · 467) * ========================================================================== */ import { useDesignStore } from "./design_store"; -import { pageRouteId } from "./design_page_route"; -import { readState } from "../b_page_state"; -import { - bermSpansFromStructures, - writeBermSpans, - type BermSpan, -} from "../../B06_Section/B06_Section_UI_Page_Persist"; import { pipeIdAt, placePipeIds, prunePipeIds, withPipeIds } from "./design_pipe_ids"; -import { calcOnServerNow } from "./design_calc_sites"; -import { bermRequest, calcChecked, calcOnServerOnly } from "./design_calc_server"; import { plannedIrregular } from "./design_structure_status"; import { planStations } from "../../common_util/common_util_structure_station"; import { followPipeReports } from "../../B05_Profile/logic/B05_Profile_Pipeline_Follow"; @@ -99,7 +89,7 @@ export function createFacilityLane( let types: StructureType[] = []; let stored: StructureInstance[] | null = null; let structures: StructureInstance[] = []; - let structuresKnown = false; // 구조물을 받기 전 = 소단 사본 안 씀(빈 목록으로 저장분 소단을 지우지 않게) + let structuresKnown = false; // 구조물을 받기 전 = 측점 계획 모름(빈 목록으로 저장 시설 측점을 지우지 않게) /** 마지막 관 알림 — 노선 따라감 · 관 초안이 바뀌면 이것을 초안 측점으로 다시 맞춤. */ let reported: LanePipe[] | null = null; let pipes: LanePipe[] = []; @@ -152,36 +142,6 @@ export function createFacilityLane( const marks = (): StructureInstance[] => [...structures, ...pipesToStructureMarks(pipes)]; - /** 소단 사본 = 지금 구조물의 소단 구간(467 · 사본 없는 동안 다시 셈 = 저장분 `design.berm`) · 같으면 안 씀 · - * 쓰면 저장 노선 cross 한 번 더(같은 값 측점은 다시 셈 기억이 건너뜀 · 묶음은 늦게 불러옴 · 순환 없음). */ - function syncBerm(): void { - const routeId = pageRouteId(projectId); - if (!structuresKnown || routeId === null) return; - // 계산 자리 quantity = server 면 잠잠 뒤 서버 값만(511-A) · 아니면 브라우저 셈 - if (calcOnServerNow("quantity")) { - void calcOnServerOnly("quantity", "C4.copy", bermRequest(structures)).then( - (next) => next && putBerm(routeId, next), - ); - return; - } - putBerm( - routeId, - calcChecked("quantity", "cross.berm_spans", bermSpansFromStructures, [structures]), - ); - } - - function putBerm(routeId: number, next: BermSpan[]): void { - const copy = readState("berm", projectId, routeId); - if (Array.isArray(copy) && JSON.stringify(copy) === JSON.stringify(next)) return; - writeBermSpans(projectId, routeId, next); - void Promise.all([import("./design_pipeline_cross"), import("./design_pipeline_page")]) - .then(([cross, page]) => { - cross.forceCross(); - page.pipelineCross(projectId); - }) - .catch(() => undefined); - } - /** 측점 계획 입력이 다 옴 — 모름(관 알림 전 · 구조물 받기 전)을 빈 목록으로 셈하면 종단도가 저장 시설 측점 * (관 등)을 뺀 선으로 횡단을 다시 셈 · 유토 배지가 잠깐 틀림(473 · 요구 272). */ const known = (): boolean => types.length > 0 && reported !== null && structuresKnown; @@ -234,14 +194,12 @@ export function createFacilityLane( } if (next.pipes) reported = [...next.pipes]; refresh(carryEdits); - if (next.structures) run(syncBerm); }, follow() { const drafted = store.get("structures"); structures = drafted ?? stored ?? []; structuresKnown ||= drafted !== null || stored !== null; refresh(false); - run(syncBerm); }, setIntervalM(next) { if (next > 0 && next !== intervalM) { diff --git a/A00_Common/design/design_pipeline_page.ts b/A00_Common/design/design_pipeline_page.ts index f34f77075..53b666f76 100644 --- a/A00_Common/design/design_pipeline_page.ts +++ b/A00_Common/design/design_pipeline_page.ts @@ -338,14 +338,13 @@ function createPagePipeline( groundPvi: groundPviAt, // 계산 자리 「서버」 = 서버 Node(511-B B-3) }), profile: profileAt, - // 단면 유형 = 새 지형 상단측 · 소단 = 지금 구조물(365-5) · 저장 노선 = 다시 셈 창구 한 벌(365-4) · 엔진은 - // 창구 출력을 앞 결과로 안 이어 씀(초안 = 새로고침) + // 단면 유형 = 상단측 · 소단 = 얹힌 제원(577-2) · 저장 노선 = 다시 셈 창구(365-4) · 엔진은 창구 출력 안 이어 씀 cross: (ctx) => { const stations = ctx.out.ground?.stations ?? []; const status = statusOf(ctx.inputs.pipes, ctx.inputs.structures); const engine = () => crossStage.stage({ - ...contextCross(ctx, status, options.bermAt?.(ctx.inputs.structures) ?? null), + ...contextCross(ctx, status), prev: ctx.prev && savedOut.has(ctx.prev) ? null : ctx.prev, }); // 계산 자리 cross = server — 엔진 길도 서버 Node 단계(같은 TS · 511-B B-4) · 못 받음 = 브라우저 diff --git a/A00_Common/design/design_pipeline_page_types.ts b/A00_Common/design/design_pipeline_page_types.ts index b783be862..1e1837bcc 100644 --- a/A00_Common/design/design_pipeline_page_types.ts +++ b/A00_Common/design/design_pipeline_page_types.ts @@ -14,7 +14,6 @@ import type { AlignmentBase, AlignmentEdits, } from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment"; -import type { BermAt } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail"; import type { SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch"; import type { DesignStoreValues } from "./design_store"; @@ -61,8 +60,6 @@ export interface PagePipelineOptions { stationKeys?: (routeId: number, detail: SectionDetailResponse) => string[]; /** 고리가 `RECONCILE_ROUNDS` 바퀴 안에 안 맞음 — 「계산 불안정」 한 줄(365-2). */ onUnstable?: () => void; - /** 소단 구간(지금 구조물 목록 → 누가거리마다 제원) — 초안 cross 가 따라간 구조물로 소단을 셈(365-5). */ - bermAt?: (structures: unknown) => BermAt; /** 저장 노선 cross = 다시 셈 창구 한 벌(365-4 · 측점이 상세와 다르면 null = 엔진) · 주면 종단도는 따로 안 셈. */ savedCross?: ( d: SectionDetailResponse, diff --git a/A00_Common/design/design_shared.ts b/A00_Common/design/design_shared.ts index e4f1cbee4..f8a5598e0 100644 --- a/A00_Common/design/design_shared.ts +++ b/A00_Common/design/design_shared.ts @@ -59,8 +59,7 @@ import { refreshCrossDesigns, type CrossRefreshInput, } from "../../B06_Section/B06_Section_Cross_Refresh"; -import { bermSpansFromStructures, bermSpecAt } from "../../B06_Section/B06_Section_UI_Page_Persist"; -import { areasRequest, bermRequest, calcChecked } from "./design_calc_server"; +import { areasRequest, calcChecked } from "./design_calc_server"; import { registeredKeys, stationIntervalOf } from "../../B06_Section/B06_Section_Station_Keys"; import { detailCross } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail"; import type { CrossOutput, PipelineStation } from "./logic/design_pipeline_types"; @@ -255,15 +254,6 @@ function startPagePipeline(shared: DesignPageShared, profile: DesignProfile): vo each((hooks) => hooks.onFacilitiesFollowed?.(far)); }, onUnstable: () => showToast(L("Design_Pipeline_Unstable"), "warning"), - // 초안 소단 = 지금 구조물(따라간 자리)의 소단 구간(365-5 · 저장 노선은 다시 셈 창구가 세션 사본으로) - bermAt: (structures) => { - const list = Array.isArray(structures) ? structures : []; - // 계산 자리 quantity = server 면 잠잠 뒤 서버 대조만(초안 미리보기 · 511-A) - const spans = calcChecked("quantity", "cross.berm_spans", bermSpansFromStructures, [list], { - request: bermRequest(list), - }); - return (chainageM) => bermSpecAt(spans, chainageM); - }, // 저장 노선 횡단 = 다시 셈 창구 한 벌(365-4) — 카드 · 유토곡선 · [저장] 패치가 보는 상세 설계를 늘 고치고 그 // 값을 묶음 출력으로(발자국 · 평면 횡단 판) · 측점이 상세와 다르면(갈아끼움 전) 묶음 엔진(439) savedCross: (detail, routeId, stations) => diff --git a/A00_Common/design/logic/design_calc_node_stages.ts b/A00_Common/design/logic/design_calc_node_stages.ts index 472bc7562..6d7ff6e73 100644 --- a/A00_Common/design/logic/design_calc_node_stages.ts +++ b/A00_Common/design/logic/design_calc_node_stages.ts @@ -18,7 +18,6 @@ * ========================================================================== */ import { readFileSync, statSync } from "node:fs"; -import { bermSpansFromStructures, bermSpecAt } from "@util/common_util_berm_spans"; import type { FollowingStructure } from "@util/common_util_route_follow"; import { useCatalogDefinition, type CatalogDefinition } from "@util/common_util_structure_catalog"; import type { PlanPipe, PlanStructure, PlanType } from "@util/common_util_structure_station"; @@ -189,9 +188,7 @@ async function runStages( ctx.inputs.structures ?? savedStructures, types, ); - const list = Array.isArray(ctx.inputs.structures) ? ctx.inputs.structures : []; - const spans = bermSpansFromStructures(list); - return crossStage.stage(contextCross(ctx, status, (at) => bermSpecAt(spans, at))); + return crossStage.stage(contextCross(ctx, status)); }, }; if (stages.until === "follow") return followOnly(table, inputs); diff --git a/A00_Common/design/logic/design_pipeline_stage_inputs.ts b/A00_Common/design/logic/design_pipeline_stage_inputs.ts index 695d67fcc..a6028e3d3 100644 --- a/A00_Common/design/logic/design_pipeline_stage_inputs.ts +++ b/A00_Common/design/logic/design_pipeline_stage_inputs.ts @@ -10,7 +10,6 @@ import { } from "../../../common_util/common_util_route_follow"; import { crossWithContext, - type BermAt, type CrossInput, type ShapeStatus, } from "../../../B05_Profile/logic/B05_Profile_Pipeline_Detail"; @@ -45,11 +44,10 @@ export function placedFollowInputs( }; } -/** cross 단계 입력 = 시설 맥락(새 자리 그림) · 단면 유형 기본값(상단측) · 소단(지금 구조물) 얹음(365-5 · 457). */ +/** cross 단계 입력 = 시설 맥락(새 자리 그림 · 소단 = 얹힌 제원 · 577-2) · 단면 유형 기본값(상단측) 얹음(365-5 · 457). */ export const contextCross = ( ctx: StageContext<"cross">, status: ShapeStatus | null, - bermAt: BermAt | null, ): StageContext<"cross"> => ({ ...ctx, inputs: { @@ -58,7 +56,6 @@ export const contextCross = ( ctx.inputs.cross as CrossInput | null, ctx.out.ground?.stations ?? [], status, - bermAt, ), }, }); diff --git a/B05_Profile/logic/B05_Profile_Pipeline_Detail.ts b/B05_Profile/logic/B05_Profile_Pipeline_Detail.ts index 68b790170..7852e4f61 100644 --- a/B05_Profile/logic/B05_Profile_Pipeline_Detail.ts +++ b/B05_Profile/logic/B05_Profile_Pipeline_Detail.ts @@ -8,7 +8,8 @@ * `resources/tester/test_346_2_pipeline_cross_page.py`. * crossWithUphill cross 단계 입력에 단면 유형 기본값(상단측)을 얹음 — 서버 노선 저장이 단면 유형 · * 측구 방향을 새 지형으로 정하는 것과 같은 규칙(넘기는 칸 = 부르는 쪽 입력). - * crossWithShapes 얹힌 시설 맥락(포장 · 기울임 · 하강) = 지금 현황을 새 측점에 다시 얹은 그림에서(457). + * crossWithShapes 얹힌 시설 맥락(포장 · 기울임 · 하강 · 소단) = 지금 현황을 새 측점에 다시 얹은 그림에서(457 · + * 소단 = 얹힌 `berm` 제원 + 세션 쪽 깊이 · 577-2). * crossAtSeat 자리 칸(암 경계 · 2단 경사 · 손 상단측 · `seats`)은 측점이 그 자리 그대로일 때만 · 새 자리 = * 기본값(요구 275-3 · 판정 `sameSeat` · 서버 노선 저장 · Node 넘김과 같은 규칙). * draftSectionDetail ground · profile · cross → 상세(측점 · 지반 · 횡단 설계 · 그림 자료 · 카드 이름 = 지금 @@ -113,38 +114,6 @@ export function crossWithUphill( return { ...input, byKey }; } -/** 소단 한 측점 제원(세션 `berm` 꼴 · 구조물 목록에서 폄). */ -export type BermAt = ( - chainageM: number, -) => { width_m: number; interval_m: number; slope_deg: number; depth_m?: number } | null; - -/** 측점마다 소단 = 지금 구조물(초안 따라간 자리)의 소단 구간 · 쪽 깊이는 세션 고침(`bermDepthLeftM` · - * `bermDepthRightM` · 측점 열쇠) — 노선 초안 동안 소단 고침도 묶음으로(365-5). 구간 밖 측점은 소단 없음. */ -export function crossWithBerm( - input: CrossInput | null, - stations: ReadonlyArray, - bermAt: BermAt | null, -): CrossInput | null { - if (!input || !bermAt) return input; - const byKey: CrossInput["byKey"] = { ...input.byKey }; - for (const station of stations) { - const { bermDepthLeftM, bermDepthRightM, ...rest } = input.byKey[station.key] ?? {}; - const at = bermAt(station.chainageM); - byKey[station.key] = { - ...rest, - berm: at && { - widthM: at.width_m, - intervalM: at.interval_m, - slopeDeg: at.slope_deg, - depthM: at.depth_m, - depthLeftM: (bermDepthLeftM as number | undefined) ?? null, - depthRightM: (bermDepthRightM as number | undefined) ?? null, - }, - }; - } - return { ...input, byKey }; -} - const kindOf = (station: PipelineStation): SectionStation["kind"] => station.key === "end:BP" ? "bp" @@ -269,7 +238,8 @@ function shapeContext(section: CrossSection): Record { }; } -/** 측점마다 시설 맥락(포장 · 물넘이 기울임 · 세월교 노면 하강) = 지금 현황을 새 측점에 다시 얹은 그림에서(457). */ +/** 측점마다 시설 맥락(포장 · 물넘이 기울임 · 세월교 노면 하강 · 소단) = 지금 현황을 새 측점에 다시 얹은 그림에서 + * (457) · 소단 쪽 깊이 = 세션 고침(`bermDepthLeftM` · `bermDepthRightM` · 측점 열쇠 · 365-5) · 구간 밖 = 소단 없음. */ export function crossWithShapes( input: CrossInput | null, stations: ReadonlyArray, @@ -285,23 +255,31 @@ export function crossWithShapes( ); const byKey: CrossInput["byKey"] = { ...input.byKey }; stations.forEach((station, i) => { - byKey[station.key] = { ...byKey[station.key], ...shapeContext(sections[i]) }; + const { bermDepthLeftM, bermDepthRightM, ...rest } = byKey[station.key] ?? {}; + const at = sections[i].berm; + byKey[station.key] = { + ...rest, + ...shapeContext(sections[i]), + berm: at && { + widthM: at.width_m, + intervalM: at.interval_m, + slopeDeg: at.slope_deg, + depthM: at.depth_m, + depthLeftM: (bermDepthLeftM as number | undefined) ?? null, + depthRightM: (bermDepthRightM as number | undefined) ?? null, + }, + }; }); return { ...input, byKey }; } -/** cross 단계 입력 = 시설 맥락(새 자리 그림) · 단면 유형 기본값(상단측) · 소단(지금 구조물) 얹음 — 페이지 묶음 cross. */ +/** cross 단계 입력 = 시설 맥락(새 자리 그림 · 소단 포함) · 단면 유형 기본값(상단측) 얹음 — 페이지 묶음 cross. */ export const crossWithContext = ( input: CrossInput | null, stations: ReadonlyArray, status: ShapeStatus | null, - bermAt: BermAt | null, ): CrossInput | null => - crossWithBerm( - crossWithUphill(crossWithShapes(crossAtSeat(input, stations), stations, status), stations), - stations, - bermAt, - ); + crossWithUphill(crossWithShapes(crossAtSeat(input, stations), stations, status), stations); /** 묶음 출력 → 상세. 저장 상세의 종단 머리(이름)는 그대로 · 측점 · 지반 · 횡단 · 계획선 표본은 새 노선 * (십자선 대체 높이도 초안 종단 · 413) · 그림 자료 · 카드 이름 = `status`(지금 현황)를 새 측점에 다시 얹음(457). */ diff --git a/B06_Section/B06_Section_Api_Types.ts b/B06_Section/B06_Section_Api_Types.ts index 44251272c..d4dc6d806 100644 --- a/B06_Section/B06_Section_Api_Types.ts +++ b/B06_Section/B06_Section_Api_Types.ts @@ -62,6 +62,7 @@ import type { FordPavementSpec } from "./logic/B06_Section_Cross_Ford_Pavement"; import type { RevetmentSpec } from "./logic/B06_Section_Cross_Revetment_Geom"; import type { PavementEntry } from "@util/common_util_structure_pavement"; import type { UnderdrainSpec } from "@util/common_util_structure_underdrain"; +import type { BermSessionSpec } from "@util/common_util_berm_spans"; export type { FordPavementSpec, RevetmentSpec }; export interface SectionContextResponse { @@ -343,6 +344,8 @@ export interface CrossSection extends SectionStation { pavement?: PavementEntry[]; /** 맹암거 — 기준 측점(288-1). 높이 · 경사 · 측구 없음 · 노면 기울임 · 돌 무늬 형상. */ underdrain?: UnderdrainSpec; + /** 소단 — 덮는 구간 제원(577-2 · null = 얹었는데 없음 · 칸 없음 = 아직 안 얹음 → 저장분 `design.berm`). */ + berm?: BermSessionSpec | null; /** 빈칸 경고(323-4 · 읽기 칸 · 저장 안 함) — 「종류 칸」(형상 필수 칸이 빈 관 · 구조물 · 측점 계획 자리). */ blank?: string[]; } diff --git a/B06_Section/B06_Section_Cross_Refresh.ts b/B06_Section/B06_Section_Cross_Refresh.ts index 610385875..5c841e917 100644 --- a/B06_Section/B06_Section_Cross_Refresh.ts +++ b/B06_Section/B06_Section_Cross_Refresh.ts @@ -27,8 +27,6 @@ import type { StandardCrossSectionSpec } from "@util/common_util_cross_design"; import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch"; import { readCutSlopeSession, readRockBoundarySession } from "./B06_Section_UI_Page_Persist"; -import type { BermSpan } from "./B06_Section_UI_Page_Persist"; -import { readState } from "../A00_Common/b_page_state"; import { USER_TOUCHED_KEYS } from "./logic/B06_Section_Design_Keys"; import { savedDesigns } from "./B06_Section_Section_Canon"; import { @@ -228,17 +226,14 @@ function gatherRefresh(input: CrossRefreshInput): RefreshInputs | null { const standard = effectiveStandardCross(projectId) as StandardCrossSectionSpec | null; if (!standard) return null; // 컨텍스트를 아직 못 받음 — 받은 뒤 다시 셈 const { routeId } = input; - // 소단 사본 없음(시설 줄이 구조물을 받기 전) = 저장분 `design.berm`(파이썬 `load_berm_spans` 못 읽음 → - // 저장분 · `stored_berm` 짝 · 467) - const bermCopy = readState("berm", projectId, routeId); return { alignment, standard, rockOffsets: [...readRockOffsets(projectId, routeId)], rockDefault: readRockBoundaryDefault(projectId), - // 측점별 암 절토 경사 · 소단 · 카드 선택 · 노견 확장 = 세션에만 있는 값 — **계산 전에** 실음(2026-09-07 · 3-9) + // 측점별 암 절토 경사 · 소단 깊이 · 카드 선택 · 노견 확장 = 세션에만 있는 값 — **계산 전에** 실음(2026-09-07 · + // 3-9) · 소단 제원 = 측점에 얹힌 `section.berm`(577-2) cutSlopes: [...readCutSlopeRatios(projectId, routeId)], - bermSpans: Array.isArray(bermCopy) ? bermCopy : null, bermDepths: [...readBermDepths(projectId, routeId)], choices: [...crossDesignChoices(projectId, routeId)], shoulderExtends: [...readShoulderExtends(projectId, routeId)], diff --git a/B06_Section/B06_Section_Engine_Attach.py b/B06_Section/B06_Section_Engine_Attach.py index a1f6b4ce7..bce5a50dc 100644 --- a/B06_Section/B06_Section_Engine_Attach.py +++ b/B06_Section/B06_Section_Engine_Attach.py @@ -1,7 +1,7 @@ """형상 얹기 한 벌(323-4) — 측점(횡단 dict) + 현황(관 · 구조물) + 구조물 표 → 그 측점 그림 자료. 구조물 열쇠(`SHAPE_KEYS` · 빈칸 경고 `blank`)를 걷고 차례대로 다시 얹음 — 관 세트 → 벽 → 포장 → 맹암거 → -빈칸 경고. 고르기 = 표 형상(322-1). 서버 상세 읽기(`_read_section_detail`) · 재생성 응답이 이것 하나를 씀. +소단 → 빈칸 경고. 고르기 = 표 형상(322-1). 서버 상세 읽기(`_read_section_detail`) · 재생성 응답이 이것 하나를 씀. 셈 = 늘 떠 있는 Node `facility.attach`(화면과 같은 TS · 511-B B-4) · 파이썬 몫(`_attach_python`)은 Node 를 못 부를 때만. TS 짝 = `common_util/common_util_structure_attach.ts` `attachShapes`(화면 = 초안 ?? 저장분 현황) · @@ -18,7 +18,12 @@ from typing import Any from B05_Profile.B05_Profile_Structures_Repository import load_structures from B05_Profile.B05_Profile_Structures_Schema import structure_type_map from B06_Section.B06_Section_Engine_Culvert_Sets import attach_culvert_specs, culvert_sets_from -from B06_Section.B06_Section_Engine_Structures_Wall import attach_wall_specs, wall_specs_from +from B06_Section.B06_Section_Engine_Structures_Wall import ( + attach_berm_specs, + attach_wall_specs, + berm_spans_from, + wall_specs_from, +) from B06_Section.B06_Section_Engine_Tilt import attach_underdrain_specs, underdrains_from from common_util.common_util_structure_catalog import catalog_row, field_name, type_ids_with from common_util.common_util_structure_pavement import ( @@ -35,7 +40,16 @@ from common_util.common_util_structure_station import ( logger = logging.getLogger(__name__) #: 측점 그림 자료 열쇠 — TS `common_util_structure_shape_keys.SHAPE_KEYS` 짝. -SHAPE_KEYS = ("culvert", "ford", "box", "ford_pavement", "revetment", "pavement", "underdrain") +SHAPE_KEYS = ( + "culvert", + "ford", + "box", + "ford_pavement", + "revetment", + "pavement", + "underdrain", + "berm", +) def _blank_labels(type_id: str, options: dict | None, types: dict[str, Any]) -> list[str]: @@ -128,6 +142,7 @@ def _attach_python( pavement_specs_from([s.model_dump() for s in structures], type_ids_with(shape="pavement")), ) attach_underdrain_specs(sections, underdrains_from(structures)) + attach_berm_specs(sections, berm_spans_from(structures)) _attach_blanks(sections, pipes, structures) return misses diff --git a/B06_Section/B06_Section_Engine_Structures_Wall.py b/B06_Section/B06_Section_Engine_Structures_Wall.py index a427db9dd..16124e886 100644 --- a/B06_Section/B06_Section_Engine_Structures_Wall.py +++ b/B06_Section/B06_Section_Engine_Structures_Wall.py @@ -110,11 +110,10 @@ def wall_specs_from(structures: list[Any]) -> list[dict[str, Any]]: return found -def load_berm_spans(project_root: Path) -> list[tuple[float, float, dict[str, float]]] | None: - """구조물 정본의 소단 구간 `[(시작, 끝, 제원)]` — [저장] 서버 다시 셈의 소단 원천(194-1). +def berm_spans_from(structures: list[Any]) -> list[tuple[float, float, dict[str, float]]]: + """구조물 목록 → 소단 구간 `[(시작, 끝, 제원)]`(형상 얹기 한 벌 577-2 · [저장] 194-1). 짝: TS `bermSpansFromStructures`(같은 규칙 — 폭 0 = 없음 · 깊이 빈 값 = 기본 1.0 · 0 미만 = 0). - 못 읽으면 None(부르는 쪽이 저장분 `design.berm` 으로 물러섬). """ from common_util.common_util_cross_berm import ( BERM_DEFAULT_DEPTH_M, @@ -130,35 +129,41 @@ def load_berm_spans(project_root: Path) -> list[tuple[float, float, dict[str, fl return fallback return value if math.isfinite(value) and value >= 0 else fallback + spans: list[tuple[float, float, dict[str, float]]] = [] + for item in structures: + if (catalog_row(item.type_id) or {}).get("shape") != "berm": # 표 형상(323-4) + continue + anchor = item.chainage_m if item.chainage_m is not None else item.start_m + start = item.start_m if item.start_m is not None else anchor + end = item.end_m if item.end_m is not None else anchor + if start is None or end is None: + continue + options = item.options or {} + width = number(options, "width_m", BERM_DEFAULT_WIDTH_M) + if width <= 0: + continue + raw = options.get("depth_m") + try: + depth = BERM_DEFAULT_DEPTH_M if raw in (None, "") else max(float(raw), 0.0) + except (TypeError, ValueError): + depth = BERM_DEFAULT_DEPTH_M + if not math.isfinite(depth): + depth = BERM_DEFAULT_DEPTH_M + spec = { + "width_m": width, + "interval_m": number(options, "interval_m", BERM_DEFAULT_INTERVAL_M), + "slope_deg": number(options, "slope_deg", BERM_DEFAULT_SLOPE_DEG), + "depth_m": depth, + } + spans.append((float(min(start, end)), float(max(start, end)), spec)) + return spans + + +def load_berm_spans(project_root: Path) -> list[tuple[float, float, dict[str, float]]] | None: + """구조물 정본의 소단 구간 — [저장] 서버 다시 셈의 소단 원천(194-1) · 못 읽으면 None(부르는 쪽이 + 저장분 `design.berm` 으로 물러섬).""" try: - spans: list[tuple[float, float, dict[str, float]]] = [] - for item in load_structures(str(project_root))[1]: - if (catalog_row(item.type_id) or {}).get("shape") != "berm": # 표 형상(323-4) - continue - anchor = item.chainage_m if item.chainage_m is not None else item.start_m - start = item.start_m if item.start_m is not None else anchor - end = item.end_m if item.end_m is not None else anchor - if start is None or end is None: - continue - options = item.options or {} - width = number(options, "width_m", BERM_DEFAULT_WIDTH_M) - if width <= 0: - continue - raw = options.get("depth_m") - try: - depth = BERM_DEFAULT_DEPTH_M if raw in (None, "") else max(float(raw), 0.0) - except (TypeError, ValueError): - depth = BERM_DEFAULT_DEPTH_M - if not math.isfinite(depth): - depth = BERM_DEFAULT_DEPTH_M - spec = { - "width_m": width, - "interval_m": number(options, "interval_m", BERM_DEFAULT_INTERVAL_M), - "slope_deg": number(options, "slope_deg", BERM_DEFAULT_SLOPE_DEG), - "depth_m": depth, - } - spans.append((float(min(start, end)), float(max(start, end)), spec)) - return spans + return berm_spans_from(load_structures(str(project_root))[1]) except Exception: # noqa: BLE001 — 정본을 못 읽으면 저장분으로 물러섬 logger.exception("B06 소단 구간을 읽지 못했습니다 (저장분 소단을 씀)") return None @@ -174,6 +179,24 @@ def berm_span_at( return None +def attach_berm_specs( + cross_sections: list[dict[str, Any]], spans: list[tuple[float, float, dict[str, float]]] +) -> int: + """측점마다 소단 제원을 얹음(TS `attachBermSpecs` 짝 · 577-2) — 덮는 소단 없음 = None + (얹었음 표시). 얹은 측점 수.""" + attached = 0 + for section in cross_sections: + chainage = section.get("chainage_m") + if isinstance(chainage, bool) or not isinstance(chainage, (int, float)): + continue + if not math.isfinite(chainage): + continue + spec = berm_span_at(spans, float(chainage)) + section["berm"] = dict(spec) if spec else None + attached += 1 if spec else 0 + return attached + + def attach_wall_specs(cross_sections: list[dict[str, Any]], walls: list[dict[str, Any]]) -> int: """구간 안 측점에 벽 제원을 얹는다(TS `attachWallSpecs` 짝). 얹은 개수를 돌려준다.""" if not walls: diff --git a/B06_Section/B06_Section_UI_Page_Persist.ts b/B06_Section/B06_Section_UI_Page_Persist.ts index 5f82ba91b..93f06fce4 100644 --- a/B06_Section/B06_Section_UI_Page_Persist.ts +++ b/B06_Section/B06_Section_UI_Page_Persist.ts @@ -16,7 +16,7 @@ import { } from "./B06_Section_Api_Fetch"; import type { DesignViewSave } from "../A00_Common/design/design_shared"; import { buildCrossPatches, type CrossPatchSources } from "./logic/B06_Section_UI_Page_Patches"; -import { clearState, readByKey, writeByKey, writeState } from "../A00_Common/b_page_state"; +import { clearState, readByKey, writeByKey } from "../A00_Common/b_page_state"; import { kindChangedChainages } from "./B06_Section_Api_Culvert_Options"; import { crossDesignChoices, @@ -30,15 +30,8 @@ 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"; -// 소단 구간(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"; +// 소단 종류 판정 — common_util 로 옮김 · 부르는 곳 그대로(다시 내보냄). +export { isBermType } from "@util/common_util_berm_spans"; import { readKeyed, writeKeyed, type KeyedState } from "./B06_Section_Station_Keys"; /** @@ -124,11 +117,6 @@ function putCell( } } -/** 소단 사본(결과 통 · 구조물에서 셈 · 477-4) — 쓰는 곳은 페이지 시설 줄 하나. */ -export function writeBermSpans(projectId: string, routeId: number, spans: BermSpan[]): void { - writeState("berm", spans, projectId, routeId); -} - /** 암 경계선 오프셋 저장소 — 값(세션 칸)과 조정창 제어기를 함께 낸다. */ export interface RockBoundaryStore { /** 지금 세션 칸 — 측점키(누가거리 2자리) → 오프셋(m). `buildCrossPatches` 재료(새 Map). */ diff --git a/B06_Section/logic/B06_Section_Cross_Refresh_Calc.ts b/B06_Section/logic/B06_Section_Cross_Refresh_Calc.ts index bf1b76a26..ba1143a8a 100644 --- a/B06_Section/logic/B06_Section_Cross_Refresh_Calc.ts +++ b/B06_Section/logic/B06_Section_Cross_Refresh_Calc.ts @@ -7,7 +7,7 @@ * 순수 — 화면 · fetch · 세션 없음(세션 읽기는 부르는 쪽). * ========================================================================== */ -import { bermSpecAt, type BermSessionSpec, type BermSpan } from "@util/common_util_berm_spans"; +import type { BermSessionSpec } from "@util/common_util_berm_spans"; import { BERM_DEFAULT_DEPTH_M, BERM_DEFAULT_INTERVAL_M, @@ -42,8 +42,6 @@ export interface RefreshInputs { rockDefault: number | null; /** 측점별 암 절토 경사(0 = 표준값). */ cutSlopes: Array<[number, number]>; - /** 소단 사본(세션 `berm` · 없으면 null = 저장분 `design.berm`). */ - bermSpans: BermSpan[] | null; bermDepths: Array<[number, SideValues]>; /** 카드 선택(지반 · 측구 · 2단 경사 …). */ choices: Array<[number, CrossDesignChoice]>; @@ -141,13 +139,12 @@ export function refreshDesigns( // 측점별 암 절토 경사도 세션에만 있는 값이다 — **계산 전에** 실어야 설계선이 새 경사로 // 그려진다. 계산 뒤에 값만 베껴 붙이면 그림은 옛 경사, 숫자만 새것이 된다(2026-09-07). const cutSlopeRatios = new Map(inputs.cutSlopes); - // 소단도 같은 성격 — 세션에만 있는 값이라 여기서 실어 주지 않으면 계획선을 고치는 - // 순간 계단이 사라진다(계획서 3-9). 사본 없음(시설 줄이 구조물을 받기 전) = 저장분 `design.berm` - // (파이썬 `load_berm_spans` 못 읽음 → 저장분 · `stored_berm` 짝 · 467). - const bermSpans = inputs.bermSpans; + // 소단도 같은 성격 — 계산 전에 실어야 계획선을 고치는 순간 계단이 안 사라진다(계획서 3-9). 제원 = 측점에 + // 얹힌 소단(형상 얹기 한 벌 · 577-2) · 아직 안 얹음(칸 없음) = 저장분 `design.berm`(`stored_berm` 짝 · 467) const bermDepthAt = bermDepthReader(new Map(inputs.bermDepths)); - const bermAt = (chainageM: number, design: Record): BermSpec | null => { - const spec = bermSpans ? bermSpecAt(bermSpans, chainageM) : storedBermSpec(design.berm); + const bermAt = (section: CrossSection, design: Record): BermSpec | null => { + const chainageM = section.chainage_m; + const spec = section.berm === undefined ? storedBermSpec(design.berm) : section.berm; return spec ? { widthM: spec.width_m, @@ -226,7 +223,7 @@ export function refreshDesigns( tilted: Boolean(section.ford_pavement || section.underdrain), tiltPct: section.ford_pavement?.slope_pct ?? null, // 맹암거 = 표준 경사(371-3) standard, - berm: bermAt(section.chainage_m, design), + berm: bermAt(section, design), rockBoundaryOffsetM, cutSlopeRatio: cutSlopeAt(section.chainage_m, design), twoStageSlope: choice?.two_stage_slope ?? Boolean(design.two_stage_slope), diff --git a/common_util/common_util_berm_spans.ts b/common_util/common_util_berm_spans.ts index 58ad71e3f..cba294ec0 100644 --- a/common_util/common_util_berm_spans.ts +++ b/common_util/common_util_berm_spans.ts @@ -1,10 +1,10 @@ /* ============================================================================= * 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`). + * 소단 구간 — 구조물 목록(표 형상 berm)에서 구간 · 측점마다 제원(PLAN 472-4 C4) · 측점에 얹기(577-2 · + * 벽처럼 형상 얹기 한 벌 `attachShapes` 가 `section.berm` 을 세움 · 계단은 계산 안쪽 그대로). + * 브라우저(형상 얹기 · 다시 셈)와 서버 Node 입구(`design_calc_node` · 등록부 `cross.berm_spans` · `facility.attach`)가 같은 셈. + * 파이썬 짝 = `B06_Section_Engine_Structures_Wall.py` `berm_spans_from` · `load_berm_spans` · `berm_span_at` · + * `attach_berm_specs`(Node 를 못 부를 때만 · 거울 시험 `test_272_berm_source.py` · `test_323_4_attach_mirror.py`). * ========================================================================== */ import { @@ -15,7 +15,7 @@ import { } from "./common_util_cross_berm"; import { catalogRow } from "./common_util_structure_catalog"; -/** 측점 하나의 소단 제원 — 서버 payload 와 같은 이름을 쓴다(그대로 실어 보낸다). */ +/** 측점 하나의 소단 제원(`section.berm`) — 서버 payload 와 같은 이름을 쓴다(그대로 실어 보낸다). */ export interface BermSessionSpec { width_m: number; interval_m: number; @@ -42,10 +42,6 @@ export const isBermType = (typeId: string): boolean => catalogRow(typeId)?.shape /** * 구조물 목록에서 **소단 구간**을 뽑는다 — 사용자는 「구조물 배치」에서 놓는다 * (2026-09-07 사용자 확정: 별도 폼이 아니라 다른 옹벽·기슭막이와 같은 자리). - * - * 세션 열쇠 `berm` 은 그 결과를 담는 **사본**이다. 재계산(브라우저·서버)이 측점마다 값을 - * 읽어야 하는데 구조물 목록은 비동기로 오므로, 목록이 바뀔 때마다 여기서 펴 두고 계산은 - * 그 사본만 본다. */ export function bermSpansFromStructures( structures: ReadonlyArray<{ @@ -105,3 +101,19 @@ export function bermSpecAt(spans: BermSpan[], chainageM: number): BermSessionSpe } : null; } + +/** 측점마다 소단 제원을 얹음(파이썬 `attach_berm_specs` 짝 · 제자리) — 덮는 소단 없음 = null(얹었음 표시 · + * 칸 없음 = 아직 안 얹음 → 다시 셈이 저장분 `design.berm` 으로 물러섬). 얹은 측점 수. */ +export function attachBermSpecs( + sections: Array>, + spans: BermSpan[], +): number { + let attached = 0; + for (const section of sections) { + const chainage = section.chainage_m; + if (typeof chainage !== "number" || !Number.isFinite(chainage)) continue; + section.berm = bermSpecAt(spans, chainage); + if (section.berm) attached += 1; + } + return attached; +} diff --git a/common_util/common_util_structure_attach.ts b/common_util/common_util_structure_attach.ts index b9dd76dc2..2b9698fc9 100644 --- a/common_util/common_util_structure_attach.ts +++ b/common_util/common_util_structure_attach.ts @@ -3,7 +3,7 @@ * 형상 얹기 한 벌(323-4) — 측점(횡단) + 현황(관 · 구조물) + 구조물 표 → 그 측점 그림 자료. * * 구조물 열쇠(`SHAPE_KEYS` · 빈칸 경고 `blank`)를 걷고 서버와 같은 차례로 다시 얹음 — 관 세트 → 벽 → - * 포장 → 맹암거 → 빈칸 경고. 고르기 = 표 형상(322-1). 화면은 초안 ?? 저장분 현황으로 이것 하나를 씀 + * 포장 → 맹암거 → 소단 → 빈칸 경고. 고르기 = 표 형상(322-1). 화면은 초안 ?? 저장분 현황으로 이것 하나를 씀 * (`B06_Section_Section_Drafts.withDraftShapes`) — 저장 전 그림 = 저장 뒤 그림. * 파이썬 짝 = `B06_Section/B06_Section_Engine_Attach.py` `attach_shapes`(서버 상세 읽기 · 재생성 응답) · * 거울 시험 = `resources/tester/test_323_4_attach_mirror.py`. @@ -30,6 +30,7 @@ import { underdrainSpecsFrom, type UnderdrainInput, } from "./common_util_structure_underdrain"; +import { attachBermSpecs, bermSpansFromStructures } from "./common_util_berm_spans"; import { catalogRow, fieldName } from "./common_util_structure_catalog"; import { blankKeys, @@ -163,6 +164,10 @@ export function attachShapes( ), ); attachUnderdrainSpecs(sections, underdrainSpecsFrom(structures as UnderdrainInput[])); + attachBermSpecs( + sections, + bermSpansFromStructures(structures as Parameters[0]), + ); attachBlanks(sections, pipes, structures, types); return misses; } diff --git a/common_util/common_util_structure_shape_keys.ts b/common_util/common_util_structure_shape_keys.ts index 7b3995390..c48c1fd98 100644 --- a/common_util/common_util_structure_shape_keys.ts +++ b/common_util/common_util_structure_shape_keys.ts @@ -13,4 +13,5 @@ export const SHAPE_KEYS = [ "revetment", "pavement", "underdrain", + "berm", ] as const; diff --git a/resources/tester/helper_271_facility_lane.cjs b/resources/tester/helper_271_facility_lane.cjs index 6e33e51a8..cb78f3a2b 100644 --- a/resources/tester/helper_271_facility_lane.cjs +++ b/resources/tester/helper_271_facility_lane.cjs @@ -35,8 +35,6 @@ const STUBS = { }), "ui_template_elements.ts": anyModule(), "B04_PreProcess_Api_Fetch.ts": anyModule(), - // 소단 사본(467) — 끌어오는 줄이 import.meta 를 써 이 로더로 못 읽음 · 소단 몫은 test_272_berm_source(vite 묶음) - "B06_Section_UI_Page_Persist.ts": anyModule({ bermSpansFromStructures: () => [] }), }; const resolve = (base) => diff --git a/resources/tester/helper_272_berm_source.mjs b/resources/tester/helper_272_berm_source.mjs index 1f22b7f66..4b0d5b813 100644 --- a/resources/tester/helper_272_berm_source.mjs +++ b/resources/tester/helper_272_berm_source.mjs @@ -1,7 +1,7 @@ -/* 467 헬퍼 — 소단 원천 한 벌: 구조물 → 소단 구간(TS) · 저장분 소단 제원(TS) · 시설 줄이 세션 사본을 씀 · - * 다시 셈 = 사본 없으면 저장분 `design.berm`. +/* 467 · 577-2 헬퍼 — 소단 원천 한 벌: 구조물 → 소단 구간(TS) · 측점 얹기(TS) · 저장분 소단 제원(TS) · + * 다시 셈 = 얹힌 `section.berm` · 칸 없으면 저장분 `design.berm`. * node helper_272_berm_source.mjs <묶음.mjs> - * job = { standard, rockDefault, structureCases, storedCases, ground } · 짝 = `test_272_berm_source.py`. + * job = { standard, rockDefault, structureCases, storedCases, ground, chainages } · 짝 = `test_272_berm_source.py`. */ import { readFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; @@ -45,34 +45,12 @@ const out = {}; out.spans = job.structureCases.map((list) => m.bermSpansFromStructures(list)); out.stored = job.storedCases.map((spec) => m.storedBermSpec(spec)); -/* 2. 시설 줄이 사본을 씀 — 구조물 받기 전 안 씀 · 같으면 안 씀 · 바뀌면 한 번 */ -{ - m.setPageRouteId(P, R); - const key = m.stateKey("berm", P, R); - const count = () => writes.filter((k) => k === key).length; - const copy = () => m.readState("berm", P, R); - const lane = m.createFacilityLane(P, () => null); - const steps = {}; - lane.setTypes([]); - lane.follow(); // 저장분 · 가게 구조물 없음 = 아직 모름 - steps.beforeKnown = { copy: copy(), writes: count() }; - const saved = job.structureCases[0]; - lane.setStored(saved); - lane.set({ structures: saved }, false); - steps.loaded = { copy: copy(), writes: count() }; - lane.set({ structures: saved.map((item) => ({ ...item })) }); - lane.set({ pipes: [] }); - steps.same = { writes: count() }; - const moved = saved.map((item) => - item.type_id === "berm" ? { ...item, start_m: item.start_m + 5, end_m: item.end_m + 5 } : item, - ); - lane.set({ structures: moved }); - steps.moved = { copy: copy(), writes: count() }; - lane.set({ structures: [] }); - steps.removed = { copy: copy(), writes: count() }; - lane.dispose(); - out.lane = steps; -} +/* 2. 측점 얹기 — 구조물마다 측점 목록에 얹은 칸(덮는 소단 없음 = null · 누가거리 없음 = 칸 없음) */ +out.attach = job.structureCases.map((list) => { + const sections = job.chainages.map((c) => ({ chainage_m: c })); + m.attachBermSpecs(sections, m.bermSpansFromStructures(list)); + return sections; +}); /* 2-2. 시설 줄 칠함 — 종류 표 받기 전 측점(빈 목록)을 종단도에 안 씀 · 받은 뒤 한 번 */ { @@ -132,10 +110,9 @@ out.stored = job.storedCases.map((spec) => m.storedBermSpec(spec)); ], balance: { cut_area_m2: 0, fill_area_m2: 0, net_area_m2: 0 }, }; - const run = async (copy) => { - if (copy === undefined) sessionStorage.removeItem(m.stateKey("berm", P, R)); - else m.writeState("berm", copy, P, R); + const run = async (berm) => { const detail = detailOf(); + if (berm !== undefined) detail.cross_sections[0].berm = berm; await m.refreshCrossDesigns({ projectId: P, routeId: R, @@ -147,9 +124,9 @@ out.stored = job.storedCases.map((spec) => m.storedBermSpec(spec)); return { fill: design.fill_area_m2 ?? null, cut: design.cut_area_m2 ?? null }; }; out.refresh = { - noCopy: await run(undefined), - copySame: await run(m.bermSpansFromStructures(job.structureCases[0])), - copyEmpty: await run([]), + unattached: await run(undefined), + attachedSame: await run(job.storedBerm), + attachedNone: await run(null), }; } diff --git a/resources/tester/helper_272_berm_source_entry.ts b/resources/tester/helper_272_berm_source_entry.ts index 9aa269cd0..ccfaf3d82 100644 --- a/resources/tester/helper_272_berm_source_entry.ts +++ b/resources/tester/helper_272_berm_source_entry.ts @@ -1,7 +1,7 @@ -/* 467 시험 묶음 진입점 — 소단 원천 한 벌(시설 줄 사본 · 다시 셈 저장분 폴백 · 진짜 모듈 · vite ssr). */ +/* 467 · 577-2 시험 묶음 진입점 — 소단 원천 한 벌(측점 얹기 · 다시 셈 저장분 폴백 · 진짜 모듈 · vite ssr). */ export { createFacilityLane } from "../../A00_Common/design/design_facility_lane"; export { setPageRouteId } from "../../A00_Common/design/design_page_route"; export { refreshCrossDesigns, storedBermSpec } from "../../B06_Section/B06_Section_Cross_Refresh"; -export { bermSpansFromStructures } from "../../B06_Section/B06_Section_UI_Page_Persist"; +export { attachBermSpecs, bermSpansFromStructures } from "../../common_util/common_util_berm_spans"; export { rememberRockBoundaryDefault } from "../../B06_Section/B06_Section_UI_Standard_Panel"; export { readState, stateKey, writeState } from "../../A00_Common/b_page_state"; diff --git a/resources/tester/helper_272_choice_undo.mjs b/resources/tester/helper_272_choice_undo.mjs index 21fe04717..3c73195f9 100644 --- a/resources/tester/helper_272_choice_undo.mjs +++ b/resources/tester/helper_272_choice_undo.mjs @@ -38,13 +38,6 @@ const R = 6; const AT = 40; m.writeState("std-cross", job.standard, P); m.rememberRockBoundaryDefault(P, job.rockDefault); -// 소단 사본 = 측점을 덮는 구간(소단 깊이가 먹게) -m.writeState( - "berm", - [{ start_m: 0, end_m: 100, width_m: 1, interval_m: 3, slope_deg: 0, depth_m: 1 }], - P, - R, -); const canonDetail = () => ({ longitudinal: { length_m: 100, stations: [], profile_alignment: { policy: {} } }, cross_sections: [ @@ -59,6 +52,8 @@ const canonDetail = () => ({ center_y: 0, frame: { left_xy: [0, 1] }, uphill_side: "left", + // 측점에 얹힌 소단(577-2 · 소단 깊이가 먹게) + berm: { width_m: 1, interval_m: 3, slope_deg: 0, depth_m: 1 }, samples: job.ground, design: { design_elevation_m: 100, diff --git a/resources/tester/helper_365_page_pipeline.cjs b/resources/tester/helper_365_page_pipeline.cjs index 34613542d..154a0c1c7 100644 --- a/resources/tester/helper_365_page_pipeline.cjs +++ b/resources/tester/helper_365_page_pipeline.cjs @@ -80,8 +80,24 @@ STUBS["B05_Profile_Api_Replan"] = { station_snap: false, }), }; +// ⑪ 소단 프로젝트 = 저장 구조물에 소단 한 구간(577-2 · 측점에 얹힌 제원으로 셈) +const BERM_SPAN = { from: 30, to: 70 }; STUBS["B05_Profile_Api_Structures"] = { - fetchStructures: async () => ({ structures: [] }), + fetchStructures: async (projectId) => ({ + structures: + projectId === "proj-365c" + ? [ + { + structure_id: "berm-1", + type_id: "berm", + placement: "interval", + start_m: BERM_SPAN.from, + end_m: BERM_SPAN.to, + options: { width_m: 1.5, interval_m: 5, slope_deg: 60, depth_m: 1 }, + }, + ] + : [], + }), fetchStructureTypes: async () => job.types, }; STUBS["B05_Profile_Api_TerrainGrid"] = { @@ -421,17 +437,11 @@ async function run() { pageMod.disposePagePipeline(Q); page.clearDrafts(Q); - // ⑪ 초안 소단(365-5) — 소단 = 지금 구조물 구간(bermAt) · 쪽 깊이 = 세션 고침(측점 열쇠) → 묶음 cross + // ⑪ 초안 소단(365-5) — 소단 = 지금 구조물 구간(측점에 얹힌 제원 · 577-2) · 쪽 깊이 = 세션 고침(측점 열쇠) → 묶음 cross const B = "proj-365c"; window.localStorage.setItem("frd_current_project_id", B); - const span = { from: 30, to: 70 }; - const bBridge = pageMod.routeDraftBridge(B, link(), { - stationIntervalM: job.interval, - bermAt: () => (c) => - c >= span.from && c <= span.to - ? { width_m: 1.5, interval_m: 5, slope_deg: 60, depth_m: 1 } - : null, - }); + const span = BERM_SPAN; + const bBridge = pageMod.routeDraftBridge(B, link(), { stationIntervalM: job.interval }); pageMod.pipelineEarthwork(B, { conversion: job.conversion }); const bPipe = pageMod.pagePipeline(B); await settle(runs().length + 1); diff --git a/resources/tester/helper_472_1_browser.mjs b/resources/tester/helper_472_1_browser.mjs index 3518dfeec..2c4756118 100644 --- a/resources/tester/helper_472_1_browser.mjs +++ b/resources/tester/helper_472_1_browser.mjs @@ -69,10 +69,6 @@ const savedStations = m.buildStations(savedLine(), savedCurves(), pipes, structu }); const crossSource = m.pipelineCrossSource(P, detail, savedStations, R); const edits = m.readAlignment(detail.longitudinal).edits; -const bermAt = (list) => { - const spans = m.bermSpansFromStructures(Array.isArray(list) ? list : []); - return (chainageM) => m.bermSpecAt(spans, chainageM); -}; const crossStage = m.createCrossStage({ grid: () => ({ grid }) }); const pipe = m.createDesignPipeline({ stages: { @@ -119,7 +115,6 @@ const pipe = m.createDesignPipeline({ ctx.inputs.cross, ctx.out.ground?.stations ?? [], m.shapeStatus(ctx.inputs.pipes, ctx.inputs.structures, types), - bermAt(ctx.inputs.structures), ), }, }), diff --git a/resources/tester/helper_472_1_browser_entry.ts b/resources/tester/helper_472_1_browser_entry.ts index fe5c74b9f..db5972c6b 100644 --- a/resources/tester/helper_472_1_browser_entry.ts +++ b/resources/tester/helper_472_1_browser_entry.ts @@ -30,7 +30,6 @@ export { applyStructureAreaRows, structureAreaRows, } from "../../B06_Section/logic/B06_Section_Structure_Layouts"; -export { bermSpansFromStructures, bermSpecAt } from "../../B06_Section/B06_Section_UI_Page_Persist"; export { parseDtmGrid } from "../../common_util/common_util_surface_sampler"; export { computeMassHaul } from "../../common_util/common_util_mass_haul"; export { writeState, writeStateRaw } from "../../A00_Common/b_page_state"; diff --git a/resources/tester/helper_472_8_section_mode.cjs b/resources/tester/helper_472_8_section_mode.cjs index fecefc6a9..426ec17fe 100644 --- a/resources/tester/helper_472_8_section_mode.cjs +++ b/resources/tester/helper_472_8_section_mode.cjs @@ -49,7 +49,6 @@ refreshDesigns( rockOffsets: [], rockDefault: job.rockDefault, cutSlopes: [], - bermSpans: null, bermDepths: [], choices: [], shoulderExtends: [], diff --git a/resources/tester/helper_472_open_no_draft.mjs b/resources/tester/helper_472_open_no_draft.mjs index cdc034f81..a94e51f53 100644 --- a/resources/tester/helper_472_open_no_draft.mjs +++ b/resources/tester/helper_472_open_no_draft.mjs @@ -183,14 +183,9 @@ await sync.handleDesignChange(20, { two_stage_slope: true }).catch(() => undefin out.choice = Object.fromEntries(m.crossDesignChoices("pD", 7)); await sync.handleDesignChange(20, {}).catch(() => undefined); out.choiceAfterEmpty = Object.fromEntries(m.crossDesignChoices("pD", 7)); -// 소단 사본(시설 줄이 열 때 구조물에서 씀) — 떠남 확인에 안 잡힘 · 진짜 고침은 잡힘 +// 결과 통 칸(계산 묶음 결과) — 떠남 확인에 안 잡힘 · 진짜 고침은 잡힘(소단 사본 칸은 걷음 · 577-2) sessionStorage.clear(); -m.writeState( - "berm", - [{ start_m: 0, end_m: 40, width_m: 1, interval_m: 5, slope_deg: 45, depth_m: 1 }], - "pB", - 7, -); +m.writeState("design-stations", [{ chainage_m: 0 }], "pB"); out.dirtyBermOnly = m.designDirty("pB"); m.writeCrossDesignChoice("pB", 7, 20, { ditch_choice: false }); out.dirtyWithEdit = m.designDirty("pB"); diff --git a/resources/tester/helper_473_lane_ready.cjs b/resources/tester/helper_473_lane_ready.cjs index 3db3902a4..088c8701b 100644 --- a/resources/tester/helper_473_lane_ready.cjs +++ b/resources/tester/helper_473_lane_ready.cjs @@ -35,8 +35,6 @@ const STUBS = { }), "ui_template_elements.ts": anyModule(), "B04_PreProcess_Api_Fetch.ts": anyModule(), - // 소단 사본(467) — 끌어오는 줄이 import.meta 를 써 이 로더로 못 읽음 · 소단 몫은 test_272_berm_source(vite 묶음) - "B06_Section_UI_Page_Persist.ts": anyModule({ bermSpansFromStructures: () => [] }), }; const resolve = (base) => diff --git a/resources/tester/test_272_berm_source.py b/resources/tester/test_272_berm_source.py index 380e3368d..6e6253d71 100644 --- a/resources/tester/test_272_berm_source.py +++ b/resources/tester/test_272_berm_source.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- -"""467 (요구 272 · 분석 4장 (B2)) — 소단 원천 한 벌. +"""467 · 577-2 (요구 272 · 434 · 437) — 소단 원천 한 벌 = 측점에 얹은 제원(옹벽처럼). 못박는 것 - - 소단 사본(세션 `berm`) 쓰는 자리 = 페이지 시설 줄 하나 — 구조물 받기 전 안 씀 · - 같으면 안 씀 · 바뀌면 한 번 · 지우면 빈 목록(B05 만 열어도 씀 · 횡단 보기는 안 씀). - - 다시 셈 = 사본 없으면 저장분 `design.berm`(파이썬 `load_berm_spans` 규칙) — - B05 만 연 값 = B06 연 뒤 값 = [저장] 서버 값(`enforce_design_lines`) · 빈 사본 = 소단 없음. + - 측점 얹기 — 구조물 → 측점마다 `section.berm`(덮는 소단 없음 = null · 겹치면 먼저 놓은 것) · + TS `attachBermSpecs` ↔ 파이썬 `attach_berm_specs`. + - 다시 셈 = 얹힌 제원 · 칸 없음(아직 안 얹음) = 저장분 `design.berm` · null = 소단 없음 — + 얹힌 값 = 저장분 값 = [저장] 서버 값(`enforce_design_lines`). - 거울 — TS `bermSpansFromStructures` ↔ 파이썬 `load_berm_spans` · TS `storedBermSpec` ↔ 파이썬 `berm_from`. + - 세션 사본(`berm` 칸 · 시설 줄 `syncBerm`) 없음. Node 헬퍼 `helper_272_berm_source.mjs` 가 vite ssr 로 묶은 진짜 모듈을 부름. """ @@ -25,7 +26,10 @@ ROOT = HERE.parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) -from B06_Section.B06_Section_Engine_Structures_Wall import load_berm_spans # noqa: E402 +from B06_Section.B06_Section_Engine_Structures_Wall import ( # noqa: E402 + attach_berm_specs, + load_berm_spans, +) from B06_Section.B06_Section_Router_Design import berm_from, enforce_design_lines # noqa: E402 from config.config_system import ( # noqa: E402 STANDARD_CROSS_SECTION, @@ -69,6 +73,7 @@ GROUND = [ for o in range(-40, 41) ] STORED_BERM = {"width_m": 1.0, "interval_m": 3.0, "slope_deg": 0.0, "depth_m": 1.0} +CHAINAGES = [0.0, 10.0, 25.0, 30.0, 35.0, 55.0, 65.0, 100.0, 100.5, None] LONG = { "design_profiles": [ { @@ -126,6 +131,7 @@ def out(tmp_path_factory): "storedCases": STORED_CASES, "ground": GROUND, "storedBerm": STORED_BERM, + "chainages": CHAINAGES, }, ensure_ascii=False, ), @@ -163,25 +169,27 @@ def test_거울_저장분_소단_제원(out): assert ts == pytest.approx(want), spec -def test_시설_줄이_사본을_씀(out): - lane = out["lane"] - assert lane["beforeKnown"] == {"copy": None, "writes": 0} # 구조물 받기 전 = 안 씀 - assert lane["loaded"]["writes"] == 1 - assert lane["loaded"]["copy"] == out["spans"][0] # 저장 구조물 소단 구간 - assert lane["same"]["writes"] == 1 # 같은 구간 · 관만 알림 = 안 씀 - assert lane["moved"]["writes"] == 2 - assert lane["moved"]["copy"][0]["start_m"] == 5.0 and lane["moved"]["copy"][0]["end_m"] == 105.0 - assert lane["removed"] == { - "copy": [], - "writes": 3, - } # 소단 지움 = 빈 사본(저장분 소단으로 안 물러섬) +def test_거울_측점_얹기(out, tmp_path): + for index, (structures, ts) in enumerate(zip(STRUCTURE_CASES, out["attach"], strict=True)): + spans = load_berm_spans(_project(tmp_path / f"a{index}", structures)) + py = [{"chainage_m": c} for c in CHAINAGES] + attach_berm_specs(py, spans) + assert [("berm" in r) for r in ts] == [("berm" in r) for r in py], index + for got, exp in zip(ts, py, strict=True): + if exp.get("berm") is None: + assert got.get("berm") is None, (index, got, exp) + else: + assert got["berm"] == pytest.approx(exp["berm"]), (index, got, exp) + first = out["attach"][0] + assert first[1]["berm"] == pytest.approx(STORED_BERM) and first[8]["berm"] is None + assert "berm" not in first[-1] # 누가거리 없음 = 칸 없음 def test_다시_셈_B05만_B06_저장_같은_값(out, tmp_path): r = out["refresh"] - no_copy, same, empty = r["noCopy"], r["copySame"], r["copyEmpty"] - assert no_copy["fill"] == same["fill"] and no_copy["cut"] == same["cut"] # B05 만 = B06 연 뒤 - assert empty["fill"] != no_copy["fill"] # 빈 사본 = 소단 없음(저장분으로 안 물러섬) + no_copy, same, empty = r["unattached"], r["attachedSame"], r["attachedNone"] + assert no_copy["fill"] == same["fill"] and no_copy["cut"] == same["cut"] # 안 얹음 = 저장분 + assert empty["fill"] != no_copy["fill"] # 얹었는데 없음 = 소단 없음(저장분으로 안 물러섬) # [저장] = 서버가 구조물 정본으로 다시 셈 root = _project(tmp_path / "save", SAVED) design = {"ground_type": "soil", "section_mode": "both_fill", "ditch_type": "standard"} @@ -193,11 +201,27 @@ def test_다시_셈_B05만_B06_저장_같은_값(out, tmp_path): assert abs(bare[0]["design"]["fill_area_m2"] - empty["fill"]) < 1e-3 -def test_쓰는_자리_하나(): - sync = (ROOT / "B06_Section" / "B06_Section_UI_Page_Design_Sync.ts").read_text(encoding="utf-8") - assert "writeBermSpans" not in sync and "useStructureIds(projectId, structures)" in sync - lane = (ROOT / "A00_Common" / "design" / "design_facility_lane.ts").read_text(encoding="utf-8") - assert "writeBermSpans(projectId, routeId, next)" in lane +def test_사본_없음(): + for rel in ( + "A00_Common/design/design_facility_lane.ts", + "A00_Common/b_page_state.ts", + "B06_Section/B06_Section_UI_Page_Persist.ts", + "B06_Section/B06_Section_Cross_Refresh.ts", + "A00_Common/design/design_shared.ts", + ): + text = (ROOT / rel).read_text(encoding="utf-8") + for word in ( + "writeBermSpans", + "syncBerm", + "putBerm", + 'readState("berm"', + "bermAt:", + ): + assert word not in text, (rel, word) + shape = (ROOT / "common_util" / "common_util_structure_shape_keys.ts").read_text( + encoding="utf-8" + ) + assert '"berm"' in shape def test_시설_줄_종류_표_전_측점_안_칠함(out): diff --git a/resources/tester/test_323_4_attach_mirror.py b/resources/tester/test_323_4_attach_mirror.py index 95068d93b..3aea2d98d 100644 --- a/resources/tester/test_323_4_attach_mirror.py +++ b/resources/tester/test_323_4_attach_mirror.py @@ -188,10 +188,14 @@ def test_every_shape_lands(ts: dict) -> None: "revetment", "pavement", "underdrain", + "berm", } <= keys + assert any(section.get("berm") for section in ts["cases"]["all"]) # 소단 = 측점에 얹음(577-2) old = ts["cases"]["all"][1] # 옛 물넘이 자리(5 m) — 걷힘 assert "ford_pavement" not in old - assert all(len(section) == 1 for section in ts["cases"]["empty"]) + # 구조물 없음 = 소단 「얹었는데 없음」(null)만 + assert all(set(section) == {"chainage_m", "berm"} for section in ts["cases"]["empty"]) + assert all(section["berm"] is None for section in ts["cases"]["empty"]) def test_blank_chips(ts: dict) -> None: diff --git a/resources/tester/test_472_open_no_draft.py b/resources/tester/test_472_open_no_draft.py index 4b3b4b46a..f85d26fd8 100644 --- a/resources/tester/test_472_open_no_draft.py +++ b/resources/tester/test_472_open_no_draft.py @@ -104,6 +104,6 @@ def test_세션_고름은_바꾼_칸끼리_얹음(out): assert out["choiceAfterEmpty"] == out["choice"] -def test_소단_사본은_떠남_확인에_안_잡힘(out): +def test_결과_통은_떠남_확인에_안_잡힘(out): assert out["dirtyBermOnly"] is False assert out["dirtyWithEdit"] is True diff --git a/resources/tester/test_477_undo_now_berm.py b/resources/tester/test_477_undo_now_berm.py index b37561579..731947e58 100644 --- a/resources/tester/test_477_undo_now_berm.py +++ b/resources/tester/test_477_undo_now_berm.py @@ -3,7 +3,8 @@ 못박는 것 - 477-3 조작 놓은 직후(찍기 기다림 400 ms 안) ↶ 켜짐 · 단추 맞춤 알림 · 바뀐 것 없는 누름은 안 켜짐. - 기다림 중 ↶ = 먼저 찍고 되돌림(한 번에 조작 전) · ↷ = 다시 · 서버 다시 셈 뒤 쓰기 묶음은 그대로 한 걸음. - - 477-4 `berm` = 결과 통(사진 · 떠남 물음 밖) · 떠남 제외 목록에 사본 줄 없음. + - 477-4 결과 통 칸은 사진 밖 · 소단 사본 칸 `berm` 은 걷음(577-2 · 측점에 얹힌 제원) · + 떠남 목록에 줄 없음. ./venv/Scripts/python.exe -m pytest resources/tester/test_477_undo_now_berm.py -q """ @@ -41,7 +42,7 @@ const { createProfileHistory, onPageHistory } = loadTs(process.argv[4]); const wait = (ms) => new Promise((r) => setTimeout(r, ms)); const PIPES = "aislo:draft:pipes:p1"; const CROSS = "aislo:draft:crossdesign:p1:7"; -const BERM = "aislo:result:berm:p1:7"; +const BERM = "aislo:result:design-stations:p1"; const out = {}; let changes = 0; onPageHistory({ change: () => (changes += 1) }); @@ -81,7 +82,7 @@ sessionStorage.setItem(PIPES, "b"); lift(); await wait(200); sessionStorage.setItem(CROSS, "y"); -sessionStorage.setItem(BERM, '[{"s":1}]'); // 사본 = 결과 통 — 사진 밖 +sessionStorage.setItem(BERM, '[{"s":1}]'); // 결과 통 칸 — 사진 밖 await wait(300); h2.undo(); out.grouped = [sessionStorage.getItem(PIPES), sessionStorage.getItem(CROSS)]; @@ -128,15 +129,15 @@ def test_기다림_중_되돌리기_먼저_찍고_한_걸음(result) -> None: def test_서버_다시_셈_묶음_그대로(result) -> None: assert result["grouped"] == ["a", "x"] and result["groupedLeft"] is False - assert result["bermKept"] == '[{"s":1}]' # 결과 통 사본은 되돌리기가 안 건드림 + assert result["bermKept"] == '[{"s":1}]' # 결과 통 칸은 되돌리기가 안 건드림 def test_바뀐_것_없는_누름은_안_켜짐(result) -> None: assert result["idleCanUndo"] is False -def test_소단_사본은_결과_통() -> None: +def test_소단_사본_칸_없음() -> None: state = (ROOT / "A00_Common" / "b_page_state.ts").read_text(encoding="utf-8") - assert re.search(r'^\s*berm: \{ bucket: "result", scope: "route" \}', state, re.M) + assert not re.search(r"^\s*berm: \{", state, re.M) # 577-2 — 측점에 얹힌 제원(사본 걷음) leave = (ROOT / "A00_Common" / "design" / "design_leave.ts").read_text(encoding="utf-8") assert '"berm"' not in leave diff --git a/resources/tester/test_511_a_calc_browser.py b/resources/tester/test_511_a_calc_browser.py index 965c8f299..bcf3cec4a 100644 --- a/resources/tester/test_511_a_calc_browser.py +++ b/resources/tester/test_511_a_calc_browser.py @@ -57,9 +57,9 @@ def test_자리마다_판_열쇠_저장_길_안_부름(): assert 'calcOnServerNow("quantity")' in refresh and '"F7.saved"' in refresh shared = _text("A00_Common/design/design_shared.ts") assert 'calcChecked("quantity", "structure.layouts"' in shared - assert 'calcChecked("quantity", "cross.berm_spans"' in shared - lane = _text("A00_Common/design/design_facility_lane.ts") - assert '"C4.copy", bermRequest(structures)' in lane + # 소단 = 측점에 얹힌 제원(577-2) — 사본 · 초안 구간 셈 부름 없음 + assert '"cross.berm_spans"' not in shared + assert '"C4.copy"' not in _text("A00_Common/design/design_facility_lane.ts") mass = _text("B05_Profile/B05_Profile_UI_Profile_MassHaul.ts") assert 'calcFromServer("quantity", "masshaul.series"' in mass assert "openCalcSites(projectId)" in _text("A00_Common/design/design_page.ts")