fix(457 노선 바뀜 초안 카드): 그림 · 이름 · 횡단 맥락 = 지금 현황을 새 측점에 다시 얹음 · 등록 열쇠 짝 · 가게 쓰기 = 지금 가게 값과 견줌
- 초안 상세(카드)도 캐시와 같은 attachShapes 를 follow 뒤 현황으로 부름 · atChainage 지움 · 이름 = 측점 계획 제목 - cross 입력 넘김 = 사용자 칸 · 카드 번호만 · 포장 · 기울임 · 하강 = 새 자리 그림에서(crossWithShapes · 점 창도) - 초안 자리 시설 측점 짝 = 등록 열쇠(B06 측점 열쇠 목록 · 처음 본 열쇠) · 차례 짝 지움 - writeFacilities = 지금 가게 값과 견줌(되돌리기 뒤 같은 조작도 씀) - 시험 test_457_draft_card(재현 E · F · G · H) · 365 · 433 · 440 도우미 새 규칙으로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/design_pipeline_cross.ts
|
||||
* 계산 묶음 cross 입력 모음(PLAN 346-2 붙임) — 저장 노선 상세의 측점마다 횡단 입력 · 얹힌 시설 제원을
|
||||
* 계산 묶음 cross 입력 모음(PLAN 346-2 붙임) — 저장 노선 상세의 측점마다 사용자 횡단 입력 · 카드 번호를
|
||||
* **측점 열쇠**로 모음. 노선이 바뀌어도 같은 열쇠 측점(격자 번호 · 관 id · 구조물 id)이 그 입력을 받음.
|
||||
* 열쇠 = 저장 노선을 stations 단계와 같은 함수로 짚은 측점(`buildStations`)과 누가거리로 짝지음 —
|
||||
* 묶음 측점과 같은 열쇠 규칙(관 = 저장 관 id).
|
||||
* 묶음 측점과 같은 열쇠 규칙(관 = 저장 관 id). 초안 자리로 뜬 시설 측점 = 등록 열쇠(주인 id)로 짝(457).
|
||||
*
|
||||
* 넘기는 칸 = 서버 노선 저장(`CARRY_INPUT_KEYS`)과 같은 일곱(지반 유형 · 측구 종류 · 측구 선택 · 2단 경사 ·
|
||||
* 암 경계 · 암 경사 · 소단) + 사면 쉬프트(노견 확장) · 소단 깊이(417 · 서버 `ROUTE_CARRY_KEYS`) + 얹힌 시설
|
||||
* 맥락(포장 · 물넘이 기울임 · 세월교 노면 하강). 단면 유형 · 측구 방향은 새 지형이 정함
|
||||
* (`logic/B05_Profile_Pipeline_Detail.ts` `crossWithUphill`). 값 = 상세에 얹힌 설계 위에 세션 고침(측점 열쇠 칸 ·
|
||||
* 암 경계 · 암 경사 · 소단) + 사면 쉬프트(노견 확장) · 소단 깊이(417 · 서버 `ROUTE_CARRY_KEYS`) — 사용자 칸만.
|
||||
* 그림 자료 · 카드 이름 · 얹힌 시설 맥락(포장 · 기울임 · 하강) · 단면 유형 · 측구 방향은 새 자리에서 다시 셈
|
||||
* (`logic/B05_Profile_Pipeline_Detail.ts` `crossWithShapes` · `draftSectionDetail` · `crossWithUphill` · 457). 값 = 상세에 얹힌 설계 위에 세션 고침(측점 열쇠 칸 ·
|
||||
* 지반 유형 · 측구 · 2단 경사 · 암 경계 · 암 경사 · 사면 쉬프트 · 소단 깊이) — 노선 초안 동안 B06 고침은 저장 상세에
|
||||
* 안 얹혀도 입력에 들어감(365-2 · 365-5 · 417). 부를 때마다 모음.
|
||||
* ========================================================================== */
|
||||
@@ -25,21 +25,10 @@ import type { PipelineStation } from "./logic/design_pipeline_types";
|
||||
|
||||
export interface PipelineCrossSource {
|
||||
input: CrossInput;
|
||||
/** 측점 열쇠 → 얹힌 시설 제원. */
|
||||
/** 측점 열쇠 → 카드 번호(457 · 그림 자료는 새 자리에서 다시 얹음). */
|
||||
specs: Map<string, SectionSpecs>;
|
||||
}
|
||||
|
||||
const SPEC_KEYS = [
|
||||
"structure",
|
||||
"culvert",
|
||||
"ford",
|
||||
"box",
|
||||
"ford_pavement",
|
||||
"revetment",
|
||||
"pavement",
|
||||
"underdrain",
|
||||
] as const;
|
||||
|
||||
/** 같은 측점으로 보는 누가거리 차(m) — 측점 열쇠 글 자릿수(0.01 m). */
|
||||
const SAME_M = 0.01;
|
||||
|
||||
@@ -58,13 +47,12 @@ export const forceCross = (): void => void (forced += 1);
|
||||
const finite = (value: unknown): number | null =>
|
||||
typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
|
||||
/** 측점 하나의 넘기는 입력(cross 단계 `CrossStationInput` 꼴). */
|
||||
/** 측점 하나의 넘기는 사용자 입력(cross 단계 `CrossStationInput` 꼴 · 시설 맥락은 새 자리 그림에서 · 457). */
|
||||
function carried(section: CrossSection, saved?: CrossSection["design"]): Record<string, unknown> {
|
||||
const design = (saved ?? section.design ?? {}) as unknown as Record<string, unknown>;
|
||||
const berm = design.berm as
|
||||
{ width_m: number; interval_m: number; slope_deg: number; depth_m?: number } | undefined;
|
||||
const cutSlope = finite(design.cut_slope_ratio_user);
|
||||
const drop = Number(section.ford?.overflow_depth_m);
|
||||
return {
|
||||
...(typeof design.ground_type === "string" ? { groundType: design.ground_type } : {}),
|
||||
...(typeof design.ditch_type === "string" ? { ditchType: design.ditch_type } : {}),
|
||||
@@ -82,10 +70,6 @@ function carried(section: CrossSection, saved?: CrossSection["design"]): Record<
|
||||
depthM: berm.depth_m,
|
||||
}
|
||||
: null,
|
||||
paved: Boolean(section.pavement?.length) || Boolean(section.ford_pavement),
|
||||
tilted: Boolean(section.ford_pavement || section.underdrain),
|
||||
tiltPct: section.ford_pavement?.slope_pct ?? null, // 맹암거 = 표준 경사(371-3)
|
||||
surfaceDropM: drop > 0 ? drop : 0,
|
||||
// 저장분 사면 쉬프트 · 소단 깊이(417 L4) — 세션 고침이 없어도 저장 뒤 그대로
|
||||
...sides(design, "shoulder_extend", "shoulderExtend"),
|
||||
...sides(design, "berm_depth", "bermDepth"),
|
||||
@@ -147,29 +131,30 @@ function sessionCarried(projectId: string, routeId: number): CrossInput["byKey"]
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 저장 상세 + 저장 노선 측점(열쇠) + 세션 고침(`routeId` 칸) → cross 입력 · 시설 제원 — 표준단면 ·
|
||||
* 상세가 없으면 null. */
|
||||
/** 초안 자리로 뜬 측점 → 처음 본 등록 열쇠 — 노선 초안 동안 등록 목록이 새 노선 것으로 바뀌어도 그대로(457). */
|
||||
const strayKeys = new WeakMap<CrossSection, string>();
|
||||
|
||||
/** 저장 상세 + 저장 노선 측점(열쇠) + 세션 고침(`routeId` 칸) → cross 입력 · 카드 번호 — 표준단면 ·
|
||||
* 상세가 없으면 null. `keysOf` = 상세 측점마다 등록 열쇠(B06 측점 열쇠 목록 · 초안 자리 측점 짝 · 457). */
|
||||
export function pipelineCrossSource(
|
||||
projectId: string,
|
||||
detail: SectionDetailResponse | null,
|
||||
savedStations: ReadonlyArray<PipelineStation>,
|
||||
routeId: number | null = null,
|
||||
keysOf?: (routeId: number, detail: SectionDetailResponse) => string[],
|
||||
): PipelineCrossSource | null {
|
||||
const standard = detail && effectiveStandardCross(projectId);
|
||||
if (!detail || !standard) return null;
|
||||
const byKey: CrossInput["byKey"] = {};
|
||||
const specs = new Map<string, SectionSpecs>();
|
||||
// 설계 칸 = 세션에 남긴 저장 상세(초안 · 다시 셈이 안 고친 정본 — 들어온 화면 · 고친 차례 무관 · 365-5) ·
|
||||
// 시설 제원 = 화면 상세(초안 형상을 얹은 것)
|
||||
// 설계 칸 = 세션에 남긴 저장 상세(초안 · 다시 셈이 안 고친 정본 — 들어온 화면 · 고친 차례 무관 · 365-5)
|
||||
const stored = routeId === null ? null : savedDesigns(projectId, routeId); // 세션 다시 안 풂(387-7)
|
||||
const storedAt = (chainageM: number) =>
|
||||
stored?.find((row) => Math.abs(row.chainage_m - chainageM) < SAME_M)?.design;
|
||||
const put = (key: string, section: CrossSection, at = section.chainage_m): void => {
|
||||
byKey[key] = carried(section, storedAt(at));
|
||||
// 카드 번호 = 저장 카드 번호(같은 측점 열쇠 · 옮긴 관도 · 찾기 · 고름이 같은 id · 440)
|
||||
const own: Record<string, unknown> = { station_id: section.station_id };
|
||||
for (const name of SPEC_KEYS) if (section[name] !== undefined) own[name] = section[name];
|
||||
specs.set(key, own as SectionSpecs);
|
||||
specs.set(key, { station_id: section.station_id });
|
||||
};
|
||||
const strays: CrossSection[] = [];
|
||||
for (const section of detail.cross_sections) {
|
||||
@@ -182,20 +167,25 @@ export function pipelineCrossSource(
|
||||
if (best && !byKey[best.key]) put(best.key, section);
|
||||
else if (!best && section.kind === "irregular") strays.push(section);
|
||||
}
|
||||
// 초안 자리로 미리 뜬 관 · 구조물 측점(초안 둔 채 열기 — 상세가 초안 누가거리) = 남은 저장 시설 측점과 차례로 짝 ·
|
||||
// 수가 다르면 안 짝지음(349-0d 와 같은 규칙 · 들어온 화면 무관 같은 제원 · 365-5)
|
||||
const left = savedStations.filter(
|
||||
(station) => (station.kind === "pipe" || station.kind === "structure") && !byKey[station.key],
|
||||
);
|
||||
left.sort((a, b) => a.chainageM - b.chainageM);
|
||||
if (strays.length === left.length)
|
||||
strays.forEach((section, i) => put(left[i].key, section, left[i].chainageM));
|
||||
// 초안 자리로 미리 뜬 관 · 구조물 측점(초안 둔 채 열기 — 상세가 초안 누가거리) = 등록 열쇠(주인 id)가 같은 저장
|
||||
// 시설 측점과 짝 — 차례로 짝짓지 않음(엇갈려 옮긴 관 · 457) · 모르는 열쇠는 버림
|
||||
const keys = strays.length && routeId !== null ? (keysOf?.(routeId, detail) ?? null) : null;
|
||||
for (const section of strays) {
|
||||
const index = detail.cross_sections.indexOf(section);
|
||||
const key = strayKeys.get(section) ?? keys?.[index];
|
||||
if (!key) continue;
|
||||
strayKeys.set(section, key);
|
||||
const owner = savedStations.find(
|
||||
(station) => station.key === key && (station.kind === "pipe" || station.kind === "structure"),
|
||||
);
|
||||
if (owner && !byKey[key]) put(key, section, owner.chainageM);
|
||||
}
|
||||
if (routeId !== null)
|
||||
for (const [key, entry] of Object.entries(sessionCarried(projectId, routeId)))
|
||||
byKey[key] = { ...byKey[key], ...entry };
|
||||
return {
|
||||
// 암 경계 기본값 = 서버 config(화면이 문맥에서 기억) — 입력 없는 측점(초안 새 측점)도 서버처럼(349-0p)
|
||||
// 제원 지문 — 보기가 나중에 얹은 시설 제원도 입력 차이로(고리가 다시 셈 · 들어온 화면 무관 같은 면적 · 365-5)
|
||||
// 카드 번호 지문(365-5 · 시설 그림이 바뀌면 관 · 구조물 입력이 cross 를 다시 돌림 · 457)
|
||||
input: {
|
||||
standard,
|
||||
byKey,
|
||||
|
||||
@@ -86,9 +86,9 @@ import {
|
||||
routeRange,
|
||||
} from "../../B05_Profile/logic/B05_Profile_Pipeline_Grade";
|
||||
import {
|
||||
crossWithBerm,
|
||||
crossWithUphill,
|
||||
crossWithContext,
|
||||
draftSectionDetail,
|
||||
shapeStatus,
|
||||
type CrossInput,
|
||||
} from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
|
||||
import {
|
||||
@@ -227,9 +227,7 @@ function createPagePipeline(
|
||||
let dropPending = false;
|
||||
/** 노선 초안 전 가게 관 · 구조물 — 초안 동안 follow 가 새 측점을 가게에 씀 · 초안을 버리면 이것으로. */
|
||||
let beforeDraft: Facilities | null = null;
|
||||
/** 묶음이 가게에 마지막으로 쓴 관 · 구조물 · 쓰는 동안(메아리 — 다시 입력으로 안 받음). */
|
||||
let written: Facilities | null = null;
|
||||
let echo = false;
|
||||
let echo = false; // 묶음이 가게에 쓰는 동안(메아리 — 다시 입력으로 안 받음)
|
||||
/** 되돌리기 · 다시하기로 바뀐 노선(356-4) — follow 가 계획고 편집을 옮기지 않고 그 걸음 사진(세션 노선 0 칸)
|
||||
* 그대로. 바뀐 구간 첫 계획선 맞춤 · 계획고 옮김은 새 조작 때만. */
|
||||
let replay = false;
|
||||
@@ -268,10 +266,14 @@ function createPagePipeline(
|
||||
canon?.detail ?? null,
|
||||
savedStations,
|
||||
typeof routeId === "number" ? routeId : null,
|
||||
options.stationKeys, // 초안 자리 측점 짝(457)
|
||||
);
|
||||
if (source) crossSource = source;
|
||||
return source;
|
||||
};
|
||||
/** 지금 현황(follow 뒤 입력 = 가게 값) — 새 측점 그림 · 이름 · 시설 맥락을 다시 얹음(457). */
|
||||
const statusOf = (pipes: unknown, structures: unknown) =>
|
||||
shapeStatus(pipes ?? savedPipes, structures ?? savedStructures, types);
|
||||
const groundDeps = groundStageDeps(projectId);
|
||||
const crossStage = createCrossStage({ grid: groundDeps.grid }); // 미교차 넓힘 = 같은 지형 격자(349-0j)
|
||||
const followStage = createFollowStage({
|
||||
@@ -352,9 +354,10 @@ function createPagePipeline(
|
||||
prev: ctx.prev && savedOut.has(ctx.prev) ? null : ctx.prev,
|
||||
inputs: {
|
||||
...ctx.inputs,
|
||||
cross: crossWithBerm(
|
||||
crossWithUphill(ctx.inputs.cross as CrossInput | null, stations),
|
||||
cross: crossWithContext(
|
||||
ctx.inputs.cross as CrossInput | null,
|
||||
stations,
|
||||
statusOf(ctx.inputs.pipes, ctx.inputs.structures),
|
||||
options.bermAt?.(ctx.inputs.structures) ?? null,
|
||||
),
|
||||
},
|
||||
@@ -485,6 +488,7 @@ function createPagePipeline(
|
||||
pipe.output("cross"),
|
||||
crossSource?.specs ?? new Map(),
|
||||
options.stationIntervalM ?? profile.policy.station_interval_m,
|
||||
statusOf(pipe.input("pipes"), pipe.input("structures")),
|
||||
)
|
||||
: null;
|
||||
if (shown) drafted.add(shown);
|
||||
@@ -509,17 +513,15 @@ function createPagePipeline(
|
||||
function facilitiesNow(): Facilities {
|
||||
return { pipes: store.get("pipes"), structures: store.get("structures") };
|
||||
}
|
||||
/** 가게에 씀 — 앞에 쓴 것과 같으면 안 씀(되돌리기가 세션을 되쓴 뒤에도 듣는 쪽은 다시 부름). */
|
||||
/** 가게에 씀 — 지금 가게 값과 같으면 안 씀(되돌리기가 가게를 되쓴 뒤 같은 조작도 씀 · 457). */
|
||||
function writeFacilities(next: Facilities): void {
|
||||
echo = true;
|
||||
try {
|
||||
if (!written || !same(written.pipes, next.pipes)) store.set("pipes", next.pipes);
|
||||
if (!written || !same(written.structures, next.structures))
|
||||
store.set("structures", next.structures);
|
||||
if (!same(store.get("pipes"), next.pipes)) store.set("pipes", next.pipes);
|
||||
if (!same(store.get("structures"), next.structures)) store.set("structures", next.structures);
|
||||
} finally {
|
||||
echo = false;
|
||||
}
|
||||
written = next;
|
||||
}
|
||||
|
||||
const inputsNow = (): PipelineInputs => {
|
||||
|
||||
@@ -54,6 +54,8 @@ export interface PagePipelineOptions {
|
||||
/** 초안 전 가게 관 · 구조물(초안 둔 채 새로고침도 · 440-4) — 묶음이 초안을 버릴 때 되씀. */
|
||||
seed?: (facilities: Pick<DesignStoreValues, "pipes" | "structures">) => void,
|
||||
) => (stations: PipelineStation[]) => Promise<string>;
|
||||
/** 저장 상세 측점마다 등록 열쇠(B06 측점 열쇠 목록) — 초안 자리로 뜬 시설 측점의 입력 짝(457). */
|
||||
stationKeys?: (routeId: number, detail: SectionDetailResponse) => string[];
|
||||
/** 고리가 `RECONCILE_ROUNDS` 바퀴 안에 안 맞음 — 「계산 불안정」 한 줄(365-2). */
|
||||
onUnstable?: () => void;
|
||||
/** 소단 구간(지금 구조물 목록 → 누가거리마다 제원) — 초안 cross 가 따라간 구조물로 소단을 셈(365-5). */
|
||||
|
||||
@@ -19,9 +19,12 @@ import { probeStation } from "./logic/design_pipeline_probe_station";
|
||||
import { groundStageDeps } from "../../B05_Profile/B05_Profile_Api_TerrainGrid";
|
||||
import { createGroundStage } from "../../B05_Profile/logic/B05_Profile_Pipeline_RouteGround";
|
||||
import {
|
||||
crossWithShapes,
|
||||
crossWithUphill,
|
||||
shapeStatus,
|
||||
type CrossInput,
|
||||
} from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
|
||||
import { fetchStructureTypes } from "../../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { createCrossStage } from "../../B06_Section/logic/B06_Section_Cross_Stage";
|
||||
import { pipelineCrossAt } from "../../B05_Profile/B05_Profile_UI_RouteEdit_Cross";
|
||||
import type { CrossPreviewResponse } from "../../B05_Profile/B05_Profile_Api_Replan";
|
||||
@@ -72,6 +75,13 @@ export async function pipelineCrossAtChainage(
|
||||
station = filled.stations[0] ?? null;
|
||||
}
|
||||
if (!station) return null;
|
||||
// 시설 맥락 = 지금 현황을 그 측점에 얹은 그림에서(묶음 cross 와 같음 · 457)
|
||||
const status = shapeStatus(
|
||||
inputs.pipes,
|
||||
inputs.structures,
|
||||
await fetchStructureTypes(true).catch(() => []),
|
||||
);
|
||||
const shaped = crossWithShapes(inputs.cross as CrossInput | null, [station], status);
|
||||
const out = {
|
||||
profile,
|
||||
ground: { stations: [station], longitudinal: { chainageM: [], z: [] } },
|
||||
@@ -80,7 +90,7 @@ export async function pipelineCrossAtChainage(
|
||||
generation: 0,
|
||||
range: null,
|
||||
signal: new AbortController().signal,
|
||||
inputs: { ...inputs, cross: crossWithUphill(inputs.cross as CrossInput | null, [station]) },
|
||||
inputs: { ...inputs, cross: crossWithUphill(shaped, [station]) },
|
||||
out,
|
||||
before: {},
|
||||
prev: null,
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
type CrossRefreshInput,
|
||||
} from "../../B06_Section/B06_Section_Cross_Refresh";
|
||||
import { bermSpansFromStructures, bermSpecAt } from "../../B06_Section/B06_Section_UI_Page_Persist";
|
||||
import { registeredKeys, stationIntervalOf } from "../../B06_Section/B06_Section_Station_Keys";
|
||||
import { detailCross } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
|
||||
import type { CrossOutput, PipelineStation } from "./logic/design_pipeline_types";
|
||||
|
||||
@@ -245,6 +246,9 @@ function startPagePipeline(shared: DesignPageShared, profile: DesignProfile): vo
|
||||
},
|
||||
),
|
||||
plantedBase, // 저장 노선 종단 기준 = 입력이 심는 측점(386-3 · 보기 심기 무관)
|
||||
// 저장 상세 측점마다 등록 열쇠 — 초안 자리로 뜬 시설 측점의 입력 짝(주인 id · 457)
|
||||
stationKeys: (routeId, detail) =>
|
||||
registeredKeys(projectId, routeId, detail.cross_sections, stationIntervalOf(detail)),
|
||||
structureAreas: (detail) => {
|
||||
// 초안 상세에도 저장 안 한 조절(옹벽 · 세월 · BOX · 집수 · 417 M3) — 면적 얹기 앞
|
||||
const routeId = routeIdOf(shared);
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
* `resources/tester/test_346_2_pipeline_cross_page.py`.
|
||||
* crossWithUphill cross 단계 입력에 단면 유형 기본값(상단측)을 얹음 — 서버 노선 저장이 단면 유형 ·
|
||||
* 측구 방향을 새 지형으로 정하는 것과 같은 규칙(넘기는 칸 = 부르는 쪽 입력).
|
||||
* draftSectionDetail ground · profile · cross → 상세(측점 · 지반 · 횡단 설계 · 얹힌 시설 제원).
|
||||
* crossWithShapes 얹힌 시설 맥락(포장 · 기울임 · 하강) = 지금 현황을 새 측점에 다시 얹은 그림에서(457).
|
||||
* draftSectionDetail ground · profile · cross → 상세(측점 · 지반 · 횡단 설계 · 그림 자료 · 카드 이름 = 지금
|
||||
* 현황을 새 측점에 다시 얹음 · 캐시 `withDraftShapes` 와 같은 `attachShapes` · 457).
|
||||
*
|
||||
* 순수 — 화면 · fetch · 세션 없음.
|
||||
* ========================================================================== */
|
||||
@@ -26,24 +28,26 @@ import type {
|
||||
import type { ProfileAlignment } from "./B05_Profile_UI_Profile_Alignment";
|
||||
import { toDesignProfile } from "./B05_Profile_UI_Profile_Data";
|
||||
import { stationLabel } from "../../common_util/common_util_station";
|
||||
import { attachShapes, type AttachType } from "../../common_util/common_util_structure_attach";
|
||||
import { registryDefaults } from "../../common_util/common_util_culvert_sets";
|
||||
import {
|
||||
planStations,
|
||||
SAME_STATION_M,
|
||||
type PlanPipe,
|
||||
type PlanStructure,
|
||||
type PlanType,
|
||||
} from "../../common_util/common_util_structure_station";
|
||||
import { stationIdOf } from "./B05_Profile_Pipeline_Grade";
|
||||
|
||||
/** 측점에 얹힌 시설 제원(저장 상세에서 측점 열쇠로 옮김) — 낡음 판정 · 그림이 읽음 · 카드 번호(`station_id` ·
|
||||
* 있으면 저장 카드 번호를 이어 씀 · 440). */
|
||||
export type SectionSpecs = Partial<
|
||||
Pick<
|
||||
CrossSection,
|
||||
| "station_id"
|
||||
| "structure"
|
||||
| "culvert"
|
||||
| "ford"
|
||||
| "box"
|
||||
| "ford_pavement"
|
||||
| "revetment"
|
||||
| "pavement"
|
||||
| "underdrain"
|
||||
>
|
||||
>;
|
||||
/** 측점 열쇠로 이어 받는 카드 번호(`station_id` · 저장 카드 번호를 이어 씀 · 440) — 그림 자료 · 이름은 안 옮김(457). */
|
||||
export type SectionSpecs = Partial<Pick<CrossSection, "station_id">>;
|
||||
|
||||
/** 지금 현황(follow 뒤 관 · 구조물) + 구조물 표 — 새 측점에 그림 · 이름 · 시설 맥락을 다시 얹는 재료(457). */
|
||||
export interface ShapeStatus {
|
||||
pipes: ReadonlyArray<PlanPipe>;
|
||||
structures: ReadonlyArray<PlanStructure>;
|
||||
types: ReadonlyArray<AttachType>;
|
||||
}
|
||||
|
||||
/** cross 단계 입력 꼴(`B06_Section_Cross_Stage` `CrossStageInput`) — 칸은 그 단계 몫. */
|
||||
export interface CrossInput {
|
||||
@@ -128,19 +132,6 @@ const kindOf = (station: PipelineStation): SectionStation["kind"] =>
|
||||
? "curve"
|
||||
: "irregular";
|
||||
|
||||
/** 제원의 누가거리 = 새 측점(서버 부착은 따라간 관 · 구조물 자리 = 그 측점) — 옛 값이면 구조물 면적이 관 자리를 잘못 잡음(349-0n). */
|
||||
function atChainage(own: SectionSpecs, chainageM: number): SectionSpecs {
|
||||
const out: Record<string, unknown> = { ...own };
|
||||
for (const [name, spec] of Object.entries(own))
|
||||
if (
|
||||
spec &&
|
||||
typeof spec === "object" &&
|
||||
typeof (spec as { chainage_m?: unknown }).chainage_m === "number"
|
||||
)
|
||||
out[name] = { ...spec, chainage_m: chainageM };
|
||||
return out as SectionSpecs;
|
||||
}
|
||||
|
||||
/** 저장 상세 → 묶음 cross 출력(365-4 · 저장 노선 = 다시 셈 창구 한 벌이 고친 설계 그대로) — 측점이 상세와
|
||||
* 다르면(갈아끼움 전) null. */
|
||||
export function detailCross(
|
||||
@@ -181,7 +172,6 @@ export function stationSection(
|
||||
chainage_m: station.chainageM,
|
||||
label: stationLabel(station.chainageM, intervalM),
|
||||
kind: kindOf(station),
|
||||
...(own.structure ? { structure: own.structure } : {}),
|
||||
center_z: center,
|
||||
azimuth_deg: station.azimuthDeg,
|
||||
plan_radius_m: station.planRadiusM,
|
||||
@@ -215,8 +205,82 @@ export function stationSection(
|
||||
};
|
||||
}
|
||||
|
||||
/** 측점마다 그림 자료 · 카드 이름 = 지금 현황을 그 자리에 다시 얹음(캐시 `withDraftShapes` · 이름 = 측점 계획
|
||||
* 제목 · 457) — 옛 측점 그림을 옮기지 않음. */
|
||||
function attachStatus(sections: CrossSection[], status: ShapeStatus): void {
|
||||
const { pipes, structures, types } = status;
|
||||
attachShapes(sections as unknown as Array<Record<string, unknown>>, pipes, structures, types);
|
||||
const planned = planStations(pipes, structures, types, registryDefaults(types));
|
||||
for (const section of sections) {
|
||||
const hit = planned.find((p) => Math.abs(p.chainage_m - section.chainage_m) < SAME_STATION_M);
|
||||
if (hit) section.structure = hit.title;
|
||||
else delete section.structure;
|
||||
}
|
||||
}
|
||||
|
||||
/** 현황 묶음(입력 칸은 묶음 모양 `unknown` · 457) — 정의 옵션 · 이름이 빈 종류는 빈 목록 · 종류 id. */
|
||||
export const shapeStatus = (
|
||||
pipes: unknown,
|
||||
structures: unknown,
|
||||
types: ReadonlyArray<PlanType & { name?: string }>,
|
||||
): ShapeStatus =>
|
||||
({
|
||||
pipes,
|
||||
structures,
|
||||
types: types.map((type) => ({
|
||||
...type,
|
||||
name: type.name ?? type.type_id,
|
||||
options: type.options ?? [],
|
||||
})),
|
||||
}) as ShapeStatus;
|
||||
|
||||
/** 그 측점 그림 자료 → 얹힌 시설 맥락(B06 `Cross_Refresh` 와 같은 규칙 · 서버 `tilt_ranges` · `ford_drop_at` 짝). */
|
||||
function shapeContext(section: CrossSection): Record<string, unknown> {
|
||||
const drop = Number(section.ford?.overflow_depth_m);
|
||||
return {
|
||||
paved: Boolean(section.pavement?.length) || Boolean(section.ford_pavement),
|
||||
tilted: Boolean(section.ford_pavement || section.underdrain),
|
||||
tiltPct: section.ford_pavement?.slope_pct ?? null, // 맹암거 = 표준 경사(371-3)
|
||||
surfaceDropM: drop > 0 ? drop : 0,
|
||||
};
|
||||
}
|
||||
|
||||
/** 측점마다 시설 맥락(포장 · 물넘이 기울임 · 세월교 노면 하강) = 지금 현황을 새 측점에 다시 얹은 그림에서(457). */
|
||||
export function crossWithShapes(
|
||||
input: CrossInput | null,
|
||||
stations: ReadonlyArray<PipelineStation>,
|
||||
status: ShapeStatus | null,
|
||||
): CrossInput | null {
|
||||
if (!input || !status) return input;
|
||||
const sections = stations.map((station) => ({ chainage_m: station.chainageM }) as CrossSection);
|
||||
attachShapes(
|
||||
sections as unknown as Array<Record<string, unknown>>,
|
||||
status.pipes,
|
||||
status.structures,
|
||||
status.types,
|
||||
);
|
||||
const byKey: CrossInput["byKey"] = { ...input.byKey };
|
||||
stations.forEach((station, i) => {
|
||||
byKey[station.key] = { ...byKey[station.key], ...shapeContext(sections[i]) };
|
||||
});
|
||||
return { ...input, byKey };
|
||||
}
|
||||
|
||||
/** cross 단계 입력 = 시설 맥락(새 자리 그림) · 단면 유형 기본값(상단측) · 소단(지금 구조물) 얹음 — 페이지 묶음 cross. */
|
||||
export const crossWithContext = (
|
||||
input: CrossInput | null,
|
||||
stations: ReadonlyArray<PipelineStation>,
|
||||
status: ShapeStatus | null,
|
||||
bermAt: BermAt | null,
|
||||
): CrossInput | null =>
|
||||
crossWithBerm(
|
||||
crossWithUphill(crossWithShapes(input, stations, status), stations),
|
||||
stations,
|
||||
bermAt,
|
||||
);
|
||||
|
||||
/** 묶음 출력 → 상세. 저장 상세의 종단 머리(이름)는 그대로 · 측점 · 지반 · 횡단 · 계획선 표본은 새 노선
|
||||
* (십자선 대체 높이도 초안 종단 · 413). */
|
||||
* (십자선 대체 높이도 초안 종단 · 413) · 그림 자료 · 카드 이름 = `status`(지금 현황)를 새 측점에 다시 얹음(457). */
|
||||
export function draftSectionDetail(
|
||||
saved: SectionDetailResponse,
|
||||
ground: GroundOutput,
|
||||
@@ -224,15 +288,12 @@ export function draftSectionDetail(
|
||||
cross: CrossOutput | null,
|
||||
specs: ReadonlyMap<string, SectionSpecs>,
|
||||
intervalM: number,
|
||||
status: ShapeStatus | null = null,
|
||||
): SectionDetailResponse {
|
||||
const sections: CrossSection[] = ground.stations.map((station) =>
|
||||
stationSection(
|
||||
station,
|
||||
cross,
|
||||
atChainage(specs.get(station.key) ?? {}, station.chainageM),
|
||||
intervalM,
|
||||
),
|
||||
stationSection(station, cross, specs.get(station.key) ?? {}, intervalM),
|
||||
);
|
||||
if (status) attachStatus(sections, status);
|
||||
const { chainageM, z } = ground.longitudinal;
|
||||
return {
|
||||
longitudinal: {
|
||||
|
||||
@@ -514,7 +514,10 @@ async function run() {
|
||||
st("pipe:p28", 28, "pipe"),
|
||||
st("end:EP", 40, "end"),
|
||||
];
|
||||
const source = crossMod.pipelineCrossSource(C, screen, savedList, 7);
|
||||
// 초안 자리 관 측점(27 m) = 등록 열쇠(주인 id)로 저장 관 측점과 짝(457 · 차례 짝 없음)
|
||||
const source = crossMod.pipelineCrossSource(C, screen, savedList, 7, () =>
|
||||
savedList.map((s) => s.key),
|
||||
);
|
||||
const bare = crossMod.pipelineCrossSource(
|
||||
C,
|
||||
{ ...screen, cross_sections: screen.cross_sections.filter((x) => x.chainage_m !== 27) },
|
||||
@@ -534,6 +537,7 @@ async function run() {
|
||||
const viewFree = {
|
||||
grid20: source.input.byKey["grid:1"],
|
||||
pipeSpec: source.specs.get("pipe:p28") ?? null,
|
||||
pipeCarried: Boolean(source.input.byKey["pipe:p28"]),
|
||||
specsDiffer: source.input.specsKey !== bare.input.specsKey,
|
||||
firstStored: firstPvi(planlineOf(() => savedBase.basePvi)),
|
||||
firstShaken: firstPvi(planlineOf(undefined)),
|
||||
|
||||
@@ -9,6 +9,7 @@ const STUBS = {
|
||||
B05_Profile_Pipeline_Grade: { stationIdOf: idOf },
|
||||
B05_Profile_UI_Profile_Data: { toDesignProfile: () => ({}) },
|
||||
};
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs");
|
||||
const cache = new Map();
|
||||
function load(full) {
|
||||
if (cache.has(full)) return cache.get(full).exports;
|
||||
@@ -18,6 +19,7 @@ function load(full) {
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box);
|
||||
const req = (r) => {
|
||||
if (CATALOG.isCatalogRequest(r)) return CATALOG; // 형상 얹기(Pipeline_Detail · 457)가 끌어감
|
||||
const name = r.split("/").pop();
|
||||
if (STUBS[name]) return STUBS[name];
|
||||
if (r.startsWith("@util/")) return load(path.join(ROOT, "common_util", `${r.slice(6)}.ts`));
|
||||
|
||||
@@ -6,7 +6,8 @@ const ROOT = path.resolve(__dirname, "..", "..");
|
||||
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||||
const idOf = (m) => `station_${String(Math.round(m * 1000)).padStart(12, "0")}`;
|
||||
const REAL =
|
||||
/(B05_Profile_Pipeline_Detail|B06_Section_Station_Swap|design_pipeline_cross|common_util_station_key)$/;
|
||||
/(B05_Profile_Pipeline_Detail|B06_Section_Station_Swap|design_pipeline_cross|common_util_\w+)$/;
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs");
|
||||
const STUBS = {
|
||||
B05_Profile_Pipeline_Grade: { stationIdOf: idOf },
|
||||
common_util_station: { stationLabel: (m) => String(m) },
|
||||
@@ -27,6 +28,9 @@ function load(full) {
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box);
|
||||
const req = (r) => {
|
||||
if (CATALOG.isCatalogRequest(r)) return CATALOG;
|
||||
if (r.endsWith(".json"))
|
||||
return JSON.parse(fs.readFileSync(path.resolve(path.dirname(full), r)));
|
||||
const name = r.split("/").pop();
|
||||
if (STUBS[name]) return STUBS[name];
|
||||
if (REAL.test(r)) return load(path.resolve(path.dirname(full), r + ".ts"));
|
||||
@@ -83,6 +87,13 @@ const draftStations = [
|
||||
const oldKeys = savedStations.map((s) => s.key);
|
||||
|
||||
const source = crossMod.pipelineCrossSource("p1", saved, savedStations, null);
|
||||
// 그림 자료 = 지금 현황(따라간 관 · 457) — 관 p35100 이 354 로
|
||||
const types = [{ type_id: "pipe", name: "배수관", options: [] }];
|
||||
const status = detailMod.shapeStatus(
|
||||
[{ id: "p35100", facility: "pipe", chainage_m: 354, options: { pipe_diameter_mm: "800" } }],
|
||||
[],
|
||||
types,
|
||||
);
|
||||
const draft = detailMod.draftSectionDetail(
|
||||
saved,
|
||||
{ stations: draftStations, longitudinal: { chainageM: [0, 400], z: [0, 0] } },
|
||||
@@ -90,6 +101,7 @@ const draft = detailMod.draftSectionDetail(
|
||||
null,
|
||||
source.specs,
|
||||
20,
|
||||
status,
|
||||
);
|
||||
const swapped = swapMod.swapCrossSections(saved, draftStations, oldKeys, 20).detail;
|
||||
const ids = (detail) => detail.cross_sections.map((s) => s.station_id);
|
||||
@@ -106,6 +118,7 @@ const added = detailMod.draftSectionDetail(
|
||||
null,
|
||||
source.specs,
|
||||
20,
|
||||
status,
|
||||
);
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
/* 457 헬퍼 — 노선 바뀜 뒤 초안 카드(묶음 새 상세) 그림 · 이름 · 시설 맥락 = 지금 현황을 새 측점에 다시 얹은 것.
|
||||
* 진짜 TS(`design_pipeline_cross` · `B05_Profile_Pipeline_Detail` · 측점 · 따라감 · 형상 얹기) · 세션 창구만 가짜.
|
||||
* 재현 E · F(시점 당김) · G(세션에서 놓은 BOX) · H(관 둘 엇갈려 옮김) — 결과 = 표준출력 JSON. 짝 = `test_457_draft_card.py`. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ROOT = path.resolve(__dirname, "..", "..");
|
||||
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs");
|
||||
|
||||
const STUBS = {
|
||||
b_page_state: { readState: () => null },
|
||||
B06_Section_Section_Canon: { savedDesigns: () => null },
|
||||
B06_Section_UI_Standard_Panel: {
|
||||
effectiveStandardCross: () => ({ road_width_m: 3 }),
|
||||
readRockBoundaryDefault: () => null,
|
||||
},
|
||||
B05_Profile_UI_Profile_Data: { toDesignProfile: () => ({}) },
|
||||
};
|
||||
const cache = new Map();
|
||||
function resolveTs(base) {
|
||||
for (const c of [`${base}.ts`, base]) if (fs.existsSync(c) && fs.statSync(c).isFile()) return c;
|
||||
throw new Error(`없음: ${base}`);
|
||||
}
|
||||
function load(full) {
|
||||
if (cache.has(full)) return cache.get(full).exports;
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box);
|
||||
const req = (r0) => {
|
||||
if (CATALOG.isCatalogRequest(r0)) return CATALOG;
|
||||
const r = r0.replace(/\.js$/, "");
|
||||
const stub = STUBS[r.split("/").pop()];
|
||||
if (stub) return stub;
|
||||
if (r.endsWith(".json"))
|
||||
return JSON.parse(fs.readFileSync(path.join(path.dirname(full), r), "utf8"));
|
||||
if (r.startsWith("@util/")) return load(resolveTs(path.join(ROOT, "common_util", r.slice(6))));
|
||||
if (r.startsWith(".")) return load(resolveTs(path.join(path.dirname(full), r)));
|
||||
return require(r);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, req);
|
||||
return box.exports;
|
||||
}
|
||||
const P = (...a) => path.join(ROOT, ...a);
|
||||
const follow = load(P("common_util", "common_util_route_follow.ts"));
|
||||
const SS = load(P("B05_Profile", "logic", "B05_Profile_Sections_Stations.ts"));
|
||||
const AT = load(P("common_util", "common_util_structure_attach.ts"));
|
||||
const ST = load(P("common_util", "common_util_structure_station.ts"));
|
||||
const CS = load(P("common_util", "common_util_culvert_sets.ts"));
|
||||
const PD = load(P("B05_Profile", "logic", "B05_Profile_Pipeline_Detail.ts"));
|
||||
const CR = load(P("A00_Common", "design", "design_pipeline_cross.ts"));
|
||||
|
||||
const def = JSON.parse(
|
||||
fs.readFileSync(P("resources", "master_template", "구조물정의.json"), "utf8"),
|
||||
);
|
||||
const types = def.types
|
||||
.filter((t) => t.enabled !== false)
|
||||
.map((t) => ({
|
||||
type_id: t.type_id,
|
||||
group: t.group,
|
||||
placement: t.placement,
|
||||
managed_by: t.managed_by,
|
||||
name: t.name,
|
||||
options: (t.options || []).map((o) => ({ key: o.key, label: o.label, default: o.default })),
|
||||
}));
|
||||
const SHAPES = AT.SHAPE_KEYS;
|
||||
const idOf = (m) => `station_${String(Math.round(m * 1000)).padStart(12, "0")}`;
|
||||
const kindOf = (s) =>
|
||||
s.kind === "end"
|
||||
? s.key.endsWith("EP")
|
||||
? "ep"
|
||||
: "bp"
|
||||
: s.kind === "grid"
|
||||
? "regular"
|
||||
: s.kind === "curve"
|
||||
? "curve"
|
||||
: "irregular";
|
||||
const ground = (s) => ({
|
||||
...s,
|
||||
ground: { centerZ: 100, left: [{ offsetM: 5, z: 101 }], right: [{ offsetM: 5, z: 99 }] },
|
||||
});
|
||||
const sectionsFrom = (S) =>
|
||||
S.map((s) => ({
|
||||
station_id: idOf(s.chainageM),
|
||||
chainage_m: s.chainageM,
|
||||
label: "",
|
||||
kind: kindOf(s),
|
||||
center_x: s.x,
|
||||
center_y: s.y,
|
||||
samples: [],
|
||||
design: { design_elevation_m: 101 },
|
||||
}));
|
||||
const xy = (L) => L.map(([x, y]) => ({ x, y }));
|
||||
function at(L, ch) {
|
||||
let walked = 0;
|
||||
for (let i = 1; i < L.length; i += 1) {
|
||||
const [a, b] = [L[i - 1], L[i]];
|
||||
const span = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
||||
if (walked + span >= ch) {
|
||||
const t = (ch - walked) / span;
|
||||
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
||||
}
|
||||
walked += span;
|
||||
}
|
||||
return L[L.length - 1];
|
||||
}
|
||||
const pipe = (id, facility, ch, L, options) => {
|
||||
const [x, y] = at(L, ch);
|
||||
return { id, facility, chainage_m: ch, x, y, options };
|
||||
};
|
||||
const wall = (L) => {
|
||||
const [x, y] = at(L, 70);
|
||||
return {
|
||||
structure_id: "w1",
|
||||
type_id: "retaining_wall",
|
||||
chainage_m: 70,
|
||||
start_m: 60,
|
||||
end_m: 80,
|
||||
x,
|
||||
y,
|
||||
options: { height_m: 2, side: "left" },
|
||||
};
|
||||
};
|
||||
/** 측점 그림 지문 — 얹힌 형상 열쇠 · 그 누가거리(구간이면 시작~끝). */
|
||||
const shapesOf = (s) =>
|
||||
SHAPES.filter((k) => s[k] !== undefined)
|
||||
.map((k) => {
|
||||
const v = s[k];
|
||||
const c = Array.isArray(v)
|
||||
? v.map((x) => `${x.start_m ?? x.chainage_m ?? ""}~${x.end_m ?? ""}`).join("/")
|
||||
: v && (v.chainage_m ?? `${v.start_m ?? ""}~${v.end_m ?? ""}`);
|
||||
return `${k}@${c}`;
|
||||
})
|
||||
.join(" ");
|
||||
|
||||
/** 정답 = 새 측점(구조물 없는 꼴)에 지금 현황(따라간 관 · 구조물)을 얹은 그림 · 측점 계획 이름(캐시 `withDraftShapes`). */
|
||||
function truthOf(S2, pipes, structures) {
|
||||
const bare = S2.map((s) => ({ chainage_m: s.chainageM }));
|
||||
AT.attachShapes(bare, pipes, structures, types);
|
||||
const planned = ST.planStations(pipes, structures, types, CS.registryDefaults(types));
|
||||
return bare.map((s) => ({
|
||||
shapes: shapesOf(s),
|
||||
name: planned.find((p) => Math.abs(p.chainage_m - s.chainage_m) < 0.005)?.title ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
function scenario(L1, L2, saved, now, opts = {}) {
|
||||
const S1 = SS.buildStations(L1, [], saved.pipes, saved.structures, { types, intervalM: 20 });
|
||||
// 저장 상세(서버) = 저장 측점에 저장 현황 그림 · 초안 둔 채 열기면 화면 상세가 초안 자리(`canon`)
|
||||
const Scanon = opts.canon ?? S1;
|
||||
const detail = {
|
||||
longitudinal: { stations: [], design_profiles: [], length_m: 0, samples: [] },
|
||||
cross_sections: sectionsFrom(Scanon),
|
||||
};
|
||||
AT.attachShapes(
|
||||
detail.cross_sections,
|
||||
opts.canonPipes ?? saved.pipes,
|
||||
opts.canonStructures ?? saved.structures,
|
||||
types,
|
||||
);
|
||||
const pF = follow.projectPipePoints(now.pipes, xy(L2));
|
||||
const sF = follow.followStructures(now.structures, xy(L2), undefined, 20).map((m) => m.item);
|
||||
const S2 = SS.buildStations(L2, [], pF, sF, { types, intervalM: 20 }).map(ground);
|
||||
const keysOf = opts.keys ? () => opts.keys : undefined;
|
||||
const source = CR.pipelineCrossSource("p457", detail, S1, 7, keysOf);
|
||||
// 노선 초안 동안 등록 목록이 새 노선 것으로 바뀜 — 처음 본 열쇠 그대로여야 함
|
||||
const again = opts.keys
|
||||
? CR.pipelineCrossSource("p457", detail, S1, 7, () => [...opts.keys].reverse())
|
||||
: source;
|
||||
const status = PD.shapeStatus
|
||||
? PD.shapeStatus(pF, sF, types)
|
||||
: { pipes: pF, structures: sF, types };
|
||||
const card = PD.draftSectionDetail(
|
||||
detail,
|
||||
{ stations: S2, longitudinal: { chainageM: [0], z: [0] } },
|
||||
{},
|
||||
null,
|
||||
again.specs,
|
||||
20,
|
||||
status,
|
||||
);
|
||||
const truth = truthOf(S2, pF, sF);
|
||||
const shaped = PD.crossWithShapes ? PD.crossWithShapes(source.input, S2, status) : source.input;
|
||||
const ctx = (key) => {
|
||||
const e = shaped.byKey[key] ?? {};
|
||||
return [Boolean(e.paved), Boolean(e.tilted), e.tiltPct ?? null, e.surfaceDropM ?? 0];
|
||||
};
|
||||
return {
|
||||
rows: S2.map((s, i) => ({
|
||||
key: s.key,
|
||||
m: s.chainageM,
|
||||
card: shapesOf(card.cross_sections[i]),
|
||||
truth: truth[i].shapes,
|
||||
name: card.cross_sections[i].structure ?? null,
|
||||
truthName: truth[i].name,
|
||||
id: card.cross_sections[i].station_id,
|
||||
ctx: ctx(s.key),
|
||||
})),
|
||||
canonIds: Object.fromEntries(
|
||||
Scanon.map((s, i) => [s.key, detail.cross_sections[i].station_id]),
|
||||
),
|
||||
sourceKeys: Object.fromEntries([...source.specs].map(([k, v]) => [k, v.station_id])),
|
||||
specFields: [...new Set([...source.specs.values()].flatMap((v) => Object.keys(v)))],
|
||||
};
|
||||
}
|
||||
|
||||
const L1 = [
|
||||
[0, 0],
|
||||
[100, 0],
|
||||
[200, 60],
|
||||
];
|
||||
const P1 = [
|
||||
pipe("p1", "pipe", 47, L1, { pipe_diameter_mm: "800" }),
|
||||
pipe("fp", "ford_pavement", 125, L1, { ford_width_m: 12 }),
|
||||
];
|
||||
const facilities = { pipes: P1, structures: [wall(L1)] };
|
||||
const out = {};
|
||||
out.E = scenario(
|
||||
L1,
|
||||
[
|
||||
[-13, 0],
|
||||
[100, 0],
|
||||
[200, 60],
|
||||
],
|
||||
facilities,
|
||||
facilities,
|
||||
);
|
||||
out.F = scenario(
|
||||
L1,
|
||||
[
|
||||
[-7, 0],
|
||||
[100, 0],
|
||||
[200, 60],
|
||||
],
|
||||
facilities,
|
||||
facilities,
|
||||
);
|
||||
out.G = scenario(
|
||||
L1,
|
||||
[
|
||||
[-7, 0],
|
||||
[100, 0],
|
||||
[200, 60],
|
||||
],
|
||||
facilities,
|
||||
{
|
||||
pipes: [...P1, pipe("p9", "box_culvert", 90, L1, { body_width_m: 2 })],
|
||||
structures: [wall(L1)],
|
||||
},
|
||||
);
|
||||
// H — 세션에서 관 둘을 엇갈려 옮김(저장 전) · 초안 둔 채 열기(화면 상세 = 옮긴 자리) → 시점 +7
|
||||
{
|
||||
const saved = [
|
||||
pipe("p1", "pipe", 47, L1, { pipe_diameter_mm: "800" }),
|
||||
pipe("p2", "box_culvert", 90, L1, { body_width_m: 2 }),
|
||||
];
|
||||
const moved = [
|
||||
pipe("p1", "pipe", 110, L1, { pipe_diameter_mm: "800" }),
|
||||
pipe("p2", "box_culvert", 30, L1, { body_width_m: 2 }),
|
||||
];
|
||||
const Snow = SS.buildStations(L1, [], moved, [], { types, intervalM: 20 });
|
||||
out.H = scenario(
|
||||
L1,
|
||||
[
|
||||
[-7, 0],
|
||||
[100, 0],
|
||||
[200, 60],
|
||||
],
|
||||
{ pipes: saved, structures: [] },
|
||||
{ pipes: moved, structures: [] },
|
||||
{ canon: Snow, canonPipes: moved, keys: Snow.map((s) => s.key) },
|
||||
);
|
||||
}
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -170,10 +170,9 @@ def test_365_5_들어온_화면_무관(r):
|
||||
g = v["grid20"]
|
||||
# 설계 칸 = 세션 저장 상세(화면 상세가 굳힌 선택 false 대신 저장값) · 옛 측구 결과도 실음
|
||||
assert g["ditchChoice"] is None and g["ditchEnabled"] is False
|
||||
assert v["pipeSpec"] == {
|
||||
"culvert": {"type": "pipe", "diameter_m": 0.8}
|
||||
} # 초안 자리 관 = 차례 짝
|
||||
assert v["specsDiffer"] # 제원이 바뀌면 입력 지문도 바뀜(고리가 다시 셈)
|
||||
# 초안 자리 관 = 등록 열쇠 짝 · 넘기는 것 = 카드 번호 · 사용자 칸만(그림은 새 자리에서 · 457)
|
||||
assert v["pipeSpec"] == {} and v["pipeCarried"] # 짝 있음(시험 상세는 카드 번호 없음)
|
||||
assert v["specsDiffer"] # 짝지은 측점이 바뀌면 입력 지문도 바뀜(고리가 다시 셈)
|
||||
assert v["firstStored"] == v["savedFirst"] and v["firstShaken"] != v["savedFirst"]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""457 노선 바뀜 — 초안 카드 그림 · 이름 · 횡단 맥락 = 지금 현황을 새 측점에 다시 얹음(요구 270).
|
||||
|
||||
재현 E · F(시점 당김 +13 · +7 — 배관 · 물넘이포장 · 옹벽) · G(세션에서 놓은 BOX) ·
|
||||
H(관 둘 엇갈려 옮김 · 초안 둔 채 열기).
|
||||
① 모든 측점 카드 그림 · 이름 = 새 측점에 따라간 관 · 구조물을 얹은 것(캐시 `withDraftShapes` 한 벌)
|
||||
② 시설마다 제 측점 하나 · 옛 측점 그림을 옮기지 않음 · 카드 번호만 열쇠로 이어 받음
|
||||
③ 시설 맥락(포장 · 기울임 · 하강) = 다시 얹은 그림에서 · 짝 = 등록 열쇠(차례 짝 없음 · 엇갈림 0)
|
||||
④ 가게 쓰기 = 지금 가게 값과 견줌(되돌리기 뒤 같은 조작도 씀)
|
||||
TS 셈은 `helper_457_draft_card.cjs`.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_457_draft_card.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).with_name("helper_457_draft_card.cjs")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def got() -> dict:
|
||||
run = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert run.returncode == 0, run.stderr
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
def _row(rows: list, key: str) -> dict:
|
||||
return next(r for r in rows if r["key"] == key)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["E", "F", "G", "H"])
|
||||
def test_카드_그림_이름이_새_자리_현황(got, name) -> None:
|
||||
rows = got[name]["rows"]
|
||||
bad = [(r["key"], r["card"], r["truth"]) for r in rows if r["card"] != r["truth"]]
|
||||
assert not bad
|
||||
names = [(r["key"], r["name"], r["truthName"]) for r in rows if r["name"] != r["truthName"]]
|
||||
assert not names
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["E", "F", "G", "H"])
|
||||
def test_카드_번호만_이어_받음(got, name) -> None:
|
||||
assert got[name]["specFields"] == ["station_id"]
|
||||
|
||||
|
||||
def test_E_물넘이포장은_제_측점에만(got) -> None:
|
||||
rows = got["E"]["rows"]
|
||||
fp = _row(rows, "pipe:fp")
|
||||
assert "ford_pavement@" in fp["card"]
|
||||
assert fp["ctx"][:2] == [True, True] # 포장 · 기울임 = 새 자리 그림
|
||||
no6 = _row(rows, "grid:6") # 120 m — 옛 물넘이 범위(119~131) · 새 범위(132~144) 밖
|
||||
assert "ford_pavement" not in no6["card"] and no6["name"] is None
|
||||
assert no6["ctx"] == [False, False, None, 0]
|
||||
pipes = [r for r in rows if "culvert@" in r["card"]]
|
||||
assert [r["key"] for r in pipes] == ["pipe:p1"] # 배관 그림 = 관 측점 하나(No.3 아님)
|
||||
|
||||
|
||||
def test_G_세션에서_놓은_BOX_카드(got) -> None:
|
||||
box = _row(got["G"]["rows"], "pipe:p9")
|
||||
assert "box@" in box["card"] and box["name"]
|
||||
|
||||
|
||||
def test_H_엇갈려_옮긴_관_짝(got) -> None:
|
||||
h = got["H"]
|
||||
p1 = _row(h["rows"], "pipe:p1")
|
||||
p2 = _row(h["rows"], "pipe:p2")
|
||||
assert "culvert@" in p1["card"] and "box@" not in p1["card"]
|
||||
assert "box@" in p2["card"] and "culvert@" not in p2["card"]
|
||||
# 카드 번호 = 같은 관의 화면 상세 번호(등록 열쇠 짝 · 목록이 바뀌어도 처음 본 열쇠)
|
||||
assert h["sourceKeys"]["pipe:p1"] == h["canonIds"]["pipe:p1"]
|
||||
assert h["sourceKeys"]["pipe:p2"] == h["canonIds"]["pipe:p2"]
|
||||
assert p1["id"] == h["canonIds"]["pipe:p1"]
|
||||
|
||||
|
||||
def test_가게_쓰기는_지금_가게_값과_견줌() -> None:
|
||||
page = (ROOT / "A00_Common" / "design" / "design_pipeline_page.ts").read_text(encoding="utf-8")
|
||||
body = page[page.index("function writeFacilities") :]
|
||||
body = body[: body.index("\n }\n")]
|
||||
assert 'same(store.get("pipes"), next.pipes)' in body
|
||||
assert 'same(store.get("structures"), next.structures)' in body
|
||||
assert "written" not in page
|
||||
Reference in New Issue
Block a user