- H3 — 횡단 patch 바탕을 캐시 설계 사용자 칸(측점마다 통째)으로 · 세션 고침은 벽 · 단 · 쪽 열쇠마다 합침(mergeCrossPatches) - M2 — 벽 ↺ · 연동 풂 = 세션 null(안쪽 열쇠 지움) · 추가 벽 0 · 빈 전체 폭 0 을 세션에 남김 · 새 탭 − 가 저장분 단 수에서 출발 - H2 — 옮긴 관 · 구조물(정본 행 없음) = 지반 등 입력(CARRY_INPUT_KEYS)까지 실음 · 화면도 들고 감 · 서버 기본 설계가 그 입력으로 섬 - H5 — 노선 바꾼 [저장] 본문에 카드 선택 · 암선 · 암 경사 · 표준 횡단면 · 풍선 자리 - M1 — 집수정 내공 화면 범위 = 서버 1.0~2.0 - M5 — /save · /confirm 응답에 버린 patch 측점 · 기본 설계 실패 · 측점 맞춤 실패 · 화면 알림 - M7 — kindreset 을 측점 옮김 · 노선 저장에 따라 옮김 - 시험 test_445_save_patch · 기존 시험 꼴 맞춤(318 · 384 · 428 · 429 · fills_empty) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
130 lines
6.6 KiB
TypeScript
130 lines
6.6 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Page_Patches.ts
|
|
* 확정·임시저장이 보내는 **측점별 편집분(cross_patches) 조립** — 페이지
|
|
* (`_UI_Page.ts`)에서 700줄 제한으로 분리했다(2026-08-25).
|
|
*
|
|
* 세션·제어기에 흩어져 있던 조작값을 누가거리 하나로 합친다. 값은 만들지 않고
|
|
* 모으기만 한다 — 판정·한계는 각 제어기가 이미 끝냈다.
|
|
* ========================================================================== */
|
|
|
|
import type { CrossSectionPatch, StoredWallAdjust, StoredWallSpan } from "../B06_Section_Api_Fetch";
|
|
import type { BasinAdjust } from "../B06_Section_UI_Cross_Culvert_Types";
|
|
import type { FordAdjust } from "../B06_Section_UI_Cross_Ford";
|
|
import type { BoxAdjust } from "../B06_Section_UI_Cross_Box";
|
|
import type { InletStructureChoice } from "../B06_Section_UI_Cross_Culvert";
|
|
|
|
export interface CrossPatchSources {
|
|
/** 암 경계선 오프셋(누가거리 문자열 키). */
|
|
rockOffsets: Map<string, number>;
|
|
/** 측점별 암 절토 경사비(1:n 의 n). **0 은 「표준값을 씀」**(되돌리기)이다. */
|
|
cutSlopeRatios: Map<string, number>;
|
|
/** 측점 개별 표시 반폭(m). */
|
|
stationWidths: Map<string, number>;
|
|
inletStructures: Map<string, InletStructureChoice>;
|
|
basinAdjustments: Map<string, BasinAdjust>;
|
|
/** 벽 역할 → 값 · null = 되돌림(바탕 dict 에서 그 벽을 뺌 · 445 M2). */
|
|
revetAdjusts: Map<number, Record<string, StoredWallAdjust | null>>;
|
|
/** 세션에 있는 쪽만(445 H3 — 없는 쪽은 바탕 값 그대로). */
|
|
extraCounts: Map<number, Partial<{ outlet: number; basin: number }>>;
|
|
/** 다단 단별 구간값(2026-08-29) — 키는 벽 키("extra0"…/"bextra0"…). */
|
|
extraSpans: Map<number, Record<string, StoredWallSpan>>;
|
|
fordAdjusts: Map<number, FordAdjust>;
|
|
boxAdjusts: Map<number, BoxAdjust>;
|
|
linkFlags: Map<number, { detached?: boolean; followGrade?: boolean }>;
|
|
}
|
|
|
|
/** 누가거리별 패치를 합쳐 한 배열로 돌려준다. */
|
|
export function buildCrossPatches(sources: CrossPatchSources): CrossSectionPatch[] {
|
|
const patchByChainage = new Map<number, CrossSectionPatch>();
|
|
const patchFor = (chainageM: number): CrossSectionPatch => {
|
|
const existing = patchByChainage.get(chainageM);
|
|
if (existing) return existing;
|
|
const created: CrossSectionPatch = { chainage_m: chainageM };
|
|
patchByChainage.set(chainageM, created);
|
|
return created;
|
|
};
|
|
sources.rockOffsets.forEach((offset, chainage) => {
|
|
patchFor(Number(chainage)).rock_boundary_offset_m = offset;
|
|
});
|
|
// 측점별 암 절토 경사(2026-09-07) — 0 이면 「표준값을 씀」이라 정본의 옛 값을 덮어 지운다.
|
|
sources.cutSlopeRatios.forEach((ratio, chainage) => {
|
|
patchFor(Number(chainage)).cut_slope_ratio_user = ratio;
|
|
});
|
|
// 개별 표시 반폭(2026-08-06) — design에 병합돼 재접근 시 유지된다.
|
|
sources.stationWidths.forEach((width, chainage) => {
|
|
patchFor(Number(chainage)).display_half_width_m = width;
|
|
});
|
|
sources.inletStructures.forEach((structure, chainage) => {
|
|
patchFor(Number(chainage)).inlet_structure = structure;
|
|
});
|
|
sources.basinAdjustments.forEach((adjust, chainage) => {
|
|
patchFor(Number(chainage)).basin_adjust = adjust;
|
|
});
|
|
// 기슭막이 4축·다단 단 수 — 세션 전용이던 값을 정본에 실어 확정한다(2026-08-24).
|
|
// 되돌림(null) · 한쪽 단 수는 바탕과 합칠 때 풀림(`mergeCrossPatches`) — 혼자 나가면 null 걷고 빈 쪽 0
|
|
sources.revetAdjusts.forEach((adjusts, chainage) => {
|
|
patchFor(chainage).revet_adjust = adjusts as Record<string, StoredWallAdjust>;
|
|
});
|
|
sources.extraCounts.forEach((counts, chainage) => {
|
|
patchFor(chainage).extra_wall_counts = counts as { outlet: number; basin: number };
|
|
});
|
|
// 다단 단별 구간값 — 세션에서 만진 값을 정본에 싣는다(2026-08-29 사용자).
|
|
sources.extraSpans.forEach((spans, chainage) => {
|
|
patchFor(chainage).extra_spans = spans;
|
|
});
|
|
// 세월교 측벽 조작값(2026-08-25) — 배수관 값과 같은 자리에 실어 3D·재계산이 잇는다.
|
|
sources.fordAdjusts.forEach((adjust, chainage) => {
|
|
patchFor(chainage).ford_adjust = adjust;
|
|
});
|
|
sources.boxAdjusts.forEach((adjust, chainage) => {
|
|
patchFor(chainage).box_adjust = adjust;
|
|
});
|
|
// 연동 해제(측점별)·종단경사 반영(전체 공통) — 같은 체계로 정본에 싣는다.
|
|
sources.linkFlags.forEach((flags, chainage) => {
|
|
if (flags.detached !== undefined) patchFor(chainage).revet_link_detached = flags.detached;
|
|
if (flags.followGrade !== undefined) patchFor(chainage).revet_follow_grade = flags.followGrade;
|
|
});
|
|
return [...patchByChainage.values()];
|
|
}
|
|
|
|
/** 벽 · 단 · 쪽 열쇠로 담는 칸 — 세션은 만진 열쇠만 들고 있어 바탕(캐시 설계)과 **열쇠마다** 합침(445 H3). */
|
|
const DICT_KEYS = new Set(["revet_adjust", "extra_spans", "extra_wall_counts"]);
|
|
|
|
type Loose = Record<string, unknown>;
|
|
const isDict = (value: unknown): value is Loose =>
|
|
!!value && typeof value === "object" && !Array.isArray(value);
|
|
/** 안쪽 null = 되돌림 — 그 열쇠를 뺌(서버는 dict 를 통째 갈아 끼움). */
|
|
const withoutNulls = (dict: Loose): Loose =>
|
|
Object.fromEntries(Object.entries(dict).filter(([, value]) => value !== null));
|
|
|
|
/**
|
|
* 저장 patch = 바탕(캐시 설계의 사용자 칸 · 측점마다 통째) 위에 세션 고침을 얹음(445 H2 · H3 · M2).
|
|
* 같은 측점 = 누가거리 0.01 m 글. 벽 · 단 · 쪽 dict 는 열쇠마다 합치고 안쪽 null 은 뺌 · 나머지 칸은 세션이 이김.
|
|
* 바탕에 없는 측점의 세션 고침도 그대로 나감(안쪽 null 걷고 · 단 수 빈 쪽 0).
|
|
*/
|
|
export function mergeCrossPatches(
|
|
base: readonly CrossSectionPatch[],
|
|
overlay: readonly CrossSectionPatch[],
|
|
): CrossSectionPatch[] {
|
|
const out = new Map<string, Loose>();
|
|
const join = (patch: CrossSectionPatch): void => {
|
|
const at = patch.chainage_m.toFixed(2);
|
|
const into = out.get(at) ?? { chainage_m: patch.chainage_m };
|
|
for (const [key, value] of Object.entries(patch)) {
|
|
if (key === "chainage_m" || value === undefined) continue;
|
|
into[key] =
|
|
DICT_KEYS.has(key) && isDict(value)
|
|
? withoutNulls({ ...(isDict(into[key]) ? into[key] : {}), ...value })
|
|
: value;
|
|
}
|
|
out.set(at, into);
|
|
};
|
|
base.forEach(join);
|
|
overlay.forEach(join);
|
|
for (const patch of out.values()) {
|
|
const counts = patch.extra_wall_counts;
|
|
if (isDict(counts)) patch.extra_wall_counts = { outlet: 0, basin: 0, ...counts };
|
|
}
|
|
return [...out.values()] as unknown as CrossSectionPatch[];
|
|
}
|