fix(B06): 횡단 카드 버튼이 서버에 바로 저장하던 것을 캐시로 바꿈
사용자 확정(2026-09-06): 조작은 캐시에 쌓이고 [저장]·[확정]에서만 영구저장소에 나간다. 카드 버튼(지반유형·단면유형·측구·포장·2단 비탈)은 누를 때마다 POST cross-design 이 나가 서버가 계산하고 바로 저장하고 있었다. - 세션 초안 crossdesign 신설(B06_Section_Cross_Design_Session) — 선택값만 담는다. - 계산은 브라우저 재계산 창구 하나로 돌린다(B05·B06 공용). - 재계산 때 세션 선택이 정본보다 우선 — 새로고침 뒤에도 고른 값이 남는다. - [저장]·[확정] patch 에 선택값 + 전 측점 면적을 실어 보낸다. 카드 버튼을 바꾸면 구조물이 없는 측점 면적도 달라지므로 구조물 측점만 보내면 수량이 어긋난다. - 구조물 목록 주석 정정(이미 초안 방식인데 옛 주석만 남아 있었다). 검증: tsc --noEmit 통과, pytest 389 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -126,6 +126,10 @@ export const STATE_REGISTRY = {
|
||||
fordadjust: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:fordadjust:${p}:${r}` },
|
||||
boxadjust: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:boxadjust:${p}:${r}` },
|
||||
extraspan: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:extraspan:${p}:${r}` },
|
||||
/** 횡단 카드 버튼(지반유형·단면유형·측구·포장·2단 비탈) 선택 — 2026-09-06.
|
||||
* 예전에는 버튼을 누를 때마다 서버가 계산해 **바로 저장**했다. 조작은 캐시에 쌓고
|
||||
* [저장]·[확정]에서만 정본으로 나가야 한다(사용자 확정). */
|
||||
crossdesign: { bucket: "draft", scope: "route" },
|
||||
|
||||
/* ── ④ 계산 결과 ─────────────────────────────────────────────────────── */
|
||||
/** 노선·종단 최신 응답. 페이지를 오갈 때 이 값으로 먼저 그린다. */
|
||||
|
||||
@@ -119,9 +119,11 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
|
||||
}
|
||||
|
||||
/* ── 구조물 정본(structures.json) ────────────────────────────────────
|
||||
* 사이드 목록이 바뀌면 곧바로 서버 정본에 저장한다 — 화면에만 남겨 두면 새로고침에
|
||||
* 사라지고, 다른 창과도 어긋난다. 판번호가 밀리면(다른 창이 먼저 저장) 최신본을
|
||||
* 받아 화면을 맞추고 사용자에게 알린다. */
|
||||
* 목록이 바뀌면 **세션 초안**에 담고(`writePending`), 정본에는 [저장]·[확정]에서만
|
||||
* 쓴다(`saveStructuresIfDirty`). 판번호가 밀리면(다른 창이 먼저 저장) 최신본을
|
||||
* 받아 화면을 맞추고 사용자에게 알린다.
|
||||
* (옛 주석은 「곧바로 서버에 저장한다」였다 — 2026-08-29 에 초안 방식으로 바뀌었고
|
||||
* 주석만 남아 있었다. 2026-09-06 정정.) */
|
||||
let structureRevision = 0;
|
||||
let structureSaving: Promise<void> = Promise.resolve();
|
||||
|
||||
|
||||
@@ -506,8 +506,15 @@ export interface CrossSectionPatch {
|
||||
/** 연동 해제(측점별)·종단경사 반영(전체 공통) — 2026-08-24 사용자. */
|
||||
revet_link_detached?: boolean;
|
||||
revet_follow_grade?: boolean;
|
||||
/** 구조물이 선 측점의 폐회로 절·성토 면적(㎡) — 브라우저가 계산해 보낸다
|
||||
* (2026-09-06 사용자 확정). 구조물이 없는 측점에는 싣지 않는다. */
|
||||
/** 카드 버튼 선택(2026-09-06) — 예전에는 버튼을 누를 때 서버가 계산·저장했다.
|
||||
* 이제 조작은 세션 초안에 쌓이고 [저장]·[확정]에서 이 patch 로만 나간다. */
|
||||
ground_type?: string;
|
||||
section_mode?: string;
|
||||
ditch_side?: string | null;
|
||||
ditch_type?: string | null;
|
||||
paved?: boolean;
|
||||
two_stage_slope?: boolean;
|
||||
/** 절·성토 면적(㎡) — 브라우저가 계산해 보낸다(2026-09-06 사용자 확정). */
|
||||
cut_area_m2?: number;
|
||||
fill_area_m2?: number;
|
||||
cut_soil_area_m2?: number;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_Cross_Design_Session.ts
|
||||
* 횡단 카드 버튼(지반유형·단면유형·측구·포장·2단 비탈) 선택의 **세션 보관소**.
|
||||
*
|
||||
* 왜 생겼나(2026-09-06 사용자 확정) — 예전에는 버튼을 누를 때마다
|
||||
* `POST …/sections/{route}/cross-design` 이 나가 서버가 계산하고 **바로 저장**했다.
|
||||
* 조작은 캐시에 쌓이고 [저장]·[확정]에서만 정본으로 나가야 한다는 규칙에 어긋난다.
|
||||
* 계산은 브라우저가 하고(`common_util_cross_design.ts`), 선택값은 여기 담긴다.
|
||||
*
|
||||
* 담는 것은 **사용자가 고른 것만**이다 — 계산 결과(면적·설계선)는 담지 않는다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { readState, writeState } from "../A00_Common/b_page_state";
|
||||
|
||||
/** 카드에서 고를 수 있는 값 한 벌 — 서버 `CrossDesignRequest` 와 같은 이름을 쓴다. */
|
||||
export interface CrossDesignChoice {
|
||||
ground_type: string;
|
||||
section_mode: string;
|
||||
ditch_side?: string | null;
|
||||
ditch_type?: string | null;
|
||||
paved?: boolean;
|
||||
two_stage_slope?: boolean;
|
||||
}
|
||||
|
||||
type ChoiceMap = Record<string, CrossDesignChoice>;
|
||||
|
||||
/** 측점 키 — 암 경계선 저장소와 같은 규칙(0.01m 단위). */
|
||||
const keyOf = (chainageM: number): string => chainageM.toFixed(2);
|
||||
|
||||
function readAll(projectId: string, routeId: number): ChoiceMap {
|
||||
return readState<ChoiceMap>("crossdesign", projectId, routeId) ?? {};
|
||||
}
|
||||
|
||||
/** 이 측점의 선택값(없으면 null). */
|
||||
export function readCrossDesignChoice(
|
||||
projectId: string,
|
||||
routeId: number,
|
||||
chainageM: number,
|
||||
): CrossDesignChoice | null {
|
||||
return readAll(projectId, routeId)[keyOf(chainageM)] ?? null;
|
||||
}
|
||||
|
||||
/** 선택값을 세션에 담는다. 같은 측점의 앞선 값은 덮어쓴다. */
|
||||
export function writeCrossDesignChoice(
|
||||
projectId: string,
|
||||
routeId: number,
|
||||
chainageM: number,
|
||||
choice: CrossDesignChoice,
|
||||
): void {
|
||||
const all = readAll(projectId, routeId);
|
||||
all[keyOf(chainageM)] = choice;
|
||||
writeState("crossdesign", all, projectId, routeId);
|
||||
}
|
||||
|
||||
/** [저장]·[확정]이 patch 로 실을 목록 — 누가거리(숫자) → 선택값. */
|
||||
export function crossDesignChoices(
|
||||
projectId: string | null,
|
||||
routeId: number | null,
|
||||
): Map<number, CrossDesignChoice> {
|
||||
const choices = new Map<number, CrossDesignChoice>();
|
||||
if (!projectId || routeId === null) return choices;
|
||||
for (const [key, value] of Object.entries(readAll(projectId, routeId))) {
|
||||
const chainage = Number(key);
|
||||
if (Number.isFinite(chainage) && value) choices.set(chainage, value);
|
||||
}
|
||||
return choices;
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import type { StandardCrossSectionSpec } from "@util/common_util_cross_design";
|
||||
import { previewCrossDesigns } from "./B06_Section_Api_Fetch";
|
||||
import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import { readRockBoundarySession } from "./B06_Section_UI_Page_Persist";
|
||||
import { crossDesignChoices } from "./B06_Section_Cross_Design_Session";
|
||||
import {
|
||||
effectiveStandardCross,
|
||||
readRockBoundaryDefault,
|
||||
@@ -153,6 +154,10 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
|
||||
const rockOffsets = readRockOffsets(projectId, input.routeId);
|
||||
const rockDefault = readRockBoundaryDefault(projectId);
|
||||
// 카드 버튼 선택은 세션 초안이 정본보다 새것이다 — 새로고침 뒤에도 고른 값이 남는다
|
||||
// (2026-09-06 사용자 확정: 조작은 캐시, 저장은 [저장]·[확정]).
|
||||
const choices = crossDesignChoices(projectId, input.routeId);
|
||||
const choiceAt = (chainageM: number) => choices.get(Math.round(chainageM * 100) / 100);
|
||||
|
||||
const updated: number[] = [];
|
||||
for (const section of detail.cross_sections) {
|
||||
@@ -169,22 +174,32 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
: typeof storedOffset === "number"
|
||||
? storedOffset
|
||||
: rockDefault;
|
||||
const choice = choiceAt(section.chainage_m);
|
||||
let next;
|
||||
try {
|
||||
next = computeCrossDesign(
|
||||
section.samples ?? [],
|
||||
planElevationAt(alignment, section.chainage_m),
|
||||
{
|
||||
groundType: typeof design.ground_type === "string" ? design.ground_type : "ripping_rock",
|
||||
sectionMode: typeof design.section_mode === "string" ? design.section_mode : "left_cut",
|
||||
ditchSide: typeof design.ditch_side === "string" ? design.ditch_side : null,
|
||||
groundType:
|
||||
choice?.ground_type ??
|
||||
(typeof design.ground_type === "string" ? design.ground_type : "ripping_rock"),
|
||||
sectionMode:
|
||||
choice?.section_mode ??
|
||||
(typeof design.section_mode === "string" ? design.section_mode : "left_cut"),
|
||||
ditchSide:
|
||||
choice?.ditch_side ??
|
||||
(typeof design.ditch_side === "string" ? design.ditch_side : null),
|
||||
// 저장분은 측구가 없으면 `ditch_type: null` 이다 — 서버와 같이 기본형으로 되돌린다.
|
||||
ditchType: typeof design.ditch_type === "string" ? design.ditch_type : "standard",
|
||||
paved: Boolean(design.paved),
|
||||
ditchType:
|
||||
choice?.ditch_type ??
|
||||
(typeof design.ditch_type === "string" ? design.ditch_type : "standard"),
|
||||
paved: choice?.paved ?? Boolean(design.paved),
|
||||
standard,
|
||||
rockBoundaryOffsetM,
|
||||
twoStageSlope:
|
||||
design.two_stage_slope === undefined ? true : Boolean(design.two_stage_slope),
|
||||
choice?.two_stage_slope ??
|
||||
(design.two_stage_slope === undefined ? true : Boolean(design.two_stage_slope)),
|
||||
ditchEnabled: typeof design.ditch_enabled === "boolean" ? design.ditch_enabled : null,
|
||||
// 세월교 월류 하강은 계획선 편집으로 바뀌지 않는다 — 저장분 값을 그대로 잇는다.
|
||||
surfaceDropM: typeof design.surface_drop_m === "number" ? design.surface_drop_m : 0,
|
||||
|
||||
@@ -140,6 +140,18 @@ async def _apply_section_edits(
|
||||
patch["revet_link_detached"] = patch_item.revet_link_detached
|
||||
if patch_item.revet_follow_grade is not None:
|
||||
patch["revet_follow_grade"] = patch_item.revet_follow_grade
|
||||
# 카드 버튼 선택 — 브라우저가 고른 값을 그대로 정본에 얹는다(2026-09-06).
|
||||
for choice_key in (
|
||||
"ground_type",
|
||||
"section_mode",
|
||||
"ditch_side",
|
||||
"ditch_type",
|
||||
"paved",
|
||||
"two_stage_slope",
|
||||
):
|
||||
choice = getattr(patch_item, choice_key)
|
||||
if choice is not None:
|
||||
patch[choice_key] = choice
|
||||
# 구조물 폐회로 면적 — 브라우저가 계산해 보낸 값을 그대로 정본에 얹는다.
|
||||
for area_key in ("cut_area_m2", "fill_area_m2", "cut_soil_area_m2", "cut_rock_area_m2"):
|
||||
value = getattr(patch_item, area_key)
|
||||
|
||||
@@ -153,6 +153,14 @@ class CrossSectionPatch(BaseModel):
|
||||
# 기슭막이 한 벌 전체 공통이라 소유 측점에만 실린다.
|
||||
revet_link_detached: bool | None = None
|
||||
revet_follow_grade: bool | None = None
|
||||
# 카드 버튼 선택(2026-09-06) — 예전에는 버튼을 누를 때 서버가 계산·저장했다.
|
||||
# 이제 조작은 세션 초안에 쌓이고 [저장]·[확정]에서 이 patch 로만 나간다.
|
||||
ground_type: str | None = None
|
||||
section_mode: str | None = None
|
||||
ditch_side: str | None = None
|
||||
ditch_type: str | None = None
|
||||
paved: bool | None = None
|
||||
two_stage_slope: bool | None = None
|
||||
# 구조물이 선 측점의 폐회로 절·성토 면적(㎡) — 브라우저가 계산해 보낸다
|
||||
# (2026-09-06 사용자 확정: 「일단은 브라우저 계산으로」). 서버는 초기값을 만들 때만
|
||||
# 같은 코드를 Node 로 돌린다(`B06_Section_Server_Calc_Node.ts`).
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { writeCrossDesignChoice } from "./B06_Section_Cross_Design_Session";
|
||||
import { CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
|
||||
import { leaveForDashboard } from "../A00_Common/b_missing_data_guard";
|
||||
import { stateKey } from "../A00_Common/b_page_state";
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
type WorkflowState,
|
||||
} from "../A00_Common/b_workflow_nav";
|
||||
import {
|
||||
computeCrossDesign,
|
||||
fetchSectionContext,
|
||||
getSections,
|
||||
type SectionContextResponse,
|
||||
@@ -199,9 +199,6 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
// 그룹 제목 행 클릭 시 접기/펼치기(N-4-1). 액션 버튼 행은 collapsible 아님.
|
||||
attachCollapsible(leftForm);
|
||||
|
||||
// 측점별 최신 요청 시퀀스 — 늦게 도착한 옛 응답을 폐기해 경합을 방지한다.
|
||||
const designRequestSeq = new Map<number, number>();
|
||||
|
||||
/**
|
||||
* 측점 설계 버튼 변경 처리: (1) 선택을 즉시 로컬 반영해 해당 카드만 리프레시(버튼 즉시 반응),
|
||||
* (2) 서버에서 단면적을 계산·저장하고 최신 요청이면 그 카드만 다시 갱신한다. 전체 재렌더 없음.
|
||||
@@ -228,33 +225,19 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
sectionView.refreshCard(chainageM);
|
||||
}
|
||||
|
||||
// (2) 서버 계산 — 최신 요청만 반영. 암 경계 오프셋은 세션 우선값을 실어 2단계 무릎을 계산시킨다.
|
||||
const seq = (designRequestSeq.get(chainageM) ?? 0) + 1;
|
||||
designRequestSeq.set(chainageM, seq);
|
||||
try {
|
||||
const response = await computeCrossDesign(projectId, currentRouteId, {
|
||||
chainage_m: chainageM,
|
||||
...change,
|
||||
rock_boundary_offset_m: rockBoundaryControl.offsetFor(target),
|
||||
standard_cross_section: standardPanel?.getValues(),
|
||||
});
|
||||
if (designRequestSeq.get(chainageM) !== seq) return;
|
||||
target.design = {
|
||||
...response.design,
|
||||
inlet_structure: target.design?.inlet_structure,
|
||||
basin_adjust: target.design?.basin_adjust,
|
||||
revet_adjust: target.design?.revet_adjust,
|
||||
extra_wall_counts: target.design?.extra_wall_counts,
|
||||
extra_spans: target.design?.extra_spans,
|
||||
revet_link_detached: target.design?.revet_link_detached,
|
||||
revet_follow_grade: target.design?.revet_follow_grade,
|
||||
};
|
||||
sectionView.refreshCard(chainageM);
|
||||
} catch (error) {
|
||||
if (designRequestSeq.get(chainageM) !== seq) return;
|
||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||
showToast(`${L("B06_Design_Failed")}${detail}`, "error");
|
||||
}
|
||||
// (2) 선택은 **세션 초안**으로 남긴다 — 화면을 오가거나 새로고침해도 남고,
|
||||
// [저장]·[확정] 때 한 번에 정본으로 나간다(2026-09-06 사용자 확정: 캐시가 저절로
|
||||
// 영구저장소로 새면 안 된다). 예전에는 여기서 서버가 계산하고 바로 저장했다.
|
||||
writeCrossDesignChoice(projectId, currentRouteId, chainageM, {
|
||||
ground_type: change.ground_type,
|
||||
section_mode: change.section_mode,
|
||||
ditch_side: change.ditch_side ?? null,
|
||||
ditch_type: change.ditch_type,
|
||||
paved: change.paved,
|
||||
two_stage_slope: change.two_stage_slope,
|
||||
});
|
||||
// (3) 계산은 브라우저 안에서 — B05·B06 이 같이 쓰는 창구 하나로 돌린다.
|
||||
await reconcileStaleDesigns({ force: true });
|
||||
}
|
||||
|
||||
/** 현재 design 값에서 재계산용 change를 복원한다(암 경계 오프셋 변경 시 재계산 트리거). */
|
||||
@@ -294,7 +277,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
* 없으면 저장분(profile_alignment.edits)을 쓴다. 측점별 사용자 선택값(지반유형·단면유형·
|
||||
* 측구·암 경계)은 서버가 저장분에서 유지하고, 세션에만 있는 암 경계 오프셋은 함께 실어 보낸다.
|
||||
*/
|
||||
async function reconcileStaleDesigns(): Promise<void> {
|
||||
async function reconcileStaleDesigns(options?: { force?: boolean }): Promise<void> {
|
||||
if (!sectionDetail || !projectId || currentRouteId === null) return;
|
||||
const draft = readAlignmentDraft(currentRouteId);
|
||||
// 낡음 판정은 B05와 **같은 규칙** 하나뿐이다(공용 판정 — 계획고 어긋남 +
|
||||
@@ -304,7 +287,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
// 저장분끼리는 늘 일치해 「낡지 않음」으로 나오는데, B05가 세션에 남긴 편집은 그 안에
|
||||
// 없어 B06이 재계산을 통째로 건너뛰었다 — 같은 시점에 B05 절토 4,774.1㎥ ↔ B06
|
||||
// 4,515.0㎥ 로 갈렸다(2026-09-03 실측). 재계산은 브라우저 안에서 끝나 값싸다.
|
||||
if (!draft && !hasStaleDesigns(sectionDetail)) return;
|
||||
if (!options?.force && !draft && !hasStaleDesigns(sectionDetail)) return;
|
||||
// 진입 정합은 화면을 잠그지 않는다 — 카드가 도착하는 대로 조용히 갱신된다
|
||||
// (CLAUDE.md 5장).
|
||||
try {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
STRUCTURE_AREA_KEYS,
|
||||
structureAreaRows,
|
||||
} from "./B06_Section_Structure_Layouts";
|
||||
import { crossDesignChoices } from "./B06_Section_Cross_Design_Session";
|
||||
import type { StandardCrossSection } from "./B06_Section_Api_Fetch";
|
||||
import type { RockBoundaryControl } from "./B06_Section_UI_Section_View";
|
||||
import { computeMassHaul, massHaulPayload } from "@util/common_util_mass_haul";
|
||||
@@ -176,20 +177,40 @@ export function collectSectionEdits(ctx: SectionPersistContext): {
|
||||
// 구조물 폐회로 면적 — **카드를 그리지 않은 측점까지** 여기서 다 계산해 싣는다
|
||||
// (2026-09-06 사용자 확정: 「일단은 브라우저 계산으로」). 카드 그리기가 고쳐 둔 값에
|
||||
// 기대면 화면에 안 뜬 측점이 표준값으로 남는다.
|
||||
const byChainage = new Map(crossPatches.map((patch) => [patch.chainage_m, patch]));
|
||||
const patchFor = (chainageM: number): CrossSectionPatch => {
|
||||
const existing = byChainage.get(chainageM);
|
||||
if (existing) return existing;
|
||||
const created: CrossSectionPatch = { chainage_m: chainageM };
|
||||
byChainage.set(chainageM, created);
|
||||
crossPatches.push(created);
|
||||
return created;
|
||||
};
|
||||
// 카드 버튼 선택(지반유형·단면유형·측구·포장·2단 비탈) — 세션 초안이 정본으로 나가는
|
||||
// 유일한 길이다(2026-09-06 사용자 확정: 버튼을 누를 때 서버가 저장하지 않는다).
|
||||
crossDesignChoices(ctx.projectId, ctx.routeId()).forEach((choice, chainage) => {
|
||||
const patch = patchFor(chainage);
|
||||
patch.ground_type = choice.ground_type;
|
||||
patch.section_mode = choice.section_mode;
|
||||
if (choice.ditch_side !== undefined) patch.ditch_side = choice.ditch_side;
|
||||
if (choice.ditch_type !== undefined) patch.ditch_type = choice.ditch_type;
|
||||
if (choice.paved !== undefined) patch.paved = choice.paved;
|
||||
if (choice.two_stage_slope !== undefined) patch.two_stage_slope = choice.two_stage_slope;
|
||||
});
|
||||
if (detail) {
|
||||
const areaRows = structureAreaRows(detail.cross_sections);
|
||||
// 유토곡선이 **고쳐진 면적 위에서** 쌓이도록 캐시에도 얹는다(Node 진입점과 같은 순서).
|
||||
applyStructureAreaRows(detail.cross_sections, areaRows);
|
||||
const byChainage = new Map(crossPatches.map((patch) => [patch.chainage_m, patch]));
|
||||
for (const row of areaRows) {
|
||||
let patch = byChainage.get(row.chainage_m);
|
||||
if (!patch) {
|
||||
patch = { chainage_m: row.chainage_m };
|
||||
byChainage.set(row.chainage_m, patch);
|
||||
crossPatches.push(patch);
|
||||
}
|
||||
// 구조물 폐회로 면적 — **카드를 그리지 않은 측점까지** 여기서 다 계산한다
|
||||
// (2026-09-06 사용자 확정: 「일단은 브라우저 계산으로」).
|
||||
applyStructureAreaRows(detail.cross_sections, structureAreaRows(detail.cross_sections));
|
||||
// 면적은 **전 측점**을 싣는다 — 브라우저가 만든 값이 곧 작업본이다. 카드 버튼을
|
||||
// 바꾸면 구조물이 없는 측점의 면적도 달라지므로 구조물 측점만 보내면 수량이 어긋난다.
|
||||
for (const section of detail.cross_sections) {
|
||||
const design = section.design as Record<string, unknown> | undefined;
|
||||
if (!design) continue;
|
||||
let patch: CrossSectionPatch | null = null;
|
||||
for (const key of STRUCTURE_AREA_KEYS) {
|
||||
if (typeof row[key] === "number") patch[key] = row[key];
|
||||
if (typeof design[key] !== "number") continue;
|
||||
patch = patch ?? patchFor(section.chainage_m);
|
||||
patch[key] = design[key] as number;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user