refactor(472-6 6-B 한 벌): 노선 바뀜 넘김 규칙 · 세션 고침 얹기 · [저장] patch 바탕을 순수 TS 로 떼어 냄 — 동작 같음
- logic/B06_Section_Route_Carry.ts = 측점 열쇠 목록 · 측점 열쇠 칸 · 누가거리 칸 넘김(Station_Keys 몸통) · Station_Keys = 세션 겉 - B06_Section_Route_Carry_Patch.ts = 조정창 · 옮긴 고침 · 관 옵션 얹기 · crossPatchesFromCache · 서버용 routeCarryPatches(옛 carryAfterRouteSave 차례) · Section_Adjusts · Section_Store = 겉 · 내줌 - 칸 목록 USER_TOUCHED_KEYS · STRUCTURE_ADJUST_KEYS = logic/B06_Section_Design_Keys.ts · PIPE_MATCH_M = common_util_pipe_match.ts(옛 자리는 내줌) - 시험 헬퍼 · 글 단언 시험 = 옮긴 자리로(목록 한 벌 자리만 바뀜) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,13 +8,14 @@
|
||||
*
|
||||
* 값은 등록표 키 `structure-pick`(② 설계 초안 · 페이지를 오가도 남음)에 실린다.
|
||||
* 어느 화면(3D · 종단 알약 · 목록 · 횡단 카드)에서 골라도 같은 값이 적히고, 다른 쪽은
|
||||
* 이 값을 읽어 따라간다. 관 · 구조물을 찾는 거리 `PIPE_MATCH_M` 도 여기 한 값뿐이다.
|
||||
* 이 값을 읽어 따라간다. 관 · 구조물을 찾는 거리 `PIPE_MATCH_M` 은 여기서 내줌(정의 = common_util).
|
||||
* ========================================================================== */
|
||||
|
||||
import { PIPE_MATCH_M } from "../../common_util/common_util_pipe_match";
|
||||
import { readState, writeState } from "../b_page_state";
|
||||
|
||||
/** 관 · 구조물 찾는 거리(m) — 기준 측점은 정수 m(234-1) · 옛 소수 자료도 같은 자리로 봄. 이 값이 유일하다. */
|
||||
export const PIPE_MATCH_M = 0.51;
|
||||
/** 관 · 구조물 찾는 거리(m) — 정의 = `common_util_pipe_match`(서버 Node 묶음도 같은 값 · 472-6). */
|
||||
export { PIPE_MATCH_M };
|
||||
|
||||
export interface DesignSelection {
|
||||
chainageM: number | null;
|
||||
|
||||
@@ -31,6 +31,10 @@ import {
|
||||
} from "../A00_Common/b_page_state";
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
import type { SavePart } from "../A00_Common/design/design_save_api";
|
||||
import { STRUCTURE_ADJUST_KEYS } from "./logic/B06_Section_Design_Keys";
|
||||
|
||||
/** 구조물(관 · 박스 · 세월교) 조정값 열쇠 — 목록 한 벌 = `logic/B06_Section_Design_Keys`(472-6). */
|
||||
export { STRUCTURE_ADJUST_KEYS };
|
||||
|
||||
/** 측점 하나에 얹을 옵션 조각 — 키는 `pipe_points` 옵션 키 그대로.
|
||||
* 값은 수치가 대부분이나 날개벽 설치("있음"/"없음")처럼 문자열도 있다. */
|
||||
@@ -101,19 +105,6 @@ export function culvertOptionsPart(sessionKey: string | null): SavePart | null {
|
||||
return { body: pending, after: () => writePending(sessionKey, {}) };
|
||||
}
|
||||
|
||||
/** 구조물(관 · 박스 · 세월교) 조정값 열쇠 — 종류를 바꾸면 버리는 몫 · 서버 짝. */
|
||||
export const STRUCTURE_ADJUST_KEYS = [
|
||||
"inlet_structure",
|
||||
"basin_adjust",
|
||||
"revet_adjust",
|
||||
"ford_adjust",
|
||||
"box_adjust",
|
||||
"extra_wall_counts",
|
||||
"extra_spans",
|
||||
"revet_link_detached",
|
||||
"revet_follow_grade",
|
||||
] as const;
|
||||
|
||||
/** 측점 조정값 세션 — 열쇠가 누가거리(`b` · `:역할` 붙은 것 포함)로 시작함. */
|
||||
const STATION_ADJUST_STATES: readonly StateName[] = [
|
||||
"culvertopt",
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
type BermSpec,
|
||||
} from "@util/common_util_cross_berm";
|
||||
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 {
|
||||
crossDesignChoices,
|
||||
@@ -89,44 +90,10 @@ export interface CrossRefreshInput {
|
||||
shouldApply?: () => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 계산이 만들지 않는 **사용자 값** — 다시 계산해도 살려 두고, [저장]·[확정]에도 이 목록으로
|
||||
* 실어 보낸다(`B06_Section_Section_Store`). 서버 `B06_Section_Router_Design.USER_TOUCHED_KEYS`
|
||||
* 와 **짝**이며 갈리면 시험이 깨진다(`tmp/tests/test_b06_user_touched_keys.py`).
|
||||
*
|
||||
* 손으로 나열하는 자리를 하나로 모은 것이다(2026-09-07) — 나열이 흩어져 있어 새 값을
|
||||
* 더할 때 한 곳만 빠지면 그 값이 조용히 사라졌다(`extra_spans` 실사고 `b6941bd2`).
|
||||
*/
|
||||
export const USER_TOUCHED_KEYS = [
|
||||
"display_half_width_m",
|
||||
"inlet_structure",
|
||||
"basin_adjust",
|
||||
"revet_adjust",
|
||||
"ford_adjust",
|
||||
"box_adjust",
|
||||
"extra_wall_counts",
|
||||
"extra_spans",
|
||||
"revet_link_detached",
|
||||
"revet_follow_grade",
|
||||
// 소단 제원 — 사용자가 구간에 놓은 값이라 다시 계산해도 살려 둔다(계획서 3-9).
|
||||
"berm",
|
||||
// 측점별 암 절토 경사(2026-09-07) — 표준을 바꿔도 개별로 고친 측점은 그대로 둔다.
|
||||
"cut_slope_ratio_user",
|
||||
// 측점 구조물 글(138-4 · 횡단 [상세] 모달 맨 위 칸) — 계산 입력이 아님.
|
||||
"structure_note",
|
||||
// 표 위 CAD 칸(260-1 · [상세] 모달 빈 도면) — 계산 입력이 아님.
|
||||
"structure_note_cad",
|
||||
// [상세] 표 손질(179-4) — 고른 칸 · 더한 행 · 열(값은 그릴 때 서버가 다시 셈)
|
||||
"detail_table",
|
||||
// 설계선 쉬프트 = 쪽마다 노견 확장(139-6) — 기하 입력이라 재계산에도 넘김
|
||||
"shoulder_extend_left_m",
|
||||
"shoulder_extend_right_m",
|
||||
// 소단 쪽마다 옮긴 깊이(194-1 · 횡단도 ▲▼) — 기하 입력이라 재계산에도 넘김
|
||||
"berm_depth_left_m",
|
||||
"berm_depth_right_m",
|
||||
] as const;
|
||||
/** 계산이 만들지 않는 **사용자 값** — 목록 한 벌 = `logic/B06_Section_Design_Keys`(서버 Node 입구도 · 472-6). */
|
||||
export { USER_TOUCHED_KEYS };
|
||||
|
||||
/** 다시 계산해도 살려 두는 값 — 위 사용자 값에 **상태를 나르는 둘**을 더한 것. */
|
||||
/** 다시 계산해도 살려 두는 값 — 사용자 값에 **상태를 나르는 둘**을 더한 것. */
|
||||
const PRESERVED_KEYS = ["status", "pavement_suggested", ...USER_TOUCHED_KEYS] as const;
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_Route_Carry_Patch.ts
|
||||
* 세션 고침 → 상세 설계 칸 얹기 · [저장] 횡단 patch 꼴 **한 벌**(472-6 6-B) — 화면 · 세션 · fetch 없음.
|
||||
*
|
||||
* · `layDraftAdjusts` · `layCarriedEdits` — 조정창 · 옮긴 고침 칸(읽는 함수를 받음 · 브라우저 세션 겉 =
|
||||
* `B06_Section_Section_Adjusts`) · `layPipeOptions` — 관 옵션의 조정값 몫(387-4b).
|
||||
* · `crossPatchesFromCache` — 캐시 설계의 사용자 칸 = [저장] patch 바탕(445 · 상세 가게가 내줌).
|
||||
* · `routeCarryPatches` — 노선 바뀐 [저장]: 옛 노선 세션 칸 → 새 노선 서버 상세(측점 열쇠 넘김
|
||||
* `logic/B06_Section_Route_Carry`) → 얹기 → patch(옛 브라우저 길 `carryAfterRouteSave` 와 같은 차례) ·
|
||||
* 서버 Node 입구 `stations.carry` 가 부름(`A00_Common/design/logic/design_calc_node_carry.ts`).
|
||||
* logic 밖 = 구조물 그림 기하(관 벽 · 단 · 구간 · 세월 · BOX 기본값)를 끔 — 서버 Node 묶음 알려진 끌림.
|
||||
* ========================================================================== */
|
||||
|
||||
import type {
|
||||
CrossDesign,
|
||||
CrossSectionPatch,
|
||||
SectionDetailResponse,
|
||||
} from "./B06_Section_Api_Fetch";
|
||||
import { DEFAULT_BASIN_ADJUST, ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { fordAdjustOf } from "./B06_Section_UI_Cross_Ford_Geom";
|
||||
import { DEFAULT_BOX_SIDE_ADJUST } from "./B06_Section_UI_Cross_Box_Geom";
|
||||
import { PIPE_MATCH_M } from "../common_util/common_util_pipe_match";
|
||||
import { inferStationInterval } from "../common_util/common_util_svg";
|
||||
import { applySpanPatch, WALL_MIN_PURE_HEIGHT_M } from "./logic/B06_Section_UI_Cross_Culvert_Const";
|
||||
import { tierSpanOf } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { computeStoredLayouts } from "./B06_Section_Structure_Layouts";
|
||||
import { ROW_CARRY_KEYS } from "./logic/B06_Section_Station_Swap";
|
||||
import { mergeCrossPatches } from "./logic/B06_Section_UI_Page_Patches";
|
||||
import { STRUCTURE_ADJUST_KEYS, USER_TOUCHED_KEYS } from "./logic/B06_Section_Design_Keys";
|
||||
import {
|
||||
carryChainageStates,
|
||||
carryKeyedStates,
|
||||
readKeyedIn,
|
||||
stationKeysFrom,
|
||||
type Keyed,
|
||||
type KeyedState,
|
||||
type Owner,
|
||||
type Raw,
|
||||
} from "./logic/B06_Section_Route_Carry";
|
||||
|
||||
export type Cells = Record<string, unknown>;
|
||||
/** 누가거리(숫자) → 쪽마다 값(세션에 있는 쪽만 · 노견 확장 · 소단 깊이). */
|
||||
export type SideValues = ReadonlyMap<number, Partial<Record<"left" | "right", number>>>;
|
||||
type Obj = Record<string, unknown>;
|
||||
const INLET = ["auto", "revet", "I", "L", "U"];
|
||||
const isObj = (value: unknown): value is Obj => !!value && typeof value === "object";
|
||||
const count = (value: unknown): number =>
|
||||
Number.isInteger(value) && (value as number) > 0 ? (value as number) : 0;
|
||||
|
||||
/** 조정창 읽기와 같은 꼴 — 옛 숫자 = x 이동량 · d 0 = 기본 자리(null). */
|
||||
function wallAdjust(value: unknown): WallAdjust | null {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return { ...ZERO_ADJUST, x: value };
|
||||
if (!isObj(value)) return null;
|
||||
const partial = value as Partial<WallAdjust>;
|
||||
return { ...ZERO_ADJUST, ...partial, d: partial.d ? partial.d : null };
|
||||
}
|
||||
|
||||
/** 저장 안 한 조정창 값(누가거리 열쇠 칸 · `read`)을 상세 설계 칸에 얹음 — 조정 4축 · 형식 · 단 수 · 구간 · 연동 ·
|
||||
* 세월교 · BOX(387-4 · 브라우저 세션 = `withDraftAdjusts` · 서버 = 노선 바뀐 [저장] 넘김 · 472-6). */
|
||||
export function layDraftAdjusts(
|
||||
detail: SectionDetailResponse,
|
||||
read: (name: string) => Cells,
|
||||
): void {
|
||||
const revet = read("revetx");
|
||||
const basin = read("basinadjust");
|
||||
const inlet = read("inletstruct");
|
||||
const extra = read("extrawall");
|
||||
const spans = read("extraspan");
|
||||
const ford = read("fordadjust");
|
||||
const box = read("boxadjust");
|
||||
const link = read("revetlink") as { detached?: Cells; grade?: Cells };
|
||||
for (const section of detail.cross_sections) {
|
||||
const design = section.design as (CrossDesign & Obj) | null | undefined;
|
||||
if (!design) continue;
|
||||
const at = section.chainage_m.toFixed(2);
|
||||
const own = (cells: Cells): Array<[string, unknown]> =>
|
||||
Object.entries(cells)
|
||||
.filter(([key]) => key.startsWith(`${at}:`))
|
||||
.map(([key, value]) => [key.slice(at.length + 1), value]);
|
||||
for (const [role, value] of own(revet)) {
|
||||
const adjust = wallAdjust(value);
|
||||
if (adjust) design.revet_adjust = { ...(design.revet_adjust ?? {}), [role]: adjust };
|
||||
else if (value === null && design.revet_adjust?.[role]) {
|
||||
// null = 되돌림 — 저장분 그 벽을 뺌(445 M2)
|
||||
const { [role]: _gone, ...rest } = design.revet_adjust;
|
||||
design.revet_adjust = rest;
|
||||
}
|
||||
}
|
||||
if (typeof inlet[at] === "string" && INLET.includes(inlet[at] as string))
|
||||
design.inlet_structure = inlet[at] as CrossDesign["inlet_structure"];
|
||||
if (isObj(basin[at])) design.basin_adjust = { ...DEFAULT_BASIN_ADJUST, ...basin[at] };
|
||||
// 세션에 있는 쪽만 — 없는 쪽은 저장분 그대로(445 H3)
|
||||
const saved = design.extra_wall_counts;
|
||||
if (at in extra || `b${at}` in extra)
|
||||
design.extra_wall_counts = {
|
||||
outlet: at in extra ? count(extra[at]) : (saved?.outlet ?? 0),
|
||||
basin: `b${at}` in extra ? count(extra[`b${at}`]) : (saved?.basin ?? 0),
|
||||
};
|
||||
for (const [wall, value] of own(spans)) {
|
||||
const span = value as { lengthM: number; beforeM: number; afterM: number } | null;
|
||||
if (!span || !Number.isFinite(span.beforeM) || !Number.isFinite(span.afterM)) continue;
|
||||
design.extra_spans = {
|
||||
...(design.extra_spans ?? {}),
|
||||
[wall]: { length_m: span.lengthM, before_m: span.beforeM, after_m: span.afterM },
|
||||
};
|
||||
}
|
||||
if (isObj(link.detached) && at in link.detached)
|
||||
design.revet_link_detached = !!link.detached[at];
|
||||
if (isObj(link.grade) && at in link.grade) design.revet_follow_grade = !!link.grade[at];
|
||||
if (isObj(ford[at])) design.ford_adjust = fordAdjustOf(ford[at]);
|
||||
const sides = box[at] as { left?: Obj; right?: Obj } | undefined;
|
||||
if (isObj(sides))
|
||||
design.box_adjust = {
|
||||
left: { ...DEFAULT_BOX_SIDE_ADJUST, ...sides.left },
|
||||
right: { ...DEFAULT_BOX_SIDE_ADJUST, ...sides.right },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** 노선 저장 뒤 새 노선으로 옮긴 세션 고침(417) — 사면 쉬프트 · 소단 깊이 · 측점 폭 · 메모 · 상세 표를 설계 칸에 얹음 ·
|
||||
* 같은 [저장]의 횡단 패치(`crossPatchesFromCache`)가 실음. 조절 4축 · 형식은 `layDraftAdjusts`(브라우저 세션 =
|
||||
* `withCarriedEdits`). */
|
||||
export function layCarriedEdits(
|
||||
detail: SectionDetailResponse,
|
||||
read: (name: string) => Cells,
|
||||
sideValues: { shoulder: SideValues; berm: SideValues },
|
||||
): void {
|
||||
const cells: Array<[string, Cells]> = [
|
||||
["display_half_width_m", read("crossw")],
|
||||
["structure_note", read("structure-notes")],
|
||||
["structure_note_cad", read("structure-note-cads")],
|
||||
["detail_table", read("detail-table")],
|
||||
];
|
||||
const sides: Array<[string, SideValues]> = [
|
||||
["shoulder_extend", sideValues.shoulder],
|
||||
["berm_depth", sideValues.berm],
|
||||
];
|
||||
for (const section of detail.cross_sections) {
|
||||
const design = section.design as (CrossDesign & Obj) | null | undefined;
|
||||
if (!design) continue;
|
||||
const at = section.chainage_m.toFixed(2);
|
||||
for (const [field, values] of cells) if (values[at] !== undefined) design[field] = values[at];
|
||||
for (const [field, values] of sides) {
|
||||
const own = values.get(Number(at));
|
||||
for (const side of ["left", "right"] as const)
|
||||
if (own?.[side] !== undefined) design[`${field}_${side}_m`] = own[side];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시에 얹힌 **사용자 수정분**을 저장 payload로 뽑는다(2026-08-24 사용자 확정 흐름).
|
||||
*
|
||||
* 초기 계산값은 원복용으로 서버에 그대로 있고, 사용자가 만진 값은 이 캐시 한 세트가
|
||||
* 최종본이다. B05 [임시저장]도 이 세트를 함께 보내야 B06에서 만진 구조물 조정이
|
||||
* 영구저장소에 남는다 — 안 보내면 캐시만 비워져 편집이 사라진다.
|
||||
*
|
||||
* 초기 계산값에는 없는 키(사용자가 만져야 생기는 키)를 측점마다 **통째로** 싣는다 — [저장] patch 의 바탕
|
||||
* (445 H3 · 세션 고침은 그 위에 `mergeCrossPatches`). `savedAt`(서버 행 누가거리)을 주면 그 자리 행이 없는
|
||||
* 측점(옮긴 관 · 구조물 · 새 측점 = 서버가 기본 설계로 새로 세움)은 지반 등 입력(`ROW_CARRY_KEYS`)도 실음(445 H2).
|
||||
*/
|
||||
export function crossPatchesFromCache(
|
||||
detail: SectionDetailResponse,
|
||||
kindChanged: readonly number[] = [],
|
||||
savedAt?: readonly number[],
|
||||
): CrossSectionPatch[] {
|
||||
const patches: CrossSectionPatch[] = [];
|
||||
// 관 종류를 바꾼 측점(318-4) — 옛 종류 조정값은 서버가 비움 · 캐시에 남은 옛 값을 다시 안 실음
|
||||
const structureKeys: readonly string[] = STRUCTURE_ADJUST_KEYS;
|
||||
const reset = (at: number) => kindChanged.some((c) => Math.abs(c - at) < PIPE_MATCH_M);
|
||||
const rowless = (at: number) => !!savedAt && savedAt.every((c) => Math.abs(c - at) >= 0.01);
|
||||
for (const at of kindChanged) patches.push({ chainage_m: at, structure_reset: true });
|
||||
for (const section of detail.cross_sections) {
|
||||
const design = section.design;
|
||||
if (!design) continue;
|
||||
const patch: CrossSectionPatch = { chainage_m: section.chainage_m };
|
||||
const skip = reset(section.chainage_m);
|
||||
let touched = false;
|
||||
// 필드를 손으로 나열하지 않는다(2026-09-07) — 목록은 `USER_TOUCHED_KEYS` 한 벌뿐이고
|
||||
// 서버 목록과 짝이다. 나열이 흩어져 있던 탓에 새 값을 더할 때 한 곳이 빠져 그 값이
|
||||
// 조용히 사라졌다(`extra_spans` 실사고 `b6941bd2`).
|
||||
const source = design as unknown as Record<string, unknown>;
|
||||
const target = patch as unknown as Record<string, unknown>;
|
||||
const keys = rowless(section.chainage_m)
|
||||
? [...USER_TOUCHED_KEYS, ...ROW_CARRY_KEYS]
|
||||
: USER_TOUCHED_KEYS;
|
||||
for (const key of keys) {
|
||||
const value = source[key];
|
||||
if (value === undefined || value === null || (skip && structureKeys.includes(key))) continue;
|
||||
target[key] = value;
|
||||
touched = true;
|
||||
}
|
||||
if (touched) patches.push(patch);
|
||||
}
|
||||
return patches;
|
||||
}
|
||||
|
||||
/** 관 하나 — 자리 · 폼 칸(관 초안 · 저장 관 `pipe_points` 같은 꼴). */
|
||||
export interface OptionPipe {
|
||||
chainage_m: number;
|
||||
options?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
const isDefault = (a: WallAdjust): boolean =>
|
||||
Math.abs(a.x) < 1e-9 && a.d == null && a.h == null && a.m == null;
|
||||
const round1 = (value: number): number => Math.round(value * 10) / 10;
|
||||
|
||||
/** 조정창 `update` 와 같은 꼴 — x · d 는 그대로 · h · m 만(폼 한 칸이 주인 · 빈 칸 = 자동 null). */
|
||||
function setWall(design: CrossDesign, role: string, h: number | null, m: string | null): void {
|
||||
const current = { ...ZERO_ADJUST, ...(design.revet_adjust?.[role] ?? {}) } as WallAdjust;
|
||||
const next: WallAdjust = { ...current, h: h === null ? null : round1(h), m };
|
||||
const bucket = { ...(design.revet_adjust ?? {}) };
|
||||
if (isDefault(next)) delete bucket[role];
|
||||
else bucket[role] = next;
|
||||
design.revet_adjust = bucket;
|
||||
}
|
||||
|
||||
/** 관 옵션의 조정값 몫을 상세에 얹음(폼 칸 전부 = 되보내기와 같은 규칙) — 같은 값이면 그대로. */
|
||||
export function layPipeOptions(
|
||||
detail: SectionDetailResponse,
|
||||
pipes: ReadonlyArray<OptionPipe>,
|
||||
): void {
|
||||
const sections = detail.cross_sections;
|
||||
for (const pipe of pipes) {
|
||||
const options = pipe.options;
|
||||
if (!options) continue;
|
||||
const near = (at: number): boolean => Math.abs(at - pipe.chainage_m) < PIPE_MATCH_M;
|
||||
// 물넘이포장 = 제원만(66-2) · 벽 없음
|
||||
if (sections.some((s) => s.ford_pavement && near(s.ford_pavement.chainage_m ?? s.chainage_m)))
|
||||
continue;
|
||||
const owner = sections.find((s) => near(s.chainage_m));
|
||||
const culvert = owner?.culvert;
|
||||
const design = owner?.design;
|
||||
if (!owner || !culvert || !design) continue;
|
||||
const num = (key: string): number | undefined => {
|
||||
const value = Number(options[key]);
|
||||
return Number.isFinite(value) ? value : undefined;
|
||||
};
|
||||
for (const role of ["inlet", "outlet"] as const) {
|
||||
const form = options[`${role}_revet_form`];
|
||||
setWall(
|
||||
design,
|
||||
role,
|
||||
num(`${role}_revet_height_m`) ?? null,
|
||||
typeof form === "string" ? form : null,
|
||||
);
|
||||
}
|
||||
if (culvert.hidden_pipe && !owner.ford && !owner.box) continue;
|
||||
// 흙막이 n 단 = 지형이 세운 유출 다단(`pipeExtraCount` 와 같은 셈)
|
||||
const outlet = design.extra_wall_counts?.outlet ?? 0;
|
||||
const probe = {
|
||||
...owner,
|
||||
design: { ...design, extra_wall_counts: { basin: 0, ...design.extra_wall_counts, outlet } },
|
||||
};
|
||||
const tiers = outlet
|
||||
? (computeStoredLayouts(probe, sections)?.culvert?.extraWalls.length ?? outlet)
|
||||
: 0;
|
||||
for (let n = 1; n <= tiers; n++) {
|
||||
const role = `extra${n - 1}`;
|
||||
const value = (item: string): number | undefined => {
|
||||
const raw = options[`soil_add_${n}_${item}`];
|
||||
return raw === undefined || raw === "" || !Number.isFinite(Number(raw))
|
||||
? undefined
|
||||
: Number(raw);
|
||||
};
|
||||
const form = options[`soil_add_${n}_form`];
|
||||
const height = value("height");
|
||||
setWall(
|
||||
design,
|
||||
role,
|
||||
height === undefined ? null : Number(Math.max(height, WALL_MIN_PURE_HEIGHT_M).toFixed(1)),
|
||||
typeof form === "string" && form ? form : null,
|
||||
);
|
||||
const span = { lengthM: value("length"), beforeM: value("before"), afterM: value("after") };
|
||||
if (Object.values(span).every((v) => v === undefined)) continue;
|
||||
const at = tierSpanOf(owner, role);
|
||||
const current = {
|
||||
lengthM: Math.round((at.beforeM + at.afterM) * 10) / 10,
|
||||
beforeM: Math.round(at.beforeM * 10) / 10,
|
||||
afterM: Math.round(at.afterM * 10) / 10,
|
||||
};
|
||||
const next = applySpanPatch(current, span);
|
||||
design.extra_spans = {
|
||||
...(design.extra_spans ?? {}),
|
||||
[role]: { length_m: next.lengthM, before_m: next.beforeM, after_m: next.afterM },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 노선 바뀐 [저장] 넘김 입력(브라우저 `routeCarryInputs` + 서버 저장분 · 472-6 6-B). */
|
||||
export interface RouteCarryInput {
|
||||
/** 새 노선 서버 상세 — 관 · 구조물 · 추가 측점 · 계획고 차례 뒤(옛 길 `loadSectionDetail` 과 같은 때). */
|
||||
detail: SectionDetailResponse;
|
||||
/** 옛 노선 세션 칸 원본 — 측점 열쇠 칸 다섯 · 누가거리 칸 · `kindreset`(상단측 뺌 · 요구 274). */
|
||||
states: Record<string, unknown>;
|
||||
/** 옛 노선 측점 열쇠 목록(브라우저가 세운 것 · 없으면 null). */
|
||||
from_keys: Keyed[] | null;
|
||||
/** 측점을 덮는 관 · 구조물(측점 계획 · 구조물 목록). */
|
||||
owners: Owner[];
|
||||
/** 관 세션 장부(`pipe-ids`). */
|
||||
pipe_ids: { next: number; ids: Array<{ id: string; at: number }> } | null;
|
||||
/** 노선 저장 직전 저장 관(id · 누가거리). */
|
||||
pipes_before: Array<{ id?: string | null; chainage_m: number }>;
|
||||
/** 관 차례 뒤 저장 관(id · 누가거리 · 옵션). */
|
||||
pipes_after: Array<OptionPipe & { id?: string | null }>;
|
||||
}
|
||||
|
||||
/** 관 세션 장부 사본 — 찾기 · 없으면 새 차례(브라우저 `pipeIdNear` · `pipeIdAt` · `pipeChainageOf` 와 같은 규칙). */
|
||||
function pipeBook(book: RouteCarryInput["pipe_ids"]) {
|
||||
const ids = [...(book?.ids ?? [])];
|
||||
let next = book?.next ?? 1;
|
||||
const near = (at: number): string | null => {
|
||||
let best: { id: string; at: number } | undefined;
|
||||
let gap = PIPE_MATCH_M;
|
||||
for (const entry of ids)
|
||||
if (Math.abs(entry.at - at) < gap) [best, gap] = [entry, Math.abs(entry.at - at)];
|
||||
return best?.id ?? null;
|
||||
};
|
||||
return {
|
||||
near,
|
||||
at: (at: number): string => {
|
||||
const hit = near(at);
|
||||
if (hit) return hit;
|
||||
const id = `pipe-s${next++}`;
|
||||
ids.push({ id, at });
|
||||
return id;
|
||||
},
|
||||
chainageOf: (id: string): number | null => {
|
||||
if (!id.startsWith("pipe-")) return null;
|
||||
const hit = ids.find((entry) => entry.id === id);
|
||||
if (hit) return hit.at;
|
||||
const legacy = Number(id.slice("pipe-".length));
|
||||
return Number.isFinite(legacy) ? legacy : null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 누가거리(0.01 m 글) 열쇠 맵 → 누가거리(숫자) 맵(값이 있는 것만). */
|
||||
function byChainage<V>(cells: ReadonlyMap<string, V>): Map<number, V> {
|
||||
const out = new Map<number, V>();
|
||||
for (const [key, value] of cells) {
|
||||
const chainage = Number(key);
|
||||
if (Number.isFinite(chainage) && value) out.set(chainage, value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 카드 선택 한 벌(세션 `crossdesign` · `CrossDesignChoice` 꼴). */
|
||||
interface CardChoice {
|
||||
ground_type?: string;
|
||||
section_mode?: string;
|
||||
ditch_side?: string | null;
|
||||
ditch_type?: string | null;
|
||||
paved?: boolean;
|
||||
two_stage_slope?: boolean;
|
||||
ditch_choice?: boolean | null;
|
||||
}
|
||||
|
||||
type Sides = Partial<Record<"left" | "right", number>>;
|
||||
|
||||
/**
|
||||
* 노선 바뀐 [저장] 횡단 patch — 옛 브라우저 길(`carryAfterRouteSave` → `saveCachedCrossPatches`)과 같은 차례:
|
||||
* 새 측점 열쇠 → 관 옵션 얹기 → 측점 열쇠 칸 · 누가거리 칸 넘김 → 조정 · 옮긴 고침 얹기 → 캐시 사용자 칸 바탕 +
|
||||
* 카드 선택 · 암선 · 암 경사. `outside` = 노선 밖으로 버린 고침 수(측점 단위). `detail` 을 제자리에서 고침.
|
||||
*/
|
||||
export function routeCarryPatches(input: RouteCarryInput): {
|
||||
patches: CrossSectionPatch[];
|
||||
outside: number;
|
||||
} {
|
||||
const { detail } = input;
|
||||
const sections = detail.cross_sections;
|
||||
const book = pipeBook(input.pipe_ids);
|
||||
const policy = (
|
||||
detail.longitudinal?.profile_alignment as
|
||||
{ policy?: { station_interval_m?: number } } | undefined
|
||||
)?.policy;
|
||||
const interval = policy?.station_interval_m ?? inferStationInterval(sections);
|
||||
const from = input.from_keys ?? undefined;
|
||||
const to = stationKeysFrom(sections, interval, input.owners, book);
|
||||
layPipeOptions(detail, input.pipes_after);
|
||||
const states = input.states;
|
||||
const keyed = carryKeyedStates((name) => (states[name] as Raw | undefined) ?? {}, from, to);
|
||||
const newAt = (id: string | null | undefined): number | null =>
|
||||
input.pipes_after.find((pipe) => id != null && pipe.id === id)?.chainage_m ?? null;
|
||||
const chain = carryChainageStates(
|
||||
(name) => (states[name] as Record<string, unknown> | undefined) ?? null,
|
||||
from,
|
||||
to,
|
||||
{
|
||||
keyAt: (key) => {
|
||||
const id = key.startsWith("pipe:") ? key.slice(5) : null;
|
||||
if (!id) return null;
|
||||
return id.startsWith("pipe-") ? book.chainageOf(id) : newAt(id);
|
||||
},
|
||||
movedAt: (chainageM) =>
|
||||
newAt(input.pipes_before.find((pipe) => Math.abs(pipe.chainage_m - chainageM) < 0.01)?.id),
|
||||
},
|
||||
);
|
||||
const cells = (name: string): Cells => chain.states[name] ?? {};
|
||||
const read = <V>(name: KeyedState) => readKeyedIn<V>(keyed.states[name], to);
|
||||
layDraftAdjusts(detail, cells);
|
||||
layCarriedEdits(detail, cells, {
|
||||
shoulder: byChainage(read<Sides>("shoulderext")),
|
||||
berm: byChainage(read<Sides>("bermdepth")),
|
||||
});
|
||||
const kindChanged = Object.keys(chain.states.kindreset ?? {}).map(Number);
|
||||
const savedAt = sections.map((section) => section.chainage_m);
|
||||
const base = crossPatchesFromCache(detail, kindChanged, savedAt);
|
||||
// 세션에만 있는 고침(카드 선택 · 암선 · 암 경사) — 옛 `sessionOverlay` 와 같은 칸(445 H5)
|
||||
const overlay: CrossSectionPatch[] = [];
|
||||
byChainage(read<CardChoice>("crossdesign")).forEach((choice, chainage) => {
|
||||
const patch: Record<string, unknown> = {
|
||||
chainage_m: chainage,
|
||||
ground_type: choice.ground_type,
|
||||
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 (typeof choice.ditch_choice === "boolean") patch.ditch_choice = choice.ditch_choice;
|
||||
overlay.push(patch as unknown as CrossSectionPatch);
|
||||
});
|
||||
for (const [chainage, offset] of read<unknown>("rockb"))
|
||||
if (typeof offset === "number")
|
||||
overlay.push({ chainage_m: Number(chainage), rock_boundary_offset_m: offset });
|
||||
for (const [chainage, ratio] of read<number>("cutslope"))
|
||||
overlay.push({ chainage_m: Number(chainage), cut_slope_ratio_user: ratio });
|
||||
const patches = mergeCrossPatches(
|
||||
base,
|
||||
overlay.filter((patch) => Number.isFinite(patch.chainage_m)),
|
||||
);
|
||||
return { patches, outside: keyed.outside.size + chain.outside.size };
|
||||
}
|
||||
@@ -13,93 +13,25 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { readState } from "../A00_Common/b_page_state";
|
||||
import type { CrossDesign, SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import { DEFAULT_BASIN_ADJUST, ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { fordAdjustOf } from "./B06_Section_UI_Cross_Ford_Geom";
|
||||
import { DEFAULT_BOX_SIDE_ADJUST } from "./B06_Section_UI_Cross_Box_Geom";
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
import { applySpanPatch, WALL_MIN_PURE_HEIGHT_M } from "./logic/B06_Section_UI_Cross_Culvert_Const";
|
||||
import { tierSpanOf } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { computeStoredLayouts } from "./B06_Section_Structure_Layouts";
|
||||
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import { readBermDepths, readShoulderExtends } from "./B06_Section_Cross_Design_Session";
|
||||
import {
|
||||
layCarriedEdits,
|
||||
layDraftAdjusts,
|
||||
layPipeOptions,
|
||||
type Cells,
|
||||
type OptionPipe,
|
||||
} from "./B06_Section_Route_Carry_Patch";
|
||||
|
||||
type Cells = Record<string, unknown>;
|
||||
type Obj = Record<string, unknown>;
|
||||
const INLET = ["auto", "revet", "I", "L", "U"];
|
||||
const isObj = (value: unknown): value is Obj => !!value && typeof value === "object";
|
||||
const count = (value: unknown): number =>
|
||||
Number.isInteger(value) && (value as number) > 0 ? (value as number) : 0;
|
||||
|
||||
/** 조정창 읽기와 같은 꼴 — 옛 숫자 = x 이동량 · d 0 = 기본 자리(null). */
|
||||
function wallAdjust(value: unknown): WallAdjust | null {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return { ...ZERO_ADJUST, x: value };
|
||||
if (!isObj(value)) return null;
|
||||
const partial = value as Partial<WallAdjust>;
|
||||
return { ...ZERO_ADJUST, ...partial, d: partial.d ? partial.d : null };
|
||||
}
|
||||
// 얹기 몸통 = `B06_Section_Route_Carry_Patch` 한 벌(서버 노선 바뀐 [저장] 넘김도 · 472-6) — 여기는 세션 겉.
|
||||
export { layPipeOptions, type OptionPipe };
|
||||
|
||||
export function withDraftAdjusts(
|
||||
detail: SectionDetailResponse,
|
||||
projectId: string,
|
||||
routeId: number,
|
||||
): void {
|
||||
const read = (name: Parameters<typeof readState>[0]): Cells =>
|
||||
readState<Cells>(name, projectId, routeId) ?? {};
|
||||
const revet = read("revetx");
|
||||
const basin = read("basinadjust");
|
||||
const inlet = read("inletstruct");
|
||||
const extra = read("extrawall");
|
||||
const spans = read("extraspan");
|
||||
const ford = read("fordadjust");
|
||||
const box = read("boxadjust");
|
||||
const link = read("revetlink") as { detached?: Cells; grade?: Cells };
|
||||
for (const section of detail.cross_sections) {
|
||||
const design = section.design as (CrossDesign & Obj) | null | undefined;
|
||||
if (!design) continue;
|
||||
const at = section.chainage_m.toFixed(2);
|
||||
const own = (cells: Cells): Array<[string, unknown]> =>
|
||||
Object.entries(cells)
|
||||
.filter(([key]) => key.startsWith(`${at}:`))
|
||||
.map(([key, value]) => [key.slice(at.length + 1), value]);
|
||||
for (const [role, value] of own(revet)) {
|
||||
const adjust = wallAdjust(value);
|
||||
if (adjust) design.revet_adjust = { ...(design.revet_adjust ?? {}), [role]: adjust };
|
||||
else if (value === null && design.revet_adjust?.[role]) {
|
||||
// null = 되돌림 — 저장분 그 벽을 뺌(445 M2)
|
||||
const { [role]: _gone, ...rest } = design.revet_adjust;
|
||||
design.revet_adjust = rest;
|
||||
}
|
||||
}
|
||||
if (typeof inlet[at] === "string" && INLET.includes(inlet[at] as string))
|
||||
design.inlet_structure = inlet[at] as CrossDesign["inlet_structure"];
|
||||
if (isObj(basin[at])) design.basin_adjust = { ...DEFAULT_BASIN_ADJUST, ...basin[at] };
|
||||
// 세션에 있는 쪽만 — 없는 쪽은 저장분 그대로(445 H3)
|
||||
const saved = design.extra_wall_counts;
|
||||
if (at in extra || `b${at}` in extra)
|
||||
design.extra_wall_counts = {
|
||||
outlet: at in extra ? count(extra[at]) : (saved?.outlet ?? 0),
|
||||
basin: `b${at}` in extra ? count(extra[`b${at}`]) : (saved?.basin ?? 0),
|
||||
};
|
||||
for (const [wall, value] of own(spans)) {
|
||||
const span = value as { lengthM: number; beforeM: number; afterM: number } | null;
|
||||
if (!span || !Number.isFinite(span.beforeM) || !Number.isFinite(span.afterM)) continue;
|
||||
design.extra_spans = {
|
||||
...(design.extra_spans ?? {}),
|
||||
[wall]: { length_m: span.lengthM, before_m: span.beforeM, after_m: span.afterM },
|
||||
};
|
||||
}
|
||||
if (isObj(link.detached) && at in link.detached)
|
||||
design.revet_link_detached = !!link.detached[at];
|
||||
if (isObj(link.grade) && at in link.grade) design.revet_follow_grade = !!link.grade[at];
|
||||
if (isObj(ford[at])) design.ford_adjust = fordAdjustOf(ford[at]);
|
||||
const sides = box[at] as { left?: Obj; right?: Obj } | undefined;
|
||||
if (isObj(sides))
|
||||
design.box_adjust = {
|
||||
left: { ...DEFAULT_BOX_SIDE_ADJUST, ...sides.left },
|
||||
right: { ...DEFAULT_BOX_SIDE_ADJUST, ...sides.right },
|
||||
};
|
||||
}
|
||||
layDraftAdjusts(detail, (name) => readState<Cells>(name as never, projectId, routeId) ?? {});
|
||||
}
|
||||
|
||||
/** 노선 저장 뒤 새 노선으로 옮긴 세션 고침(417) — 사면 쉬프트 · 소단 깊이 · 측점 폭 · 메모 · 상세 표를 설계 칸에 얹음 ·
|
||||
@@ -109,29 +41,10 @@ export function withCarriedEdits(
|
||||
projectId: string,
|
||||
routeId: number,
|
||||
): void {
|
||||
const read = (name: Parameters<typeof readState>[0]): Cells =>
|
||||
readState<Cells>(name, projectId, routeId) ?? {};
|
||||
const cells: Array<[string, Cells]> = [
|
||||
["display_half_width_m", read("crossw")],
|
||||
["structure_note", read("structure-notes")],
|
||||
["structure_note_cad", read("structure-note-cads")],
|
||||
["detail_table", read("detail-table")],
|
||||
];
|
||||
const sides: Array<[string, ReturnType<typeof readShoulderExtends>]> = [
|
||||
["shoulder_extend", readShoulderExtends(projectId, routeId)],
|
||||
["berm_depth", readBermDepths(projectId, routeId)],
|
||||
];
|
||||
for (const section of detail.cross_sections) {
|
||||
const design = section.design as (CrossDesign & Obj) | null | undefined;
|
||||
if (!design) continue;
|
||||
const at = section.chainage_m.toFixed(2);
|
||||
for (const [field, values] of cells) if (values[at] !== undefined) design[field] = values[at];
|
||||
for (const [field, values] of sides) {
|
||||
const own = values.get(Number(at));
|
||||
for (const side of ["left", "right"] as const)
|
||||
if (own?.[side] !== undefined) design[`${field}_${side}_m`] = own[side];
|
||||
}
|
||||
}
|
||||
layCarriedEdits(detail, (name) => readState<Cells>(name as never, projectId, routeId) ?? {}, {
|
||||
shoulder: readShoulderExtends(projectId, routeId),
|
||||
berm: readBermDepths(projectId, routeId),
|
||||
});
|
||||
}
|
||||
|
||||
/** 관 옵션 조정값 몫 얹기(387-4b) — 관 초안 ?? 저장 관(페이지 자료 줄). 가게 · 자료 줄은 부를 때 늦게 불러옴 —
|
||||
@@ -158,96 +71,3 @@ export async function withPipeOptions(
|
||||
.catch(() => null);
|
||||
if (pipes) layPipeOptions(detail, pipes as OptionPipe[]);
|
||||
}
|
||||
|
||||
/** 관 하나 — 자리 · 폼 칸(관 초안 · 저장 관 `pipe_points` 같은 꼴). */
|
||||
export interface OptionPipe {
|
||||
chainage_m: number;
|
||||
options?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
const isDefault = (a: WallAdjust): boolean =>
|
||||
Math.abs(a.x) < 1e-9 && a.d == null && a.h == null && a.m == null;
|
||||
const round1 = (value: number): number => Math.round(value * 10) / 10;
|
||||
|
||||
/** 조정창 `update` 와 같은 꼴 — x · d 는 그대로 · h · m 만(폼 한 칸이 주인 · 빈 칸 = 자동 null). */
|
||||
function setWall(design: CrossDesign, role: string, h: number | null, m: string | null): void {
|
||||
const current = { ...ZERO_ADJUST, ...(design.revet_adjust?.[role] ?? {}) } as WallAdjust;
|
||||
const next: WallAdjust = { ...current, h: h === null ? null : round1(h), m };
|
||||
const bucket = { ...(design.revet_adjust ?? {}) };
|
||||
if (isDefault(next)) delete bucket[role];
|
||||
else bucket[role] = next;
|
||||
design.revet_adjust = bucket;
|
||||
}
|
||||
|
||||
/** 관 옵션의 조정값 몫을 상세에 얹음(폼 칸 전부 = 되보내기와 같은 규칙) — 같은 값이면 그대로. */
|
||||
export function layPipeOptions(
|
||||
detail: SectionDetailResponse,
|
||||
pipes: ReadonlyArray<OptionPipe>,
|
||||
): void {
|
||||
const sections = detail.cross_sections;
|
||||
for (const pipe of pipes) {
|
||||
const options = pipe.options;
|
||||
if (!options) continue;
|
||||
const near = (at: number): boolean => Math.abs(at - pipe.chainage_m) < PIPE_MATCH_M;
|
||||
// 물넘이포장 = 제원만(66-2) · 벽 없음
|
||||
if (sections.some((s) => s.ford_pavement && near(s.ford_pavement.chainage_m ?? s.chainage_m)))
|
||||
continue;
|
||||
const owner = sections.find((s) => near(s.chainage_m));
|
||||
const culvert = owner?.culvert;
|
||||
const design = owner?.design;
|
||||
if (!owner || !culvert || !design) continue;
|
||||
const num = (key: string): number | undefined => {
|
||||
const value = Number(options[key]);
|
||||
return Number.isFinite(value) ? value : undefined;
|
||||
};
|
||||
for (const role of ["inlet", "outlet"] as const) {
|
||||
const form = options[`${role}_revet_form`];
|
||||
setWall(
|
||||
design,
|
||||
role,
|
||||
num(`${role}_revet_height_m`) ?? null,
|
||||
typeof form === "string" ? form : null,
|
||||
);
|
||||
}
|
||||
if (culvert.hidden_pipe && !owner.ford && !owner.box) continue;
|
||||
// 흙막이 n 단 = 지형이 세운 유출 다단(`pipeExtraCount` 와 같은 셈)
|
||||
const outlet = design.extra_wall_counts?.outlet ?? 0;
|
||||
const probe = {
|
||||
...owner,
|
||||
design: { ...design, extra_wall_counts: { basin: 0, ...design.extra_wall_counts, outlet } },
|
||||
};
|
||||
const tiers = outlet
|
||||
? (computeStoredLayouts(probe, sections)?.culvert?.extraWalls.length ?? outlet)
|
||||
: 0;
|
||||
for (let n = 1; n <= tiers; n++) {
|
||||
const role = `extra${n - 1}`;
|
||||
const value = (item: string): number | undefined => {
|
||||
const raw = options[`soil_add_${n}_${item}`];
|
||||
return raw === undefined || raw === "" || !Number.isFinite(Number(raw))
|
||||
? undefined
|
||||
: Number(raw);
|
||||
};
|
||||
const form = options[`soil_add_${n}_form`];
|
||||
const height = value("height");
|
||||
setWall(
|
||||
design,
|
||||
role,
|
||||
height === undefined ? null : Number(Math.max(height, WALL_MIN_PURE_HEIGHT_M).toFixed(1)),
|
||||
typeof form === "string" && form ? form : null,
|
||||
);
|
||||
const span = { lengthM: value("length"), beforeM: value("before"), afterM: value("after") };
|
||||
if (Object.values(span).every((v) => v === undefined)) continue;
|
||||
const at = tierSpanOf(owner, role);
|
||||
const current = {
|
||||
lengthM: Math.round((at.beforeM + at.afterM) * 10) / 10,
|
||||
beforeM: Math.round(at.beforeM * 10) / 10,
|
||||
afterM: Math.round(at.afterM * 10) / 10,
|
||||
};
|
||||
const next = applySpanPatch(current, span);
|
||||
design.extra_spans = {
|
||||
...(design.extra_spans ?? {}),
|
||||
[role]: { length_m: next.lengthM, before_m: next.beforeM, after_m: next.afterM },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { clearState, readState, readStateRaw, writeState } from "../A00_Common/b_page_state";
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
import { setGeneralStations, setPlusStations } from "../common_util/common_util_structure_station";
|
||||
import { loadSavedPlusStations, plusStationsOf } from "../B05_Profile/B05_Profile_Api_PlusStations";
|
||||
import {
|
||||
@@ -29,6 +28,10 @@ import {
|
||||
} from "./B06_Section_Section_Drafts";
|
||||
import { applyStructureAreaRows, structureAreaRows } from "./B06_Section_Structure_Layouts";
|
||||
import { withDraftAdjusts, withPipeOptions } from "./B06_Section_Section_Adjusts";
|
||||
import { crossPatchesFromCache } from "./B06_Section_Route_Carry_Patch";
|
||||
|
||||
// [저장] patch 바탕 = 한 벌(서버 노선 바뀐 [저장] 넘김도 · 472-6 6-B)
|
||||
export { crossPatchesFromCache };
|
||||
import type {
|
||||
CrossSection,
|
||||
CrossSectionPatch,
|
||||
@@ -47,7 +50,7 @@ import {
|
||||
} from "./B06_Section_Section_Canon";
|
||||
import { fetchSectionDetail, saveSections } from "./B06_Section_Api_Fetch";
|
||||
import { staleKeyAgainstScreen, USER_TOUCHED_KEYS } from "./B06_Section_Cross_Refresh";
|
||||
import { kindChangedChainages, STRUCTURE_ADJUST_KEYS } from "./B06_Section_Api_Culvert_Options";
|
||||
import { kindChangedChainages } from "./B06_Section_Api_Culvert_Options";
|
||||
import type { PipelineStation } from "../A00_Common/design/logic/design_pipeline_types";
|
||||
import type { DesignStoreValues } from "../A00_Common/design/design_store";
|
||||
import { useDesignStore } from "../A00_Common/design/design_store";
|
||||
@@ -60,11 +63,7 @@ import {
|
||||
type Keyed,
|
||||
type KeyedState,
|
||||
} from "./B06_Section_Station_Keys";
|
||||
import {
|
||||
ROW_CARRY_KEYS,
|
||||
sameStationList,
|
||||
swapCrossSections,
|
||||
} from "./logic/B06_Section_Station_Swap";
|
||||
import { sameStationList, swapCrossSections } from "./logic/B06_Section_Station_Swap";
|
||||
import { mergeCrossPatches } from "./logic/B06_Section_UI_Page_Patches";
|
||||
import { crossDesignChoices } from "./B06_Section_Cross_Design_Session";
|
||||
import { readCutSlopeSession, readRockBoundarySession } from "./B06_Section_UI_Page_Persist";
|
||||
@@ -455,53 +454,6 @@ export function peekSectionDetail(
|
||||
return routeId === null ? null : (cache.get(keyOf(projectId, routeId)) ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시에 얹힌 **사용자 수정분**을 저장 payload로 뽑는다(2026-08-24 사용자 확정 흐름).
|
||||
*
|
||||
* 초기 계산값은 원복용으로 서버에 그대로 있고, 사용자가 만진 값은 이 캐시 한 세트가
|
||||
* 최종본이다. B05 [임시저장]도 이 세트를 함께 보내야 B06에서 만진 구조물 조정이
|
||||
* 영구저장소에 남는다 — 안 보내면 캐시만 비워져 편집이 사라진다.
|
||||
*
|
||||
* 초기 계산값에는 없는 키(사용자가 만져야 생기는 키)를 측점마다 **통째로** 싣는다 — [저장] patch 의 바탕
|
||||
* (445 H3 · 세션 고침은 그 위에 `mergeCrossPatches`). `savedAt`(서버 행 누가거리)을 주면 그 자리 행이 없는
|
||||
* 측점(옮긴 관 · 구조물 · 새 측점 = 서버가 기본 설계로 새로 세움)은 지반 등 입력(`ROW_CARRY_KEYS`)도 실음(445 H2).
|
||||
*/
|
||||
export function crossPatchesFromCache(
|
||||
detail: SectionDetailResponse,
|
||||
kindChanged: readonly number[] = [],
|
||||
savedAt?: readonly number[],
|
||||
): CrossSectionPatch[] {
|
||||
const patches: CrossSectionPatch[] = [];
|
||||
// 관 종류를 바꾼 측점(318-4) — 옛 종류 조정값은 서버가 비움 · 캐시에 남은 옛 값을 다시 안 실음
|
||||
const structureKeys: readonly string[] = STRUCTURE_ADJUST_KEYS;
|
||||
const reset = (at: number) => kindChanged.some((c) => Math.abs(c - at) < PIPE_MATCH_M);
|
||||
const rowless = (at: number) => !!savedAt && savedAt.every((c) => Math.abs(c - at) >= 0.01);
|
||||
for (const at of kindChanged) patches.push({ chainage_m: at, structure_reset: true });
|
||||
for (const section of detail.cross_sections) {
|
||||
const design = section.design;
|
||||
if (!design) continue;
|
||||
const patch: CrossSectionPatch = { chainage_m: section.chainage_m };
|
||||
const skip = reset(section.chainage_m);
|
||||
let touched = false;
|
||||
// 필드를 손으로 나열하지 않는다(2026-09-07) — 목록은 `USER_TOUCHED_KEYS` 한 벌뿐이고
|
||||
// 서버 목록과 짝이다. 나열이 흩어져 있던 탓에 새 값을 더할 때 한 곳이 빠져 그 값이
|
||||
// 조용히 사라졌다(`extra_spans` 실사고 `b6941bd2`).
|
||||
const source = design as unknown as Record<string, unknown>;
|
||||
const target = patch as unknown as Record<string, unknown>;
|
||||
const keys = rowless(section.chainage_m)
|
||||
? [...USER_TOUCHED_KEYS, ...ROW_CARRY_KEYS]
|
||||
: USER_TOUCHED_KEYS;
|
||||
for (const key of keys) {
|
||||
const value = source[key];
|
||||
if (value === undefined || value === null || (skip && structureKeys.includes(key))) continue;
|
||||
target[key] = value;
|
||||
touched = true;
|
||||
}
|
||||
if (touched) patches.push(patch);
|
||||
}
|
||||
return patches;
|
||||
}
|
||||
|
||||
/**
|
||||
* [저장] · [확정] 횡단 patch(445) — 바탕 = 캐시 설계의 사용자 칸(측점마다 통째 · 서버 행이 없는 측점은 지반 등
|
||||
* 입력까지) 위에 세션 고침(`overlay` · 벽 · 단 dict 는 열쇠마다 합침 · 안쪽 null = 되돌림). 캐시가 없으면 세션 고침만.
|
||||
|
||||
@@ -22,76 +22,33 @@ import { renameChainageKeys } from "./logic/B06_Section_Station_Swap";
|
||||
import { afterAnySwap } from "./B06_Section_Swap_Bus";
|
||||
import { pipeIdAt, pipeIdNear } from "../A00_Common/design/design_pipe_ids";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
chainageKey,
|
||||
curveMoves,
|
||||
dedupeStationKeys,
|
||||
looseChainage,
|
||||
stationKey,
|
||||
type StationKeyRole,
|
||||
} from "@util/common_util_station_key";
|
||||
import { chainageKey, looseChainage, type StationKeyRole } from "@util/common_util_station_key";
|
||||
import { inferStationInterval } from "@util/common_util_svg";
|
||||
import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import {
|
||||
carryChainageStates as carryChainageCells,
|
||||
carryKeyedStates,
|
||||
chainageIn,
|
||||
HINT,
|
||||
keyIn,
|
||||
KEYED_STATES,
|
||||
NEAR_M,
|
||||
nearestKeyed,
|
||||
rawEntries,
|
||||
ROUTE_CARRY_STATES,
|
||||
stationKeysFrom,
|
||||
type CarryFacilities,
|
||||
type Keyed,
|
||||
type KeyedState,
|
||||
type Owner,
|
||||
type Raw,
|
||||
} from "./logic/B06_Section_Route_Carry";
|
||||
|
||||
/** 측점 열쇠로 담는 세션 칸. */
|
||||
export type KeyedState = "crossdesign" | "shoulderext" | "bermdepth" | "rockb" | "cutslope";
|
||||
export const KEYED_STATES: readonly KeyedState[] = [
|
||||
"crossdesign",
|
||||
"shoulderext",
|
||||
"bermdepth",
|
||||
"rockb",
|
||||
"cutslope",
|
||||
];
|
||||
|
||||
/** 노선이 바뀔 때 넘기는 카드 선택 — 서버 `CARRY_INPUT_KEYS` 중 선택 칸(나머지 셋 = rockb · cutslope · 소단 구간). */
|
||||
const CARRY_CHOICE_KEYS = ["ground_type", "ditch_type", "ditch_choice", "two_stage_slope"] as const;
|
||||
/** 노선이 바뀔 때 통째로 넘기는 칸 — 사면 쉬프트(노견 확장) · 소단 깊이도(417 · 서버 `ROUTE_CARRY_KEYS`). */
|
||||
const CARRY_WHOLE: readonly KeyedState[] = ["rockb", "cutslope", "shoulderext", "bermdepth"];
|
||||
|
||||
/**
|
||||
* 노선 저장 때 새 노선 측점으로 옮기는 **누가거리 열쇠** 칸(측점 옮김 목록 · 417) → 담긴 설계 칸.
|
||||
* ⚠ 짝 — 설계 칸 합 = 서버 `ROUTE_CARRY_KEYS`(측점 열쇠마다) + `STRUCTURE_ADJUST_KEYS`(관 · 구조물 열쇠만) ·
|
||||
* 거울 시험 `resources/tester/test_417_route_carry_mirror.py`.
|
||||
*/
|
||||
export const ROUTE_CARRY_STATES = {
|
||||
revetx: ["revet_adjust"],
|
||||
basinadjust: ["basin_adjust"],
|
||||
inletstruct: ["inlet_structure"],
|
||||
extrawall: ["extra_wall_counts"],
|
||||
extraspan: ["extra_spans"],
|
||||
revetlink: ["revet_link_detached", "revet_follow_grade"],
|
||||
fordadjust: ["ford_adjust"],
|
||||
boxadjust: ["box_adjust"],
|
||||
crossw: ["display_half_width_m"],
|
||||
"structure-notes": ["structure_note"],
|
||||
"structure-note-cads": ["structure_note_cad"],
|
||||
"detail-table": ["detail_table"],
|
||||
shoulderext: ["shoulder_extend_left_m", "shoulder_extend_right_m"],
|
||||
bermdepth: ["berm_depth_left_m", "berm_depth_right_m"],
|
||||
uphill: [], // 상단측(측구 방향) — 프로젝트 칸 · 설계 칸 없음 · 노선 바뀐 저장 때 새 측점으로(443-2)
|
||||
} as const;
|
||||
/** 구조물 조절 칸 — 관 · 구조물 열쇠 측점만 따라감(서버 `carry_cross_inputs` 와 같음). */
|
||||
const STRUCTURE_STATES = new Set<string>([
|
||||
"revetx",
|
||||
"basinadjust",
|
||||
"inletstruct",
|
||||
"extrawall",
|
||||
"extraspan",
|
||||
"revetlink",
|
||||
"fordadjust",
|
||||
"boxadjust",
|
||||
]);
|
||||
|
||||
/** 같은 측점으로 보는 거리 — 옛 초안 열쇠(0.01 m) · 서버 측점(0.001 m) 차이를 받음. */
|
||||
const NEAR_M = 0.01;
|
||||
const HINT = "@at";
|
||||
|
||||
export type Keyed = { key: string; chainageM: number };
|
||||
type Raw = Record<string, unknown> & { [HINT]?: Record<string, number> };
|
||||
// 넘김 규칙 · 칸 목록 = logic 한 벌(서버 Node 입구도 · 472-6 6-B) — 부르는 쪽은 여기서 그대로 받음
|
||||
export { KEYED_STATES, ROUTE_CARRY_STATES, type CarryFacilities, type Keyed, type KeyedState };
|
||||
|
||||
const lists = new Map<string, Keyed[]>();
|
||||
/** 프로젝트마다 구조물 측점 → id(측점 계획 · 구조물 목록). */
|
||||
type Owner = { id: string; kind: "pipe" | "structure"; at: number; role: StationKeyRole };
|
||||
const owners = new Map<string, Owner[]>();
|
||||
let current: { projectId: string; routeId: number } | null = null;
|
||||
|
||||
@@ -145,39 +102,17 @@ export function useStructureIds(
|
||||
owners.set(projectId, known);
|
||||
}
|
||||
|
||||
const looksLikePipe = (section: CrossSection): boolean =>
|
||||
Boolean(section.culvert || section.box) || /^.+ D\d+/.test(section.structure ?? "");
|
||||
|
||||
/** 측점 목록 → 열쇠(같은 열쇠가 둘이면 뒤엣것은 `at:` · `dedupeStationKeys`). 맨 앞 · 맨 끝 = 관 · 구조물이
|
||||
* 얹혀도(서버 병합이 종류를 irregular 로 갈음해도) `end:BP/EP` — 묶음 `buildStations` · 서버 `station_keys` 와 한 규칙(452). */
|
||||
/** 측점 목록 → 열쇠(규칙 = `stationKeysFrom` · 관 id = 관 세션 장부). */
|
||||
export function stationKeysOf(
|
||||
projectId: string | null,
|
||||
sections: ReadonlyArray<CrossSection>,
|
||||
intervalM: number | null,
|
||||
): Keyed[] {
|
||||
const known = projectId ? (owners.get(projectId) ?? []) : [];
|
||||
const planned = known.some((owner) => owner.kind === "pipe");
|
||||
const keys = sections.map((section, index) => {
|
||||
const at = section.chainage_m;
|
||||
let key: string;
|
||||
if (index === 0) key = stationKey("bp", at, intervalM);
|
||||
else if (index === sections.length - 1) key = stationKey("ep", at, intervalM);
|
||||
else if (section.kind === "irregular") {
|
||||
const owner = known.find((entry) => Math.abs(entry.at - at) < NEAR_M);
|
||||
if (owner) key = stationKey("irregular", at, intervalM, owner.id, owner.kind, owner.role);
|
||||
else if (looksLikePipe(section)) {
|
||||
// 관 계획을 아는 동안(초안) 계획에 없는 관 측점 = 옮겨 간 옛 자리 — 세션 id 를 새로 안 지음(411-E)
|
||||
const id = planned ? pipeIdNear(at, projectId) : pipeIdAt(at, projectId);
|
||||
key = id ? stationKey("pipe", at, intervalM, id) : stationKey("irregular", at, intervalM);
|
||||
} else
|
||||
key = section.grid
|
||||
? stationKey("regular", at, intervalM)
|
||||
: stationKey("irregular", at, intervalM);
|
||||
} else key = stationKey(section.kind, at, intervalM);
|
||||
return key;
|
||||
return stationKeysFrom(sections, intervalM, known, {
|
||||
near: (at) => pipeIdNear(at, projectId),
|
||||
at: (at) => pipeIdAt(at, projectId),
|
||||
});
|
||||
const chainages = sections.map((section) => section.chainage_m);
|
||||
return dedupeStationKeys(keys, chainages).map((key, i) => ({ key, chainageM: chainages[i] }));
|
||||
}
|
||||
|
||||
type KeyedDetail = Pick<SectionDetailResponse, "cross_sections" | "longitudinal">;
|
||||
@@ -229,22 +164,12 @@ export function registeredKeys(
|
||||
): string[] {
|
||||
const list = lists.get(scopeOf(projectId, routeId));
|
||||
const computed = stationKeysOf(projectId, sections, intervalM);
|
||||
return sections.map((s, i) => (list && nearest(list, s.chainage_m)?.key) ?? computed[i].key);
|
||||
}
|
||||
|
||||
function nearest(list: Keyed[], chainageM: number): Keyed | undefined {
|
||||
let best: Keyed | undefined;
|
||||
for (const entry of list) {
|
||||
const gap = Math.abs(entry.chainageM - chainageM);
|
||||
if (gap < NEAR_M && (!best || gap < Math.abs(best.chainageM - chainageM))) best = entry;
|
||||
}
|
||||
return best;
|
||||
return sections.map((s, i) => (list && nearestKeyed(list, s.chainage_m)?.key) ?? computed[i].key);
|
||||
}
|
||||
|
||||
/** 누가거리 → 열쇠(목록이 없거나 그 자리 측점이 없으면 `at:`). */
|
||||
export function keyAt(projectId: string, routeId: number, chainageM: number): string {
|
||||
const list = lists.get(scopeOf(projectId, routeId));
|
||||
return (list && nearest(list, chainageM)?.key) ?? `at:${chainageKey(chainageM)}`;
|
||||
return keyIn(lists.get(scopeOf(projectId, routeId)), chainageM);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -257,21 +182,7 @@ export function chainageOfKey(
|
||||
key: string,
|
||||
hint?: number,
|
||||
): number | null {
|
||||
const list = lists.get(scopeOf(projectId, routeId));
|
||||
const hit = list?.find((entry) => entry.key === key);
|
||||
if (hit) return Number(chainageKey(hit.chainageM));
|
||||
const at = looseChainage(key) ?? hint;
|
||||
if (at === undefined) return null;
|
||||
if (!list) return Number(chainageKey(at));
|
||||
const near = nearest(list, at);
|
||||
return near ? Number(chainageKey(near.chainageM)) : null;
|
||||
}
|
||||
|
||||
/** 두 노선 목록 사이 MC 가 움직인 곡선 열쇠(432-3 · 서버 `carry_cross_inputs` 와 같음). */
|
||||
function curveMovesBetween(projectId: string, fromRouteId: number, toRouteId: number) {
|
||||
const pairs = (routeId: number) =>
|
||||
(lists.get(scopeOf(projectId, routeId)) ?? []).map((e) => [e.key, e.chainageM] as const);
|
||||
return curveMoves(pairs(fromRouteId), pairs(toRouteId));
|
||||
return chainageIn(lists.get(scopeOf(projectId, routeId)), key, hint);
|
||||
}
|
||||
|
||||
function readRaw(name: KeyedState, projectId: string, routeId: number): Raw {
|
||||
@@ -279,13 +190,6 @@ function readRaw(name: KeyedState, projectId: string, routeId: number): Raw {
|
||||
return raw && typeof raw === "object" ? raw : {};
|
||||
}
|
||||
|
||||
function entries<V>(raw: Raw): Array<[string, V, number | undefined]> {
|
||||
const hints = raw[HINT] ?? {};
|
||||
return Object.entries(raw)
|
||||
.filter(([key]) => key !== HINT)
|
||||
.map(([key, value]) => [key, value as V, hints[key]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 칸 하나를 누가거리(0.01 m 글) → 값으로 읽음 — 지금 측점에 없는 고침은 뺌(지우지 않음).
|
||||
* 옛 열쇠(누가거리 · at:)는 지금 측점 열쇠로 옮겨 다시 씀(목록이 있을 때).
|
||||
@@ -294,7 +198,7 @@ export function readKeyed<V>(name: KeyedState, projectId: string, routeId: numbe
|
||||
const raw = readRaw(name, projectId, routeId);
|
||||
const out = new Map<string, V>();
|
||||
let moved = false;
|
||||
for (const [key, value, hint] of entries<V>(raw)) {
|
||||
for (const [key, value, hint] of rawEntries<V>(raw)) {
|
||||
const at = chainageOfKey(projectId, routeId, key, hint);
|
||||
if (at === null) continue;
|
||||
out.set(chainageKey(at), value);
|
||||
@@ -319,7 +223,7 @@ export function writeKeyed<V>(
|
||||
const raw = readRaw(name, projectId, routeId);
|
||||
const next: Raw = { [HINT]: {} };
|
||||
const replaced = new Set((only ?? [...byChainage.keys()]).map((at) => chainageKey(Number(at))));
|
||||
for (const [key, value, hint] of entries<unknown>(raw)) {
|
||||
for (const [key, value, hint] of rawEntries<unknown>(raw)) {
|
||||
const at = chainageOfKey(projectId, routeId, key, hint);
|
||||
if (at !== null && (only === undefined || replaced.has(chainageKey(at)))) continue;
|
||||
next[key] = value;
|
||||
@@ -346,30 +250,12 @@ export function carryStationDrafts(
|
||||
toRouteId: number,
|
||||
{ quiet = false }: { quiet?: boolean } = {},
|
||||
): number {
|
||||
const outside = new Set<string>();
|
||||
const curves = curveMovesBetween(projectId, fromRouteId, toRouteId);
|
||||
for (const name of KEYED_STATES) {
|
||||
const raw = readRaw(name, projectId, fromRouteId);
|
||||
const next: Raw = { [HINT]: {} };
|
||||
for (const [key, value] of entries<unknown>(raw)) {
|
||||
// 옛 열쇠(누가거리 · at:)는 옛 목록으로 측점 열쇠부터 · 측점 열쇠(id)는 그대로
|
||||
const fromAt =
|
||||
looseChainage(key) === null ? null : chainageOfKey(projectId, fromRouteId, key);
|
||||
const was = fromAt === null ? key : keyAt(projectId, fromRouteId, fromAt);
|
||||
const stable = curves.get(was) ?? was;
|
||||
const toAt = chainageOfKey(projectId, toRouteId, stable, looseChainage(stable) ?? undefined);
|
||||
const carried = carriedValue(name, value);
|
||||
if (toAt === null || stable.startsWith("at:")) {
|
||||
if (carried !== null) outside.add(stable);
|
||||
continue;
|
||||
}
|
||||
if (carried === null) continue;
|
||||
const toKey = keyAt(projectId, toRouteId, toAt);
|
||||
next[toKey] = carried;
|
||||
next[HINT]![toKey] = toAt;
|
||||
}
|
||||
writeState(name, next, projectId, toRouteId);
|
||||
}
|
||||
const { states, outside } = carryKeyedStates(
|
||||
(name) => readRaw(name, projectId, fromRouteId),
|
||||
lists.get(scopeOf(projectId, fromRouteId)),
|
||||
lists.get(scopeOf(projectId, toRouteId)),
|
||||
);
|
||||
for (const name of KEYED_STATES) writeState(name, states[name], projectId, toRouteId);
|
||||
if (outside.size && !quiet)
|
||||
showToast(`노선 밖 고침 ${outside.size} 건 — 사라진 측점이라 버렸습니다.`, "info");
|
||||
return outside.size;
|
||||
@@ -394,28 +280,6 @@ export function carryStationKeys(projectId: string, routeId: number, next: Keyed
|
||||
return outside;
|
||||
}
|
||||
|
||||
/** 칸마다 넘기는 값 — 넘길 것이 없으면 null. */
|
||||
function carriedValue(name: KeyedState, value: unknown): unknown {
|
||||
if (CARRY_WHOLE.includes(name)) return value;
|
||||
if (name !== "crossdesign" || !value || typeof value !== "object") return null;
|
||||
const source = value as Record<string, unknown>;
|
||||
const picked = Object.fromEntries(
|
||||
CARRY_CHOICE_KEYS.filter((key) => source[key] !== undefined).map((key) => [key, source[key]]),
|
||||
);
|
||||
return Object.keys(picked).length ? picked : null;
|
||||
}
|
||||
|
||||
/** 노선 저장 때 시설 측점 풀이(`carryChainageStates`). */
|
||||
export interface CarryFacilities {
|
||||
/** 옛 저장 시설 측점 누가거리 → 새 노선 그 시설 측점 누가거리(없으면 null). */
|
||||
movedAt?: (chainageM: number) => number | null | undefined;
|
||||
/** 관 · 구조물 열쇠 → 새 노선 누가거리(없으면 null). */
|
||||
keyAt?: (key: string) => number | null | undefined;
|
||||
}
|
||||
|
||||
/** 누가거리 열쇠 머리(`b` · 0.01 m 글 · `:역할`) — 조정창 맵 열쇠 꼴(`renameChainageKeys` 와 같음). */
|
||||
const CHAINAGE_HEAD = /^(b?)(-?\d+(?:\.\d+)?)(:.*)?$/;
|
||||
|
||||
/**
|
||||
* 노선 저장 뒤(새 노선 측점 열쇠를 세운 뒤) 누가거리 열쇠 칸(`ROUTE_CARRY_STATES` 중 측점 열쇠 칸 빼고)을 옛 노선
|
||||
* → 새 노선 측점으로 옮김 — 옛 누가거리 → 옛 측점 열쇠 → 새 누가거리. 구조물 조절은 관 · 구조물 열쇠만 ·
|
||||
@@ -429,62 +293,42 @@ export function carryChainageStates(
|
||||
toRouteId: number,
|
||||
facilities: CarryFacilities = {},
|
||||
): number {
|
||||
const facilityAt = facilities.keyAt ?? (() => null);
|
||||
const outside = new Set<string>();
|
||||
const curves = curveMovesBetween(projectId, fromRouteId, toRouteId);
|
||||
const moveAt = (state: string, text: string): string | null => {
|
||||
const moved = facilities.movedAt?.(Number(text));
|
||||
if (moved !== null && moved !== undefined) return chainageKey(moved);
|
||||
const was = keyAt(projectId, fromRouteId, Number(text));
|
||||
const stable = curves.get(was) ?? was;
|
||||
const structural = stable.startsWith("pipe:") || stable.startsWith("structure:");
|
||||
// 새 목록에 그 열쇠가 없으면 그 시설의 새 자리 · 없으면 같은 자리 측점(초안 측점 = 저장 측점)
|
||||
const toAt =
|
||||
stable.startsWith("at:") || (STRUCTURE_STATES.has(state) && !structural)
|
||||
? null
|
||||
: chainageOfKey(
|
||||
projectId,
|
||||
toRouteId,
|
||||
stable,
|
||||
(structural ? facilityAt(stable) : null) ?? Number(text),
|
||||
);
|
||||
if (toAt === null) outside.add(stable);
|
||||
return toAt === null ? null : chainageKey(toAt);
|
||||
};
|
||||
const rekey = (state: string, cells: Record<string, unknown>): Record<string, unknown> => {
|
||||
const next: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(cells)) {
|
||||
const head = CHAINAGE_HEAD.exec(key);
|
||||
const to = head && moveAt(state, head[2]);
|
||||
if (head && to !== null) next[`${head[1]}${to}${head[3] ?? ""}`] = value;
|
||||
}
|
||||
return next;
|
||||
};
|
||||
for (const state of Object.keys(ROUTE_CARRY_STATES)) {
|
||||
if ((KEYED_STATES as readonly string[]).includes(state)) continue; // 측점 열쇠 칸 = carryStationDrafts
|
||||
const name = state as StateName;
|
||||
const raw = readState<Record<string, unknown>>(name, projectId, fromRouteId);
|
||||
if (!raw || typeof raw !== "object") continue;
|
||||
// 연동 = { detached: {누가거리: …}, grade: {…} } 두 겹
|
||||
const next =
|
||||
state === "revetlink"
|
||||
? Object.fromEntries(
|
||||
Object.entries(raw).map(([part, cells]) => [
|
||||
part,
|
||||
cells && typeof cells === "object"
|
||||
? rekey(state, cells as Record<string, unknown>)
|
||||
: cells,
|
||||
]),
|
||||
)
|
||||
: rekey(state, raw);
|
||||
writeState(name, next, projectId, toRouteId);
|
||||
}
|
||||
// 관 종류 바꿈 자리(318-4 · 프로젝트 칸) — 구조물 조절과 같은 규칙으로 옮김(445 M7)
|
||||
const resets = readState<Record<string, unknown>>("kindreset", projectId);
|
||||
if (resets) writeState("kindreset", rekey("revetx", resets), projectId);
|
||||
const { states, outside } = carryChainageCells(
|
||||
(name) =>
|
||||
name === "kindreset"
|
||||
? readState<Record<string, unknown>>("kindreset", projectId)
|
||||
: readState<Record<string, unknown>>(name as StateName, projectId, fromRouteId),
|
||||
lists.get(scopeOf(projectId, fromRouteId)),
|
||||
lists.get(scopeOf(projectId, toRouteId)),
|
||||
facilities,
|
||||
);
|
||||
for (const [name, next] of Object.entries(states))
|
||||
if (name === "kindreset") writeState("kindreset", next, projectId);
|
||||
else writeState(name as StateName, next, projectId, toRouteId);
|
||||
return outside.size;
|
||||
}
|
||||
|
||||
/** 노선 바뀐 [저장] 본문의 넘김 입력(472-6 6-B) — 서버 Node `stations.carry` 가 같은 규칙으로 새 노선에 얹음.
|
||||
* 칸 = 측점 열쇠 칸 다섯 · 누가거리 칸(상단측 뺌 · 요구 274) · 관 종류 바꿈 자리 · 옛 노선 열쇠 목록 · 측점 주인 ·
|
||||
* 관 세션 장부. */
|
||||
export function routeCarryInputs(projectId: string, fromRouteId: number): Record<string, unknown> {
|
||||
const states: Record<string, unknown> = {};
|
||||
for (const name of KEYED_STATES) states[name] = readRaw(name, projectId, fromRouteId);
|
||||
for (const state of Object.keys(ROUTE_CARRY_STATES)) {
|
||||
if (state === "uphill" || (KEYED_STATES as readonly string[]).includes(state)) continue;
|
||||
const raw = readState<Record<string, unknown>>(state as StateName, projectId, fromRouteId);
|
||||
if (raw && typeof raw === "object") states[state] = raw;
|
||||
}
|
||||
const resets = readState<Record<string, unknown>>("kindreset", projectId);
|
||||
if (resets) states.kindreset = resets;
|
||||
return {
|
||||
states,
|
||||
from_keys: lists.get(scopeOf(projectId, fromRouteId)) ?? null,
|
||||
owners: owners.get(projectId) ?? [],
|
||||
pipe_ids: readState("pipe-ids", projectId) ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 측점이 움직임(갈아끼움 · 옛 0.01 m 글 → 새 글) — 조정창 맵이 안 선 누가거리 칸(B05 만 연 페이지 · 새로고침 ·
|
||||
* 세월 · BOX · 연동)을 세션에서 바로 새 자리로(415-3). 칸 = `ROUTE_CARRY_STATES`(측점 열쇠 칸 빼고) · 조정창
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_Design_Keys.ts
|
||||
* 횡단 설계 칸 목록 한 벌 — 사용자 값(`USER_TOUCHED_KEYS`) · 구조물 조정값(`STRUCTURE_ADJUST_KEYS`).
|
||||
* 자료만(브라우저 · 서버 Node 입구 `stations.carry` 가 같은 목록 · 472-6 6-B) · 서버 짝 = 각 목록 주석.
|
||||
* ========================================================================== */
|
||||
|
||||
/**
|
||||
* 계산이 만들지 않는 **사용자 값** — 다시 계산해도 살려 두고, [저장]·[확정]에도 이 목록으로
|
||||
* 실어 보낸다(`B06_Section_Section_Store` · `B06_Section_Route_Carry_Patch`). 서버 `B06_Section_Router_Design.USER_TOUCHED_KEYS`
|
||||
* 와 **짝**이며 갈리면 시험이 깨진다(`tmp/tests/test_b06_user_touched_keys.py`).
|
||||
*
|
||||
* 손으로 나열하는 자리를 하나로 모은 것이다(2026-09-07) — 나열이 흩어져 있어 새 값을
|
||||
* 더할 때 한 곳만 빠지면 그 값이 조용히 사라졌다(`extra_spans` 실사고 `b6941bd2`).
|
||||
*/
|
||||
export const USER_TOUCHED_KEYS = [
|
||||
"display_half_width_m",
|
||||
"inlet_structure",
|
||||
"basin_adjust",
|
||||
"revet_adjust",
|
||||
"ford_adjust",
|
||||
"box_adjust",
|
||||
"extra_wall_counts",
|
||||
"extra_spans",
|
||||
"revet_link_detached",
|
||||
"revet_follow_grade",
|
||||
// 소단 제원 — 사용자가 구간에 놓은 값이라 다시 계산해도 살려 둔다(계획서 3-9).
|
||||
"berm",
|
||||
// 측점별 암 절토 경사(2026-09-07) — 표준을 바꿔도 개별로 고친 측점은 그대로 둔다.
|
||||
"cut_slope_ratio_user",
|
||||
// 측점 구조물 글(138-4 · 횡단 [상세] 모달 맨 위 칸) — 계산 입력이 아님.
|
||||
"structure_note",
|
||||
// 표 위 CAD 칸(260-1 · [상세] 모달 빈 도면) — 계산 입력이 아님.
|
||||
"structure_note_cad",
|
||||
// [상세] 표 손질(179-4) — 고른 칸 · 더한 행 · 열(값은 그릴 때 서버가 다시 셈)
|
||||
"detail_table",
|
||||
// 설계선 쉬프트 = 쪽마다 노견 확장(139-6) — 기하 입력이라 재계산에도 넘김
|
||||
"shoulder_extend_left_m",
|
||||
"shoulder_extend_right_m",
|
||||
// 소단 쪽마다 옮긴 깊이(194-1 · 횡단도 ▲▼) — 기하 입력이라 재계산에도 넘김
|
||||
"berm_depth_left_m",
|
||||
"berm_depth_right_m",
|
||||
] as const;
|
||||
|
||||
/** 구조물(관 · 박스 · 세월교) 조정값 열쇠 — 종류를 바꾸면 버리는 몫 · 서버 짝. */
|
||||
export const STRUCTURE_ADJUST_KEYS = [
|
||||
"inlet_structure",
|
||||
"basin_adjust",
|
||||
"revet_adjust",
|
||||
"ford_adjust",
|
||||
"box_adjust",
|
||||
"extra_wall_counts",
|
||||
"extra_spans",
|
||||
"revet_link_detached",
|
||||
"revet_follow_grade",
|
||||
] as const;
|
||||
@@ -0,0 +1,300 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_Route_Carry.ts
|
||||
* 노선이 바뀔 때 횡단 고침 넘김 규칙 **한 벌**(PLAN 344-3 · 417 · 472-6 6-B) — 순수 계산(세션 · 화면 없음).
|
||||
*
|
||||
* · 측점 열쇠 목록 = `stationKeysFrom`(일반 grid:k · 관 pipe:<id> · 구조물 structure:<id> · 시종점 end:BP/EP ·
|
||||
* id 모름 at:<0.01 m> · 규칙 = `common_util_station_key`).
|
||||
* · 측점 열쇠 칸 다섯 = `carryKeyedStates` — 넘김 = 카드 선택 넷 · 암 경계 · 암 경사 · 사면 쉬프트 · 소단 깊이 ·
|
||||
* 단면 유형 · 측구 방향은 새 지형이 정함 · 사라진 측점 고침은 버림(수만 셈).
|
||||
* · 누가거리 열쇠 칸(`ROUTE_CARRY_STATES`) = `carryChainageStates` — 옛 누가거리 → 옛 측점 열쇠 → 새 누가거리 ·
|
||||
* 구조물 조절 = 관 · 구조물 열쇠만.
|
||||
* 쓰는 곳 = 브라우저 세션(`B06_Section_Station_Keys` · 끌기 동안 같은 노선 안 목록 갈림) · 서버 Node 입구
|
||||
* `stations.carry`(노선 바뀐 [저장] · `design_calc_node_carry.ts`).
|
||||
* ⚠ 짝 — 설계 칸 합 = 서버 `ROUTE_CARRY_KEYS` + `STRUCTURE_ADJUST_KEYS` · 거울 시험 `test_417_route_carry.py`.
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
chainageKey,
|
||||
curveMoves,
|
||||
dedupeStationKeys,
|
||||
looseChainage,
|
||||
stationKey,
|
||||
type StationKeyRole,
|
||||
} from "../../common_util/common_util_station_key";
|
||||
import type { CrossSection } from "../B06_Section_Api_Fetch";
|
||||
|
||||
/** 측점 열쇠로 담는 세션 칸. */
|
||||
export type KeyedState = "crossdesign" | "shoulderext" | "bermdepth" | "rockb" | "cutslope";
|
||||
export const KEYED_STATES: readonly KeyedState[] = [
|
||||
"crossdesign",
|
||||
"shoulderext",
|
||||
"bermdepth",
|
||||
"rockb",
|
||||
"cutslope",
|
||||
];
|
||||
|
||||
/** 노선이 바뀔 때 넘기는 카드 선택 — 서버 `CARRY_INPUT_KEYS` 중 선택 칸(나머지 셋 = rockb · cutslope · 소단 구간). */
|
||||
const CARRY_CHOICE_KEYS = ["ground_type", "ditch_type", "ditch_choice", "two_stage_slope"] as const;
|
||||
/** 노선이 바뀔 때 통째로 넘기는 칸 — 사면 쉬프트(노견 확장) · 소단 깊이도(417 · 서버 `ROUTE_CARRY_KEYS`). */
|
||||
const CARRY_WHOLE: readonly KeyedState[] = ["rockb", "cutslope", "shoulderext", "bermdepth"];
|
||||
|
||||
/**
|
||||
* 노선 저장 때 새 노선 측점으로 옮기는 **누가거리 열쇠** 칸(측점 옮김 목록 · 417) → 담긴 설계 칸.
|
||||
* ⚠ 짝 — 설계 칸 합 = 서버 `ROUTE_CARRY_KEYS`(측점 열쇠마다) + `STRUCTURE_ADJUST_KEYS`(관 · 구조물 열쇠만) ·
|
||||
* 거울 시험 `resources/tester/test_417_route_carry.py`.
|
||||
*/
|
||||
export const ROUTE_CARRY_STATES = {
|
||||
revetx: ["revet_adjust"],
|
||||
basinadjust: ["basin_adjust"],
|
||||
inletstruct: ["inlet_structure"],
|
||||
extrawall: ["extra_wall_counts"],
|
||||
extraspan: ["extra_spans"],
|
||||
revetlink: ["revet_link_detached", "revet_follow_grade"],
|
||||
fordadjust: ["ford_adjust"],
|
||||
boxadjust: ["box_adjust"],
|
||||
crossw: ["display_half_width_m"],
|
||||
"structure-notes": ["structure_note"],
|
||||
"structure-note-cads": ["structure_note_cad"],
|
||||
"detail-table": ["detail_table"],
|
||||
shoulderext: ["shoulder_extend_left_m", "shoulder_extend_right_m"],
|
||||
bermdepth: ["berm_depth_left_m", "berm_depth_right_m"],
|
||||
uphill: [], // 상단측(측구 방향) — 프로젝트 칸 · 설계 칸 없음 · 노선 바뀐 [저장]은 안 보냄(요구 274)
|
||||
} as const;
|
||||
/** 구조물 조절 칸 — 관 · 구조물 열쇠 측점만 따라감(서버 `carry_cross_inputs` 와 같음). */
|
||||
const STRUCTURE_STATES = new Set<string>([
|
||||
"revetx",
|
||||
"basinadjust",
|
||||
"inletstruct",
|
||||
"extrawall",
|
||||
"extraspan",
|
||||
"revetlink",
|
||||
"fordadjust",
|
||||
"boxadjust",
|
||||
]);
|
||||
|
||||
/** 같은 측점으로 보는 거리 — 옛 초안 열쇠(0.01 m) · 서버 측점(0.001 m) 차이를 받음. */
|
||||
export const NEAR_M = 0.01;
|
||||
export const HINT = "@at";
|
||||
|
||||
export type Keyed = { key: string; chainageM: number };
|
||||
export type Raw = Record<string, unknown> & { [HINT]?: Record<string, number> };
|
||||
/** 측점을 덮는 관 · 구조물(측점 계획 · 구조물 목록). */
|
||||
export type Owner = { id: string; kind: "pipe" | "structure"; at: number; role: StationKeyRole };
|
||||
/** 관 측점의 관 id — 관 장부(세션 `pipe-sN`) · 찾기만(`near`) · 없으면 지음(`at`). */
|
||||
export interface PipeIdBook {
|
||||
near: (chainageM: number) => string | null;
|
||||
at: (chainageM: number) => string;
|
||||
}
|
||||
|
||||
const looksLikePipe = (section: CrossSection): boolean =>
|
||||
Boolean(section.culvert || section.box) || /^.+ D\d+/.test(section.structure ?? "");
|
||||
|
||||
/** 측점 목록 → 열쇠(같은 열쇠가 둘이면 뒤엣것은 `at:` · `dedupeStationKeys`). 맨 앞 · 맨 끝 = 관 · 구조물이
|
||||
* 얹혀도(서버 병합이 종류를 irregular 로 갈음해도) `end:BP/EP` — 묶음 `buildStations` · 서버 `station_keys` 와 한 규칙(452). */
|
||||
export function stationKeysFrom(
|
||||
sections: ReadonlyArray<CrossSection>,
|
||||
intervalM: number | null,
|
||||
known: ReadonlyArray<Owner>,
|
||||
pipes: PipeIdBook | null,
|
||||
): Keyed[] {
|
||||
const planned = known.some((owner) => owner.kind === "pipe");
|
||||
const keys = sections.map((section, index) => {
|
||||
const at = section.chainage_m;
|
||||
if (index === 0) return stationKey("bp", at, intervalM);
|
||||
if (index === sections.length - 1) return stationKey("ep", at, intervalM);
|
||||
if (section.kind !== "irregular") return stationKey(section.kind, at, intervalM);
|
||||
const owner = known.find((entry) => Math.abs(entry.at - at) < NEAR_M);
|
||||
if (owner) return stationKey("irregular", at, intervalM, owner.id, owner.kind, owner.role);
|
||||
if (looksLikePipe(section)) {
|
||||
// 관 계획을 아는 동안(초안) 계획에 없는 관 측점 = 옮겨 간 옛 자리 — 세션 id 를 새로 안 지음(411-E)
|
||||
const id = pipes ? (planned ? pipes.near(at) : pipes.at(at)) : null;
|
||||
return id ? stationKey("pipe", at, intervalM, id) : stationKey("irregular", at, intervalM);
|
||||
}
|
||||
return section.grid
|
||||
? stationKey("regular", at, intervalM)
|
||||
: stationKey("irregular", at, intervalM);
|
||||
});
|
||||
const chainages = sections.map((section) => section.chainage_m);
|
||||
return dedupeStationKeys(keys, chainages).map((key, i) => ({ key, chainageM: chainages[i] }));
|
||||
}
|
||||
|
||||
/** 목록에서 그 자리(0.01 m 안) 가장 가까운 측점. */
|
||||
export function nearestKeyed(list: ReadonlyArray<Keyed>, chainageM: number): Keyed | undefined {
|
||||
let best: Keyed | undefined;
|
||||
for (const entry of list) {
|
||||
const gap = Math.abs(entry.chainageM - chainageM);
|
||||
if (gap < NEAR_M && (!best || gap < Math.abs(best.chainageM - chainageM))) best = entry;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/** 누가거리 → 열쇠(목록이 없거나 그 자리 측점이 없으면 `at:`). */
|
||||
export function keyIn(list: ReadonlyArray<Keyed> | undefined, chainageM: number): string {
|
||||
return (list && nearestKeyed(list, chainageM)?.key) ?? `at:${chainageKey(chainageM)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 열쇠 → 지금 누가거리(0.01 m). 목록에 있으면 그 측점 · 없으면 쓴 때 자리(`hint` · 옛 열쇠는 그 수)에 지금
|
||||
* 측점이 있으면 그 측점 · 목록이 아직 없으면 쓴 때 자리 그대로 · 그 밖 = null(노선 밖).
|
||||
*/
|
||||
export function chainageIn(
|
||||
list: ReadonlyArray<Keyed> | undefined,
|
||||
key: string,
|
||||
hint?: number,
|
||||
): number | null {
|
||||
const hit = list?.find((entry) => entry.key === key);
|
||||
if (hit) return Number(chainageKey(hit.chainageM));
|
||||
const at = looseChainage(key) ?? hint;
|
||||
if (at === undefined) return null;
|
||||
if (!list) return Number(chainageKey(at));
|
||||
const near = nearestKeyed(list, at);
|
||||
return near ? Number(chainageKey(near.chainageM)) : null;
|
||||
}
|
||||
|
||||
/** 두 목록 사이 MC 가 움직인 곡선 열쇠(432-3 · 서버 `carry_cross_inputs` 와 같음). */
|
||||
export function curveMovesIn(
|
||||
from: ReadonlyArray<Keyed> | undefined,
|
||||
to: ReadonlyArray<Keyed> | undefined,
|
||||
): Map<string, string> {
|
||||
const pairs = (list: ReadonlyArray<Keyed> | undefined) =>
|
||||
(list ?? []).map((e) => [e.key, e.chainageM] as const);
|
||||
return curveMoves(pairs(from), pairs(to));
|
||||
}
|
||||
|
||||
/** 칸 원본 → [열쇠, 값, 쓴 때 자리]. */
|
||||
export function rawEntries<V>(raw: Raw): Array<[string, V, number | undefined]> {
|
||||
const hints = raw[HINT] ?? {};
|
||||
return Object.entries(raw)
|
||||
.filter(([key]) => key !== HINT)
|
||||
.map(([key, value]) => [key, value as V, hints[key]]);
|
||||
}
|
||||
|
||||
/** 칸 원본을 목록 누가거리(0.01 m 글) → 값으로 읽음 — 지금 측점에 없는 고침은 뺌. */
|
||||
export function readKeyedIn<V>(raw: Raw, list: ReadonlyArray<Keyed> | undefined): Map<string, V> {
|
||||
const out = new Map<string, V>();
|
||||
for (const [key, value, hint] of rawEntries<V>(raw)) {
|
||||
const at = chainageIn(list, key, hint);
|
||||
if (at !== null) out.set(chainageKey(at), value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 칸마다 넘기는 값 — 넘길 것이 없으면 null. */
|
||||
function carriedValue(name: KeyedState, value: unknown): unknown {
|
||||
if (CARRY_WHOLE.includes(name)) return value;
|
||||
if (name !== "crossdesign" || !value || typeof value !== "object") return null;
|
||||
const source = value as Record<string, unknown>;
|
||||
const picked = Object.fromEntries(
|
||||
CARRY_CHOICE_KEYS.filter((key) => source[key] !== undefined).map((key) => [key, source[key]]),
|
||||
);
|
||||
return Object.keys(picked).length ? picked : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 측점 열쇠 칸 다섯을 옛 목록 → 새 목록으로(넘김 규칙 = 머리 주석) — 칸마다 새 원본(빈 칸도) · 노선 밖으로 나간
|
||||
* 고침의 열쇠(측점 단위).
|
||||
*/
|
||||
export function carryKeyedStates(
|
||||
read: (name: KeyedState) => Raw,
|
||||
from: ReadonlyArray<Keyed> | undefined,
|
||||
to: ReadonlyArray<Keyed> | undefined,
|
||||
): { states: Record<KeyedState, Raw>; outside: Set<string> } {
|
||||
const outside = new Set<string>();
|
||||
const curves = curveMovesIn(from, to);
|
||||
const states = {} as Record<KeyedState, Raw>;
|
||||
for (const name of KEYED_STATES) {
|
||||
const next: Raw = { [HINT]: {} };
|
||||
for (const [key, value] of rawEntries<unknown>(read(name))) {
|
||||
// 옛 열쇠(누가거리 · at:)는 옛 목록으로 측점 열쇠부터 · 측점 열쇠(id)는 그대로
|
||||
const fromAt = looseChainage(key) === null ? null : chainageIn(from, key);
|
||||
const was = fromAt === null ? key : keyIn(from, fromAt);
|
||||
const stable = curves.get(was) ?? was;
|
||||
const toAt = chainageIn(to, stable, looseChainage(stable) ?? undefined);
|
||||
const carried = carriedValue(name, value);
|
||||
if (toAt === null || stable.startsWith("at:")) {
|
||||
if (carried !== null) outside.add(stable);
|
||||
continue;
|
||||
}
|
||||
if (carried === null) continue;
|
||||
const toKey = keyIn(to, toAt);
|
||||
next[toKey] = carried;
|
||||
next[HINT]![toKey] = toAt;
|
||||
}
|
||||
states[name] = next;
|
||||
}
|
||||
return { states, outside };
|
||||
}
|
||||
|
||||
/** 노선 저장 때 시설 측점 풀이(`carryChainageStates`). */
|
||||
export interface CarryFacilities {
|
||||
/** 옛 저장 시설 측점 누가거리 → 새 노선 그 시설 측점 누가거리(없으면 null). */
|
||||
movedAt?: (chainageM: number) => number | null | undefined;
|
||||
/** 관 · 구조물 열쇠 → 새 노선 누가거리(없으면 null). */
|
||||
keyAt?: (key: string) => number | null | undefined;
|
||||
}
|
||||
|
||||
/** 누가거리 열쇠 머리(`b` · 0.01 m 글 · `:역할`) — 조정창 맵 열쇠 꼴(`renameChainageKeys` 와 같음). */
|
||||
const CHAINAGE_HEAD = /^(b?)(-?\d+(?:\.\d+)?)(:.*)?$/;
|
||||
|
||||
/**
|
||||
* 누가거리 열쇠 칸(`ROUTE_CARRY_STATES` 중 측점 열쇠 칸 빼고 · 관 종류 바꿈 자리 `kindreset`)을 옛 목록 → 새 목록
|
||||
* 측점으로 — 옛 누가거리 → 옛 측점 열쇠 → 새 누가거리. 구조물 조절은 관 · 구조물 열쇠만 · 못 옮긴 고침은 버림.
|
||||
* 돌려줌 = 원본이 있던 칸만 새 원본 · 버린 고침의 열쇠(측점 단위).
|
||||
* 시설 풀이(`facilities`) = 옛 저장 시설 측점 → 새 노선 그 시설 측점(좌표 짝 · 초안으로 안 따라간 고침) · 관 ·
|
||||
* 구조물 열쇠 → 새 누가거리(관 id 규칙이 갈림 — 세션 `pipe-s3` · 계산 줄 `pipe:p8017`).
|
||||
*/
|
||||
export function carryChainageStates(
|
||||
read: (name: string) => Record<string, unknown> | null,
|
||||
from: ReadonlyArray<Keyed> | undefined,
|
||||
to: ReadonlyArray<Keyed> | undefined,
|
||||
facilities: CarryFacilities = {},
|
||||
): { states: Record<string, Record<string, unknown>>; outside: Set<string> } {
|
||||
const facilityAt = facilities.keyAt ?? (() => null);
|
||||
const outside = new Set<string>();
|
||||
const curves = curveMovesIn(from, to);
|
||||
const moveAt = (state: string, text: string): string | null => {
|
||||
const moved = facilities.movedAt?.(Number(text));
|
||||
if (moved !== null && moved !== undefined) return chainageKey(moved);
|
||||
const was = keyIn(from, Number(text));
|
||||
const stable = curves.get(was) ?? was;
|
||||
const structural = stable.startsWith("pipe:") || stable.startsWith("structure:");
|
||||
// 새 목록에 그 열쇠가 없으면 그 시설의 새 자리 · 없으면 같은 자리 측점(초안 측점 = 저장 측점)
|
||||
const toAt =
|
||||
stable.startsWith("at:") || (STRUCTURE_STATES.has(state) && !structural)
|
||||
? null
|
||||
: chainageIn(to, stable, (structural ? facilityAt(stable) : null) ?? Number(text));
|
||||
if (toAt === null) outside.add(stable);
|
||||
return toAt === null ? null : chainageKey(toAt);
|
||||
};
|
||||
const rekey = (state: string, cells: Record<string, unknown>): Record<string, unknown> => {
|
||||
const next: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(cells)) {
|
||||
const head = CHAINAGE_HEAD.exec(key);
|
||||
const to = head && moveAt(state, head[2]);
|
||||
if (head && to !== null) next[`${head[1]}${to}${head[3] ?? ""}`] = value;
|
||||
}
|
||||
return next;
|
||||
};
|
||||
const states: Record<string, Record<string, unknown>> = {};
|
||||
for (const state of Object.keys(ROUTE_CARRY_STATES)) {
|
||||
if ((KEYED_STATES as readonly string[]).includes(state)) continue; // 측점 열쇠 칸 = carryKeyedStates
|
||||
const raw = read(state);
|
||||
if (!raw || typeof raw !== "object") continue;
|
||||
// 연동 = { detached: {누가거리: …}, grade: {…} } 두 겹
|
||||
states[state] =
|
||||
state === "revetlink"
|
||||
? Object.fromEntries(
|
||||
Object.entries(raw).map(([part, cells]) => [
|
||||
part,
|
||||
cells && typeof cells === "object"
|
||||
? rekey(state, cells as Record<string, unknown>)
|
||||
: cells,
|
||||
]),
|
||||
)
|
||||
: rekey(state, raw);
|
||||
}
|
||||
// 관 종류 바꿈 자리(318-4 · 프로젝트 칸) — 구조물 조절과 같은 규칙으로 옮김(445 M7)
|
||||
const resets = read("kindreset");
|
||||
if (resets) states.kindreset = rekey("revetx", resets);
|
||||
return { states, outside };
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/* =============================================================================
|
||||
* common_util/common_util_pipe_match.ts
|
||||
* 관 · 구조물 찾는 거리 한 값 — 화면(`design_selection` 이 내줌) · 서버 Node 묶음(노선 바뀐 [저장] 넘김)이 같이 씀.
|
||||
* ========================================================================== */
|
||||
|
||||
/** 관 · 구조물 찾는 거리(m) — 기준 측점은 정수 m(234-1) · 옛 소수 자료도 같은 자리로 봄. 이 값이 유일하다. */
|
||||
export const PIPE_MATCH_M = 0.51;
|
||||
@@ -51,6 +51,7 @@ function loadTs(file, stubs = {}) {
|
||||
|
||||
const surface = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Design_Surface.ts"));
|
||||
const selection = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"), {
|
||||
"../../common_util/common_util_pipe_match": { PIPE_MATCH_M: 0.51 }, // 찾는 거리 정의 자리(472-6)
|
||||
"../b_page_state": { readState: () => null, writeState: () => {} },
|
||||
});
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ function loadTs(file, stubs = {}) {
|
||||
// 세션(structure-pick) — 메모리 한 벌
|
||||
const store = new Map();
|
||||
const selection = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"), {
|
||||
"../../common_util/common_util_pipe_match": { PIPE_MATCH_M: 0.51 }, // 찾는 거리 정의 자리(472-6)
|
||||
"../b_page_state": {
|
||||
readState: (key, pid) => store.get(`${key}:${pid}`) ?? null,
|
||||
writeState: (key, value, pid) => store.set(`${key}:${pid}`, value),
|
||||
|
||||
@@ -27,6 +27,7 @@ global.requestAnimationFrame = (fn) => fn();
|
||||
|
||||
const store = new Map();
|
||||
const selection = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"), {
|
||||
"../../common_util/common_util_pipe_match": { PIPE_MATCH_M: 0.51 }, // 찾는 거리 정의 자리(472-6)
|
||||
"../b_page_state": {
|
||||
readState: (key, pid) => store.get(`${key}:${pid}`) ?? null,
|
||||
writeState: (key, value, pid) => store.set(`${key}:${pid}`, value),
|
||||
|
||||
@@ -59,6 +59,7 @@ function fire(type, clientX, clientY) {
|
||||
/* ── 진짜 모듈 ── */
|
||||
const store = new Map();
|
||||
const DSel = loadTs("A00_Common/design/design_selection.ts", {
|
||||
"../../common_util/common_util_pipe_match": { PIPE_MATCH_M: 0.51 }, // 찾는 거리 정의 자리(472-6)
|
||||
"../b_page_state": {
|
||||
readState: (key, pid) => store.get(`${key}:${pid}`) ?? null,
|
||||
writeState: (key, value, pid) => store.set(`${key}:${pid}`, value),
|
||||
|
||||
@@ -22,6 +22,7 @@ function loadTs(file, stubs = {}) {
|
||||
|
||||
const store = new Map();
|
||||
const DSel = loadTs("A00_Common/design/design_selection.ts", {
|
||||
"../../common_util/common_util_pipe_match": { PIPE_MATCH_M: 0.51 }, // 찾는 거리 정의 자리(472-6)
|
||||
"../b_page_state": {
|
||||
readState: (key, pid) => store.get(`${key}:${pid}`) ?? null,
|
||||
writeState: (key, value, pid) => store.set(`${key}:${pid}`, value),
|
||||
|
||||
@@ -62,6 +62,7 @@ globalThis.window = { addEventListener() {}, removeEventListener() {} };
|
||||
|
||||
const store = new Map();
|
||||
const DSel = loadTs("A00_Common/design/design_selection.ts", {
|
||||
"../../common_util/common_util_pipe_match": { PIPE_MATCH_M: 0.51 }, // 찾는 거리 정의 자리(472-6)
|
||||
"../b_page_state": {
|
||||
readState: (key, pid) => store.get(`${key}:${pid}`) ?? null,
|
||||
writeState: (key, value, pid) => store.set(`${key}:${pid}`, value),
|
||||
|
||||
@@ -39,6 +39,7 @@ const press = (key, { inInput = false, prevented = false } = {}) => {
|
||||
|
||||
const store = new Map();
|
||||
const DSel = loadTs("A00_Common/design/design_selection.ts", {
|
||||
"../../common_util/common_util_pipe_match": { PIPE_MATCH_M: 0.51 }, // 찾는 거리 정의 자리(472-6)
|
||||
"../b_page_state": {
|
||||
readState: (key, pid) => store.get(`${key}:${pid}`) ?? null,
|
||||
writeState: (key, value, pid) => store.set(`${key}:${pid}`, value),
|
||||
|
||||
@@ -26,6 +26,7 @@ globalThis.document = { addEventListener() {}, removeEventListener() {} };
|
||||
|
||||
const store = new Map();
|
||||
const DSel = loadTs("A00_Common/design/design_selection.ts", {
|
||||
"../../common_util/common_util_pipe_match": { PIPE_MATCH_M: 0.51 }, // 찾는 거리 정의 자리(472-6)
|
||||
"../b_page_state": {
|
||||
readState: (key, pid) => store.get(`${key}:${pid}`) ?? null,
|
||||
writeState: (key, value, pid) => store.set(`${key}:${pid}`, value),
|
||||
|
||||
@@ -24,6 +24,7 @@ function loadTs(file, stubs = {}) {
|
||||
}
|
||||
|
||||
const selection = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"), {
|
||||
"../../common_util/common_util_pipe_match": { PIPE_MATCH_M: 0.51 }, // 찾는 거리 정의 자리(472-6)
|
||||
"../b_page_state": { readState: () => null, writeState() {} },
|
||||
});
|
||||
const PREFIX = "irregular:";
|
||||
|
||||
@@ -488,8 +488,10 @@ const out = {};
|
||||
const load = makeLoader(
|
||||
[
|
||||
B06("B06_Section_Api_Culvert_Options"),
|
||||
B06("logic/B06_Section_Design_Keys"), // 칸 목록 한 벌(472-6)
|
||||
path.join(ROOT, "A00_Common", "b_page_state.ts"),
|
||||
path.join(ROOT, "A00_Common", "design", "design_selection.ts"),
|
||||
path.join(ROOT, "common_util", "common_util_pipe_match.ts"), // 472-6
|
||||
],
|
||||
{
|
||||
"./B06_Section_Section_Drafts": {
|
||||
@@ -596,14 +598,22 @@ const loadDrafts = makeLoader(
|
||||
const loadStore = makeLoader(
|
||||
[
|
||||
B06("B06_Section_Section_Store"),
|
||||
B06("B06_Section_Route_Carry_Patch"), // patch 바탕 한 벌(472-6)
|
||||
B06("B06_Section_Api_Culvert_Options"),
|
||||
path.join(ROOT, "A00_Common", "b_page_state.ts"),
|
||||
path.join(ROOT, "A00_Common", "design", "design_selection.ts"),
|
||||
path.join(ROOT, "common_util", "common_util_pipe_match.ts"), // 472-6
|
||||
],
|
||||
{
|
||||
"./B06_Section_Cross_Refresh": {
|
||||
USER_TOUCHED_KEYS: ["display_half_width_m", "ford_adjust", "revet_adjust", "berm"],
|
||||
},
|
||||
"./logic/B06_Section_Design_Keys": {
|
||||
USER_TOUCHED_KEYS: ["display_half_width_m", "ford_adjust", "revet_adjust", "berm"],
|
||||
STRUCTURE_ADJUST_KEYS: makeLoader([B06("logic/B06_Section_Design_Keys")])(
|
||||
B06("logic/B06_Section_Design_Keys"),
|
||||
).STRUCTURE_ADJUST_KEYS,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -89,8 +89,10 @@ const part1 = (async () => {
|
||||
B05("B05_Profile_UI_Structures_Panel_Events"),
|
||||
B05("B05_Profile_Api_Structures"),
|
||||
B06("B06_Section_Api_Culvert_Options"),
|
||||
B06("logic/B06_Section_Design_Keys"), // 칸 목록 · 찾는 거리 한 벌(472-6)
|
||||
path.join(ROOT, "A00_Common", "b_page_state.ts"),
|
||||
path.join(ROOT, "A00_Common", "design", "design_selection.ts"),
|
||||
path.join(ROOT, "common_util", "common_util_pipe_match.ts"),
|
||||
path.join(ROOT, "common_util", "common_util_summary_item.ts"),
|
||||
],
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ function loadTs(file, real) {
|
||||
const box = { exports: {} };
|
||||
const localRequire = (request) =>
|
||||
real.some((name) => request.endsWith(name))
|
||||
? loadTs(path.join(path.dirname(file), `${request}.ts`), [])
|
||||
? loadTs(path.join(path.dirname(file), `${request}.ts`), ["common_util_pipe_match"]) // 472-6
|
||||
: stub;
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
return box.exports;
|
||||
|
||||
@@ -25,6 +25,7 @@ globalThis.document = { querySelector: () => null };
|
||||
|
||||
const store = new Map();
|
||||
const DSel = loadTs("A00_Common/design/design_selection.ts", {
|
||||
"../../common_util/common_util_pipe_match": { PIPE_MATCH_M: 0.51 }, // 찾는 거리 정의 자리(472-6)
|
||||
"../b_page_state": {
|
||||
readState: (key, pid) => store.get(`${key}:${pid}`) ?? null,
|
||||
writeState: (key, value, pid) => store.set(`${key}:${pid}`, value),
|
||||
|
||||
@@ -73,6 +73,10 @@ const load = makeLoader(
|
||||
B06("B06_Section_UI_Cross_Culvert_Types"),
|
||||
B06("logic/B06_Section_Station_Swap"),
|
||||
B06("logic/B06_Section_UI_Page_Patches"),
|
||||
B06("logic/B06_Section_Design_Keys"), // 칸 목록 · 얹기 · patch 꼴 한 벌(472-6)
|
||||
B06("logic/B06_Section_Route_Carry"),
|
||||
B06("B06_Section_Route_Carry_Patch"),
|
||||
path.join(ROOT, "common_util", "common_util_pipe_match.ts"),
|
||||
path.join(ROOT, "A00_Common", "b_page_state.ts"),
|
||||
path.join(ROOT, "A00_Common", "design", "design_selection.ts"),
|
||||
],
|
||||
|
||||
@@ -25,6 +25,9 @@ function loadTs(file, stubs = {}) {
|
||||
// 세션(structure-pick) — 메모리 한 벌
|
||||
const store = new Map();
|
||||
const selection = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"), {
|
||||
"../../common_util/common_util_pipe_match": loadTs(
|
||||
path.join(ROOT, "common_util", "common_util_pipe_match.ts"),
|
||||
), // 찾는 거리 정의(472-6)
|
||||
"../b_page_state": {
|
||||
readState: (key, pid) => store.get(`${key}:${pid}`) ?? null,
|
||||
writeState: (key, value, pid) => store.set(`${key}:${pid}`, value),
|
||||
|
||||
@@ -246,7 +246,7 @@ def test_손질_없으면_도면_표_그대로():
|
||||
|
||||
def test_살림_목록_짝에_detail_table():
|
||||
assert "detail_table" in USER_TOUCHED_KEYS
|
||||
text = (ROOT / "B06_Section" / "B06_Section_Cross_Refresh.ts").read_text(encoding="utf-8")
|
||||
text = (ROOT / "B06_Section" / "logic" / "B06_Section_Design_Keys.ts").read_text(encoding="utf-8") # 목록 한 벌(472-6)
|
||||
block = re.search(r"export const USER_TOUCHED_KEYS = \[(.*?)\] as const;", text, re.S)
|
||||
assert block and '"detail_table"' in block.group(1)
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ def test_patch_칸_왕복() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
CrossSectionPatch(chainage_m=40, berm_depth_left_m=-0.1) # 도로 위 금지
|
||||
assert {"berm_depth_left_m", "berm_depth_right_m"} <= set(USER_TOUCHED_KEYS)
|
||||
refresh = (ROOT / "B06_Section" / "B06_Section_Cross_Refresh.ts").read_text(encoding="utf-8")
|
||||
refresh = (ROOT / "B06_Section" / "logic" / "B06_Section_Design_Keys.ts").read_text(encoding="utf-8") # 목록 한 벌(472-6)
|
||||
assert '"berm_depth_left_m",' in refresh and '"berm_depth_right_m",' in refresh
|
||||
persist = (ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts").read_text(encoding="utf-8")
|
||||
assert "patch.berm_depth_left_m = sides.left" in persist
|
||||
|
||||
@@ -77,7 +77,7 @@ def test_브라우저가_CAD_칸을_저장_patch_에_싣는다():
|
||||
state = read("A00_Common/b_page_state.ts")
|
||||
assert '"structure-note-cads": { bucket: "draft", scope: "route" }' in state
|
||||
# 캐시 설계 → B05 [저장] 길(`crossPatchesFromCache`)도 같은 목록
|
||||
assert '"structure_note_cad",' in read("B06_Section/B06_Section_Cross_Refresh.ts")
|
||||
assert '"structure_note_cad",' in read("B06_Section/logic/B06_Section_Design_Keys.ts") # 472-6
|
||||
# 모달 = 빈 CAD(캔버스만) · 글 상자 없음
|
||||
detail = read("B06_Section/B06_Section_UI_Cross_Detail.ts")
|
||||
assert "textarea" not in detail and "mountNoteCad(" in detail and "noteCad?.destroy()" in detail
|
||||
|
||||
@@ -252,7 +252,7 @@ def test_wiring_sources() -> None:
|
||||
|
||||
def test_structure_keys_mirror() -> None:
|
||||
"""서버 · 브라우저 열쇠 목록이 같음 · 모두 승계 목록 안."""
|
||||
ts = _src("B06_Section", "B06_Section_Api_Culvert_Options.ts")
|
||||
ts = _src("B06_Section", "logic", "B06_Section_Design_Keys.ts") # 목록 한 벌(472-6)
|
||||
block = ts[ts.index("export const STRUCTURE_ADJUST_KEYS = [") :]
|
||||
block = block[: block.index("] as const")]
|
||||
assert re.findall(r'"([a-z_]+)"', block) == list(STRUCTURE_ADJUST_KEYS)
|
||||
|
||||
@@ -41,7 +41,7 @@ from B06_Section.B06_Section_Router_Design import ( # noqa: E402
|
||||
)
|
||||
from config.config_system import STANDARD_CROSS_SECTION # noqa: E402
|
||||
|
||||
TS_KEYS = ROOT / "B06_Section" / "B06_Section_Station_Keys.ts"
|
||||
TS_KEYS = ROOT / "B06_Section" / "logic" / "B06_Section_Route_Carry.ts" # 넘김 규칙 한 벌(472-6)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
|
||||
@@ -246,9 +246,14 @@ def test_H5_노선_저장_뒤_본문_M7_kindreset_옮김():
|
||||
assert (
|
||||
"cachedCrossPatches(projectId, routeId, edits.crossPatches)" in persist
|
||||
) # 472-6 횡단 조각
|
||||
# 넘김 규칙 = logic 한 벌(472-6 6-B) · 세션 겉이 kindreset 을 프로젝트 칸에 다시 씀
|
||||
rules = (ROOT / "B06_Section" / "logic" / "B06_Section_Route_Carry.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
carry = rules[rules.index("export function carryChainageStates") :]
|
||||
assert 'states.kindreset = rekey("revetx", resets)' in carry
|
||||
keys = (ROOT / "B06_Section" / "B06_Section_Station_Keys.ts").read_text(encoding="utf-8")
|
||||
carry = keys[keys.index("export function carryChainageStates") :]
|
||||
assert 'writeState("kindreset", rekey("revetx", resets), projectId)' in carry
|
||||
assert 'if (name === "kindreset") writeState("kindreset", next, projectId);' in keys
|
||||
idle = keys[keys.index("function moveIdleStates") :]
|
||||
assert 'writeState("kindreset", move(resets), projectId)' in idle
|
||||
|
||||
|
||||
@@ -54,7 +54,12 @@ def test_거리_정의는_한_곳뿐이고_옛_0_05는_없다():
|
||||
text = _read(os.path.relpath(os.path.join(current, name), ROOT))
|
||||
if re.search(r"\b(const|let)\s+PIPE_MATCH_M\b", text):
|
||||
definitions.append(name)
|
||||
assert definitions == ["design_selection.ts"]
|
||||
assert (
|
||||
definitions == []
|
||||
) # 정의 = common_util 한 곳(서버 Node 묶음도 끔 · 472-6) · 페이지는 내줌만
|
||||
station = _read("common_util/common_util_pipe_match.ts")
|
||||
assert re.search(r"export const PIPE_MATCH_M = 0\.51;", station)
|
||||
assert "export { PIPE_MATCH_M }" in _read("A00_Common/design/design_selection.ts")
|
||||
for name, folder in (
|
||||
("B05_Profile_UI_Structures_Panel.ts", "B05_Profile"),
|
||||
("design_view_profile.ts", "A00_Common/design"),
|
||||
|
||||
@@ -90,7 +90,9 @@ def test_0_은_표준값으로_되돌림이다():
|
||||
def test_다시_계산해도_사용자_값이_남는다():
|
||||
"""표준 횡단면 설정을 바꿔도 개별로 고친 측점은 그대로 (사용자 원문 끝줄)."""
|
||||
assert "cut_slope_ratio_user" in USER_TOUCHED_KEYS
|
||||
assert '"cut_slope_ratio_user"' in _REFRESH.read_text(encoding="utf-8")
|
||||
assert '"cut_slope_ratio_user"' in (_B06 / "logic" / "B06_Section_Design_Keys.ts").read_text(
|
||||
encoding="utf-8"
|
||||
) # 목록 한 벌(472-6)
|
||||
|
||||
|
||||
def test_계산_전에_넘어간다():
|
||||
|
||||
@@ -11,7 +11,8 @@ from pathlib import Path
|
||||
from B06_Section.B06_Section_Router_Design import USER_TOUCHED_KEYS
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TS_PATH = ROOT / "B06_Section" / "B06_Section_Cross_Refresh.ts"
|
||||
TS_PATH = ROOT / "B06_Section" / "logic" / "B06_Section_Design_Keys.ts" # 목록 한 벌(472-6)
|
||||
REFRESH = ROOT / "B06_Section" / "B06_Section_Cross_Refresh.ts"
|
||||
|
||||
# 브라우저 목록에만 있고 서버 목록에는 없는 둘 — 부르는 쪽이 따로 붙인다.
|
||||
_CALLER_ADDED = {"status", "pavement_suggested"}
|
||||
@@ -36,7 +37,7 @@ def test_서버와_브라우저_목록이_같다():
|
||||
|
||||
def test_브라우저_목록도_한_벌이다():
|
||||
"""보존 목록·저장 payload 가 같은 한 벌을 쓰는지 — 손나열이 되살아나면 깨진다."""
|
||||
text = TS_PATH.read_text(encoding="utf-8")
|
||||
text = REFRESH.read_text(encoding="utf-8")
|
||||
assert "...USER_TOUCHED_KEYS" in text, "PRESERVED_KEYS 가 목록을 다시 적고 있음"
|
||||
store = (ROOT / "B06_Section" / "B06_Section_Section_Store.ts").read_text(encoding="utf-8")
|
||||
assert "USER_TOUCHED_KEYS" in store, "저장 payload 가 목록을 안 씀"
|
||||
|
||||
@@ -18,6 +18,7 @@ def _read(relative: str) -> str:
|
||||
REPO = _read("B06_Section/B06_Section_Repository.py")
|
||||
B06_ROUTER = _read("B06_Section/B06_Section_Router.py")
|
||||
REFRESH = _read("B06_Section/B06_Section_Cross_Refresh.ts")
|
||||
KEYS = _read("B06_Section/logic/B06_Section_Design_Keys.ts")
|
||||
B05_PREVIEW = _read("B05_Profile/B05_Profile_UI_Profile_Preview.ts")
|
||||
# 설계 재계산 자리는 700줄 제한으로 `_UI_Page_Design_Sync` 로 떨어져 나갔다(2026-09-13).
|
||||
B06_PAGE = _read("A00_Common/design/design_view_cross.ts") + _read(
|
||||
@@ -66,7 +67,7 @@ def test_cross_refresh_preserves_user_fields():
|
||||
"revet_link_detached",
|
||||
"revet_follow_grade",
|
||||
):
|
||||
assert f'"{field}"' in REFRESH # preserveUserFields 의 보존 필드 목록
|
||||
assert f'"{field}"' in KEYS # preserveUserFields 의 보존 필드 목록(한 벌 · 472-6)
|
||||
|
||||
|
||||
def test_cross_refresh_lays_structure_areas_once():
|
||||
|
||||
Reference in New Issue
Block a user