Merge remote-tracking branches 'origin/main_laptop_1' and 'origin/sub_laptop_5' into sub_laptop_2
This commit is contained in:
@@ -171,10 +171,12 @@ export function createFacilityLane(
|
||||
to.setStructureTypes([...types]);
|
||||
to.setStructures(marks());
|
||||
const signature = JSON.stringify(stations);
|
||||
// 종류 표를 받기 전 = 측점 계획 없음(빈 목록을 먼저 쓰면 종단도가 측점을 지웠다 다시 셈 · 467) ·
|
||||
// 같은 측점이면 안 씀 — 측점을 쓰면 종단도가 계산 묶음을 다시 돌림
|
||||
if (force || painted?.to !== to || painted.stations !== signature)
|
||||
if (types.length && (force || painted?.to !== to || painted.stations !== signature)) {
|
||||
to.setIrregularStations(stations);
|
||||
painted = { to, stations: signature };
|
||||
painted = { to, stations: signature };
|
||||
}
|
||||
to.setMinCoverPipes(pipes as MinCoverPipe[]);
|
||||
}
|
||||
|
||||
|
||||
@@ -424,6 +424,8 @@ export async function saveSharedDesign(shared: DesignPageShared, confirm: boolea
|
||||
done: async (isConfirm) => {
|
||||
markDesignClean(shared.projectId); // 떠남 확인 — 지금 초안이 저장분(346-4)
|
||||
if (isConfirm) {
|
||||
// 서버가 정본을 다시 씀 — 다음 페이지(B07)가 브라우저 셈 · 옛 세션 사본 말고 정본을 받게(470 N1)
|
||||
invalidateSectionDetail(shared.projectId);
|
||||
showToast(L("B06_Profile_Confirm_Success"), "success");
|
||||
goToWorkflowStage(shared.projectId, WORKFLOW_STEP_ROUTES[4]);
|
||||
return;
|
||||
|
||||
@@ -311,9 +311,9 @@ def _follow_moved_stations(
|
||||
남기면 측점선 없는 변화점이 계획선을 꺾는다(192-1 — 관 769.28 → 769.38 뒤 두 자리에
|
||||
편집이 남아 0.1m 사이 −465% 계단). 반올림 차이(0.006m 안) · 옛 소수 측점의 정수 m 맞춤
|
||||
자리(272)는 옮김. 옮긴 관 · 구조물(427 · 요구 255-2) — 짝 못 찾은 옛 측점과 새 측점이 같은
|
||||
수면 차례대로 짝 · 높이 고친 꼭짓점(`station_offsets`)은 높이째 · 편집째 옮기고, 나머지 새
|
||||
측점은 자동 계획선 규칙(원지반 지남 · `_seat_born_nodes`)으로 꼭짓점을 세움(되돌리면 처음
|
||||
값) · 짝 없는 옛 측점 꼭짓점은 지움.
|
||||
수면 차례대로 짝 · 높이 고친 꼭짓점(`station_offsets`)은 높이째 · 편집째 옮기고, 나머지 짝
|
||||
새 측점은 자동 계획선 규칙(원지반 지남 · `_seat_born_nodes`)으로 꼭짓점을 세움(되돌리면 처음
|
||||
값) · 짝 없는 새 측점은 꼭짓점 안 세움(467) · 짝 없는 옛 측점 꼭짓점은 지움.
|
||||
측점 목록이 계획선 측점 줄과 달라져도 다시 센다(272). 다시 셌으면 새 등급 요약(DB 몫)을,
|
||||
아니면 `None`.
|
||||
"""
|
||||
@@ -351,8 +351,9 @@ def _follow_moved_stations(
|
||||
)
|
||||
lost = sorted((key for key, target in moves.items() if target is None), key=float)
|
||||
offsets = (stored.get("edits") or {}).get("station_offsets") or {}
|
||||
# 꼭짓점을 새로 세울 측점 — 짝이면 높이 안 고친 것만(편집 R 은 옮김) · 짝 없으면 새 측점 전부
|
||||
seat = born
|
||||
# 꼭짓점을 새로 세울 측점 — 짝(옮긴 관 · 구조물)이면 높이 안 고친 것만(편집 R 은 옮김) · 짝 없는 새 측점은
|
||||
# 안 세움(계획선 그대로 = 브라우저가 보인 계획고 · [저장]으로 값이 안 바뀜 · 467 · 요구 272)
|
||||
seat: list[float] = []
|
||||
if lost and len(lost) == len(born):
|
||||
seat = []
|
||||
for old_key, value in zip(lost, born):
|
||||
|
||||
@@ -9,8 +9,23 @@
|
||||
import { readState } from "../A00_Common/b_page_state";
|
||||
import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
|
||||
/** 정본 측점 설계 — 받을 때 뜬 사본(다시 셈 · 초안 얹기가 고치는 캐시 객체와 따로). */
|
||||
export type CanonRow = { chainage_m: number; design: CrossSection["design"] };
|
||||
/** 정본 측점 설계 — 받을 때 뜬 사본(다시 셈 · 초안 얹기가 고치는 캐시 객체와 따로) · 자리 · 표본(제자리로 돌아온
|
||||
* 측점이 정본 표본을 다시 씀 · 467 · 배열은 받은 것 그대로 · 다시 셈은 새 배열로 갈아 끼움). */
|
||||
export type CanonRow = { chainage_m: number; design: CrossSection["design"] } & Partial<
|
||||
Pick<
|
||||
CrossSection,
|
||||
| "center_x"
|
||||
| "center_y"
|
||||
| "center_z"
|
||||
| "frame"
|
||||
| "samples"
|
||||
| "azimuth_deg"
|
||||
| "plan_radius_m"
|
||||
| "curve_outer_side"
|
||||
| "curve_widening_m"
|
||||
| "uphill_side"
|
||||
>
|
||||
>;
|
||||
|
||||
const canon = new Map<string, CanonRow[]>();
|
||||
/** 묶음이 먼저 푼 세션 상세 — 상세 가게가 처음 읽을 때 가져감(다시 안 풂). */
|
||||
@@ -23,9 +38,19 @@ export function keepCanon(projectId: string, routeId: number, detail: SectionDet
|
||||
parsed.delete(key);
|
||||
canon.set(
|
||||
key,
|
||||
detail.cross_sections.map(({ chainage_m, design }) => ({
|
||||
chainage_m,
|
||||
design: design ? { ...design } : design,
|
||||
detail.cross_sections.map((section) => ({
|
||||
chainage_m: section.chainage_m,
|
||||
design: section.design ? { ...section.design } : section.design,
|
||||
center_x: section.center_x,
|
||||
center_y: section.center_y,
|
||||
center_z: section.center_z,
|
||||
frame: section.frame,
|
||||
samples: section.samples,
|
||||
azimuth_deg: section.azimuth_deg,
|
||||
plan_radius_m: section.plan_radius_m,
|
||||
curve_outer_side: section.curve_outer_side,
|
||||
curve_widening_m: section.curve_widening_m,
|
||||
uphill_side: section.uphill_side,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -244,6 +244,7 @@ export async function swapSectionStations(
|
||||
oldKeys,
|
||||
interval,
|
||||
useDesignStore(projectId, null).get("routeDraft") !== null,
|
||||
savedDesigns(projectId, routeId) ?? [],
|
||||
); // 초안 없으면 정본 표본(467)
|
||||
// 설계는 사본 — 면적 얹기 · 다시 셈이 앞 상세(초안 전 상세 등)의 설계를 제자리로 안 고침(349-0o)
|
||||
for (const section of swap.detail.cross_sections)
|
||||
|
||||
@@ -298,12 +298,13 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
// 값 수정·기준점 이동 모두 B06 에서 받는다 — 이동은 종단 끌기와 **같은 함수**를 탄다
|
||||
// (2026-09-12 일원화). 세부유역은 관 초안을 쓰면 배수유역도가 뒤에서 다시 나눈다(298-1).
|
||||
onPipeUpdate: (fromChainageM, toChainageM, attributes) => {
|
||||
// 값만 고치면 폼이 옛 측점을 그대로 돌려줌(안 고친 칸) — 0.005m 넘으면 옮긴 것(80.17 → 80 도 · 234-1)
|
||||
if (Math.abs(fromChainageM - toChainageM) > 0.005) movePipeTo(fromChainageM, toChainageM);
|
||||
// 값만 고치면 폼이 옛 측점을 그대로 돌려줌(안 고친 칸) — 0.005m 넘으면 옮긴 것(80.17 → 80 도 · 234-1) ·
|
||||
// 옮긴 뒤 옵션 · 캐시 얹기는 옮긴 자리(맞춘 정수 m)로 찾음(옛 자리 = 비운 그림 · 470 N2)
|
||||
const moved = Math.abs(fromChainageM - toChainageM) > 0.005;
|
||||
const at = (moved ? movePipeTo(fromChainageM, toChainageM) : null) ?? fromChainageM;
|
||||
if (attributes.summary_item !== undefined || attributes.group_items !== undefined) {
|
||||
// 옮긴 뒤 자리(toChainageM)에서 찾는다 — 안 옮겼으면 from 과 0.51m 안이라 같은 관이다.
|
||||
const mine = pipeFacilities.find(
|
||||
(entry) => Math.abs(entry.chainage_m - toChainageM) < PIPE_MATCH_M,
|
||||
(entry) => Math.abs(entry.chainage_m - at) < PIPE_MATCH_M,
|
||||
);
|
||||
const before = mine ? JSON.stringify(linkFields(mine)) : "";
|
||||
if (mine && attributes.summary_item !== undefined)
|
||||
@@ -322,14 +323,12 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
return;
|
||||
}
|
||||
// 값은 관 목록 초안(가게 pipes) 하나로 나간다 — 조정 예약(culvertopt)에 따로 담으면 비운 칸이
|
||||
// [저장] 때 옛 값으로 되살아난다. 옮기기 전 자리로 찾는다(저장 시점 목록 기준).
|
||||
const hit = pipeFacilities.find(
|
||||
(entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M,
|
||||
);
|
||||
// [저장] 때 옛 값으로 되살아난다.
|
||||
const hit = pipeFacilities.find((entry) => Math.abs(entry.chainage_m - at) < PIPE_MATCH_M);
|
||||
const was = hit?.facility ?? "pipe";
|
||||
const facility = attributes.facility ?? was;
|
||||
// 종류를 바꿈(54-4) — 옛 종류 그림 캐시에 새 종류 칸을 얹지 않음 · 옛 칸은 안 이음
|
||||
if (facility === was) deps.applyPipeOptions?.(fromChainageM, patch);
|
||||
if (facility === was) deps.applyPipeOptions?.(at, patch);
|
||||
if (hit) {
|
||||
hit.options =
|
||||
mergeFacilityOptions(
|
||||
@@ -533,10 +532,11 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
}
|
||||
|
||||
/** 관 옮기기 — 폼의 측점 칸과 종단 끌기가 같은 길을 탄다. 재분할은 [저장]·[확정] 뒤. */
|
||||
function movePipeTo(fromChainageM: number, toChainageM: number): void {
|
||||
/** 관을 옮김 — 옮긴 자리(정수 m) · 그 자리 관이 없으면 null. */
|
||||
function movePipeTo(fromChainageM: number, toChainageM: number): number | null {
|
||||
if (!deps.projectId) {
|
||||
showToast(PIPE_MOVE_GUIDE, "error");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const to = snapStructureStation(toChainageM); // 정수 m · 일반 측점 붙기(234-1)
|
||||
const hit = pipeFacilities.find(
|
||||
@@ -555,6 +555,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
section.setPipeFacilities(pipeFacilities);
|
||||
pushMarks();
|
||||
showToast(PIPE_MOVE_NOTICE, "success");
|
||||
return hit ? to : null;
|
||||
}
|
||||
|
||||
/** 관 빼기 — 목록·폼·종단 우클릭이 같은 길을 탄다. 정본은 [저장]·[확정]에서 나간다. */
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* · 지반 표본 · 상단측 · 중심고 = 자리 · 축이 같은 앞 횡단이면 앞 값(서버 샘플링 정본 · 덧 측점만 넣어도 나머지
|
||||
* 측점 면적이 브라우저 지형으로 안 바뀜 · 433-11) · 새 측점 · 옮긴 측점만 지반 표본(채워 왔으면).
|
||||
* · 노선 초안 없는 차례(관 · 구조물 · 추가 측점만) = 같은 자리(누가거리 · 좌표 0.01 m · 축 1e-3) 앞 횡단 — 없으면
|
||||
* 그 자리 옛 횡단(격자에 구조물을 놓고 뺌) — 의 기하 · 표본 그대로(브라우저 측점 틀 ↔ 서버 틀 mm 차가 정본 표본을
|
||||
* 그 자리 옛 횡단(격자에 구조물을 놓고 뺌) · 그다음 저장 정본(`saved` · 옮겼다 제자리로 온 관) — 의 기하 · 표본 그대로(브라우저 측점 틀 ↔ 서버 틀 mm 차가 정본 표본을
|
||||
* 안 덮음 · 467 · 요구 272) · 설계는 지금 규칙.
|
||||
* · 새 측점 = 지반 표본 + 기본 설계(계획고 0 → 낡음 → 브라우저 다시 셈이 곧 채움).
|
||||
* · 상단측 = 새 지반 표본으로 다시 셈한 측점은 다시(단면 유형 기본값 재료 · 서버 횡단 파일 `uphill_side` 짝 · 428).
|
||||
@@ -109,9 +109,10 @@ function kindOf(station: PipelineStation): SectionStation["kind"] {
|
||||
return station.kind === "grid" ? "regular" : "irregular";
|
||||
}
|
||||
|
||||
/** 같은 목록인가 — 열쇠 · 누가거리가 차례대로 같음(제자리 갱신이면 갈아끼우지 않음). */
|
||||
/** 같은 목록인가 — 열쇠 · 누가거리가 차례대로 같음(제자리 갱신이면 갈아끼우지 않음) · 시설 측점 자리가 일반
|
||||
* 측점으로 돌아감(관 · 구조물 지움 · 467)은 다른 목록. */
|
||||
export function sameStationList(
|
||||
sections: ReadonlyArray<{ chainage_m: number }>,
|
||||
sections: ReadonlyArray<{ chainage_m: number; kind?: SectionStation["kind"] }>,
|
||||
oldKeys: ReadonlyArray<string>,
|
||||
stations: ReadonlyArray<PipelineStation>,
|
||||
): boolean {
|
||||
@@ -120,7 +121,8 @@ export function sameStationList(
|
||||
stations.every(
|
||||
(station, index) =>
|
||||
Math.abs(station.chainageM - sections[index].chainage_m) < 1e-6 &&
|
||||
(oldKeys[index] === station.key || kindOf(station) === "regular"),
|
||||
(oldKeys[index] === station.key ||
|
||||
(kindOf(station) === "regular" && sections[index].kind !== "irregular")),
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -149,8 +151,24 @@ export interface StationSwap {
|
||||
moved: boolean;
|
||||
}
|
||||
|
||||
/** 정본 자리 한 벌(저장 상세 측점 · 467) — 제자리로 돌아온 측점이 정본 기하 · 표본을 다시 씀. */
|
||||
export type CanonPlace = Pick<
|
||||
CrossSection,
|
||||
| "chainage_m"
|
||||
| "center_x"
|
||||
| "center_y"
|
||||
| "center_z"
|
||||
| "frame"
|
||||
| "samples"
|
||||
| "azimuth_deg"
|
||||
| "plan_radius_m"
|
||||
| "curve_outer_side"
|
||||
| "curve_widening_m"
|
||||
| "uphill_side"
|
||||
>;
|
||||
|
||||
/** 앞 횡단과 자리(누가거리 · 좌표 0.01 m) · 축(왼쪽 단위 벡터 · 허용 `axisTol`)이 같은가 — 같으면 앞 지반 표본을 그대로 씀. */
|
||||
function samePlace(prior: CrossSection, station: PipelineStation, axisTol = 1e-4): boolean {
|
||||
function samePlace(prior: CanonPlace, station: PipelineStation, axisTol = 1e-4): boolean {
|
||||
const [lx, ly] = prior.frame?.left_xy ?? [Number.NaN, Number.NaN];
|
||||
return (
|
||||
Math.abs(prior.chainage_m - station.chainageM) < 0.01 &&
|
||||
@@ -174,6 +192,7 @@ export function swapCrossSections(
|
||||
oldKeys: ReadonlyArray<string>,
|
||||
intervalM: number | null,
|
||||
routeDraft = false,
|
||||
saved: ReadonlyArray<Partial<CanonPlace>> = [],
|
||||
): StationSwap {
|
||||
const byKey = new Map(detail.cross_sections.map((section, i) => [oldKeys[i], section]));
|
||||
const used = new Set<CrossSection>();
|
||||
@@ -218,13 +237,15 @@ export function swapCrossSections(
|
||||
moves.set(text(prior.chainage_m), text(station.chainageM));
|
||||
// 노선 초안 없는 차례 · 같은 자리 = 앞 횡단(없으면 그 자리 옛 횡단 · 격자에 구조물을 놓고 뺌) 기하 · 표본 그대로
|
||||
// (467 · 되돌리면 정본과 같음)
|
||||
const atPlace = (section: CrossSection | undefined): section is CrossSection =>
|
||||
!!section?.samples?.length && samePlace(section, station, CANON_AXIS_TOL);
|
||||
const canon = routeDraft
|
||||
const atPlace = (section: Partial<CanonPlace> | undefined): section is CanonPlace =>
|
||||
!!section?.samples?.length &&
|
||||
!!section.frame &&
|
||||
samePlace(section as CanonPlace, station, CANON_AXIS_TOL);
|
||||
const canon: CanonPlace | undefined = routeDraft
|
||||
? undefined
|
||||
: atPlace(prior)
|
||||
? prior
|
||||
: detail.cross_sections.find(atPlace);
|
||||
: (detail.cross_sections.find(atPlace) ?? saved.find(atPlace));
|
||||
// 자리 같은 앞 횡단 = 서버 표본 그대로(433-11) · 아니면 새 지반 표본(채워 왔으면)
|
||||
const fresh =
|
||||
!canon && !!station.ground && !(prior?.samples?.length && samePlace(prior, station));
|
||||
|
||||
@@ -347,8 +347,9 @@ export function oneStationList<S extends ListedStation, P extends { chainage_m:
|
||||
* 사라진 구조물 측점(`gone`)의 계획선 꼭짓점을 새 측점(`born`)으로 옮기거나 지우고, 새로 세울 측점을 고른다 —
|
||||
* 서버 `_follow_moved_stations`(B05_Profile_Router_Confirm.py) 짝: 0.006m 안 반올림 차이 · 옛 소수 측점의
|
||||
* 정수 m 맞춤 자리(272)면 높이 그대로 옮김. 옮긴 관 · 구조물(427) — 짝 못 찾은 옛 측점과 새 측점이 같은
|
||||
* 수면 차례대로 짝 · 높이 고친 꼭짓점(`edited` = 저장 `station_offsets` 키)은 높이째 · 나머지 새 측점은
|
||||
* `seat`(세움은 부르는 쪽이 `seatBornNodes` · B05_Profile/logic/B05_Profile_Grade_Seat.ts) · 짝 없는 옛
|
||||
* 수면 차례대로 짝 · 높이 고친 꼭짓점(`edited` = 저장 `station_offsets` 키)은 높이째 · 나머지 짝 새 측점은
|
||||
* `seat`(세움은 부르는 쪽이 `seatBornNodes` · B05_Profile/logic/B05_Profile_Grade_Seat.ts) · 짝 없는 새 측점은
|
||||
* 안 세움(467) · 짝 없는 옛
|
||||
* 꼭짓점은 지움 · `held` = 옮긴 뒤 높이 고친 꼭짓점 측점(서버 옮긴 `station_offsets` 키 · 433-5).
|
||||
* 거울 시험 = `resources/tester/test_427_1_follow_seat.py`.
|
||||
*/
|
||||
@@ -370,8 +371,9 @@ export function followMovedNodes<N extends { chainage_m: number }>(
|
||||
.filter((value) => taken.every((key) => Math.abs(key - value) >= EPS))
|
||||
.sort((a, b) => a - b);
|
||||
const lost = gone.filter((old) => moves.get(old) === null).sort((a, b) => a - b);
|
||||
// 꼭짓점을 새로 세울 측점 — 짝이면 높이 안 고친 것만 · 짝 없으면 새 측점 전부
|
||||
let seat = left;
|
||||
// 꼭짓점을 새로 세울 측점 — 짝(옮긴 관 · 구조물)이면 높이 안 고친 것만 · 짝 없는 새 측점은 안 세움(계획선
|
||||
// 그대로 · [저장]으로 값이 안 바뀜 · 467)
|
||||
let seat: number[] = [];
|
||||
if (lost.length && lost.length === left.length) {
|
||||
seat = [];
|
||||
lost.forEach((old, index) => {
|
||||
|
||||
@@ -74,6 +74,27 @@ out.stored = job.storedCases.map((spec) => m.storedBermSpec(spec));
|
||||
out.lane = steps;
|
||||
}
|
||||
|
||||
/* 2-2. 시설 줄 칠함 — 종류 표 받기 전 측점(빈 목록)을 종단도에 안 씀 · 받은 뒤 한 번 */
|
||||
{
|
||||
const P2 = "p272paint";
|
||||
const wrote = [];
|
||||
const fake = {
|
||||
setStructureTypes: () => undefined,
|
||||
setStructures: () => undefined,
|
||||
setIrregularStations: (list) => wrote.push(list.length),
|
||||
setMinCoverPipes: () => undefined,
|
||||
resetStationEdit: () => undefined,
|
||||
};
|
||||
const lane = m.createFacilityLane(P2, () => fake);
|
||||
lane.set({ pipes: [] });
|
||||
lane.set({ structures: [] }, false);
|
||||
lane.paint();
|
||||
const before = wrote.length;
|
||||
lane.setTypes([{ type_id: "berm", name: "소단", placement: "interval" }]);
|
||||
out.paint = { before, after: wrote.length };
|
||||
lane.dispose();
|
||||
}
|
||||
|
||||
/* 3. 다시 셈 소단 원천 — 사본 없음(저장분 design.berm) · 사본 = 같은 구간 · 사본 = 빈 목록 */
|
||||
{
|
||||
m.writeState("std-cross", job.standard, P);
|
||||
|
||||
@@ -183,6 +183,35 @@ const out = {};
|
||||
};
|
||||
}
|
||||
|
||||
/* 1-3. 관을 옮겼다 제자리로 — 저장 정본(saved)이 있으면 관 측점도 정본 표본 · 기하 */
|
||||
{
|
||||
const moved = stationsAt(55);
|
||||
const keep = m.swapCrossSections(canonDetail(), moved, KEYS, 20, false);
|
||||
const saved = canonDetail().cross_sections;
|
||||
const back = m.swapCrossSections(keep.detail, stationsAt(45), keysOf(moved), 20, false, saved);
|
||||
out.pipeBack = { pipe: report(back.detail)["45.00"], bytes: bytesSame(back.detail, [45]) };
|
||||
}
|
||||
|
||||
/* 1-4. 일반 측점 자리(40)에 선 관을 지움 — 자리 · 수 같아도 다른 목록(관 카드가 안 남음) */
|
||||
{
|
||||
const onGrid = stationsAt(45).map((s) =>
|
||||
s.key === "grid:2" ? { ...s, key: "pipe:p9", kind: "pipe", id: "p9" } : s,
|
||||
);
|
||||
const placed = m.swapCrossSections(canonDetail(), onGrid, KEYS, 20, false).detail;
|
||||
placed.cross_sections.find((s) => s.chainage_m === 40).culvert = { pipe: "p9" };
|
||||
const gone = stationsAt(45);
|
||||
out.pipeGone = {
|
||||
placedKind: placed.cross_sections.find((s) => s.chainage_m === 40).kind,
|
||||
same: m.sameStationList(placed.cross_sections, keysOf(onGrid), gone),
|
||||
sameUntouched: m.sameStationList(canonDetail().cross_sections, KEYS, gone),
|
||||
};
|
||||
const after = m.swapCrossSections(placed, gone, keysOf(onGrid), 20, false).detail;
|
||||
const at40 = after.cross_sections.find((s) => s.chainage_m === 40);
|
||||
out.pipeGone.culvertLeft = "culvert" in at40;
|
||||
out.pipeGone.kind = at40.kind;
|
||||
out.pipeGone.canon = report(after)["40.00"].same;
|
||||
}
|
||||
|
||||
/* 2. 가게 길(swapSectionStations) — 가게 routeDraft 로 가름: 없음 · 있음 · 초안 버린 직후 */
|
||||
const seed = (P, R) => {
|
||||
m.writeState("section-detail", canonDetail(), P, R);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/* 467 시험 묶음 진입점 — 갈아끼움 정본 표본 유지(진짜 모듈 · vite ssr). */
|
||||
export { swapCrossSections } from "../../B06_Section/logic/B06_Section_Station_Swap";
|
||||
export {
|
||||
sameStationList,
|
||||
swapCrossSections,
|
||||
} from "../../B06_Section/logic/B06_Section_Station_Swap";
|
||||
export {
|
||||
loadSectionDetail,
|
||||
swapSectionStations,
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
/* 466 덧(470 N2) 헬퍼 — 진짜 `createB06StructuresPanel` 의 B06 폼 관 고침(`onPipeUpdate`)이 관을 0.51 m 넘게
|
||||
* 옮기며 옵션도 고칠 때 옵션이 옮긴 관(초안)에 붙고 캐시 얹기도 새 자리인지. 가짜 모듈 틀 = helper_165_pavement_flow.cjs.
|
||||
* 표준출력 JSON 한 벌 · 짝 = test_466_pipe_move_options.py. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1)
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
|
||||
function loadTs(file, stubs = {}) {
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
const localRequire = (request) => {
|
||||
if (CATALOG.isCatalogRequest(request)) return CATALOG;
|
||||
if (stubs[request]) return stubs[request];
|
||||
throw new Error(`stub 없음: ${request}`);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
// 세션(structure-pick) — 메모리 한 벌
|
||||
const store = new Map();
|
||||
const selection = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"), {
|
||||
"../b_page_state": {
|
||||
readState: (key, pid) => store.get(`${key}:${pid}`) ?? null,
|
||||
writeState: (key, value, pid) => store.set(`${key}:${pid}`, value),
|
||||
},
|
||||
});
|
||||
|
||||
const PID = "p165";
|
||||
const TYPES = [
|
||||
{ type_id: "pavement_concrete", group: "G" },
|
||||
{ type_id: "hazard_sign", group: "E" },
|
||||
{ type_id: "pipe", group: "A", managed_by: "pipe_points" },
|
||||
];
|
||||
const PAVE = {
|
||||
structure_id: "pave",
|
||||
type_id: "pavement_concrete",
|
||||
placement: "interval",
|
||||
chainage_m: 30,
|
||||
start_m: 30,
|
||||
end_m: 250,
|
||||
};
|
||||
const PAVE2 = {
|
||||
structure_id: "pave2",
|
||||
type_id: "pavement_concrete",
|
||||
placement: "interval",
|
||||
chainage_m: 100,
|
||||
start_m: 100,
|
||||
end_m: 200,
|
||||
};
|
||||
const PIPES = [{ chainage_m: 80.17, facility: "pipe", source: "spacing", options: {} }];
|
||||
// 카드 = 20 m 측점 + 관 측점(비정규)
|
||||
const CARDS = [...Array.from({ length: 16 }, (_, i) => i * 20), 80.17]
|
||||
.sort((a, b) => a - b)
|
||||
.map((at) => ({ station_id: `s${at}`, chainage_m: at }));
|
||||
const cardAt = (at) => CARDS.find((card) => Math.abs(card.chainage_m - at) < 0.01);
|
||||
|
||||
/* ── 폼 흉내(B05 패널 · loadForm / loadPipeForm / resetForm 의 알림 차례) ── */
|
||||
let binding = null;
|
||||
let structures = [];
|
||||
const form = { id: null, pipe: null };
|
||||
const loadForm = (target) => {
|
||||
form.id = target?.structure_id ?? null;
|
||||
form.pipe = null;
|
||||
binding.onSelect(target);
|
||||
};
|
||||
const loadPipeForm = (chainageM) => {
|
||||
const hadStructure = form.id !== null;
|
||||
form.id = null;
|
||||
form.pipe = chainageM;
|
||||
if (hadStructure) binding.onSelect(null);
|
||||
};
|
||||
const resetForm = () => {
|
||||
const hadPipe = form.pipe !== null;
|
||||
form.pipe = null;
|
||||
loadForm(null);
|
||||
if (hadPipe) binding.onPipeSelect(null);
|
||||
};
|
||||
const node = { addEventListener() {}, querySelector: () => ({ append() {} }) };
|
||||
const section = {
|
||||
root: node,
|
||||
listRoot: node,
|
||||
facility: { root: node, setRevetSideLabels() {}, recount() {} },
|
||||
setTypes() {},
|
||||
setStructures(next) {
|
||||
structures = next;
|
||||
},
|
||||
setPipeFacilities() {},
|
||||
hasSelection: () => form.id !== null || form.pipe !== null,
|
||||
selectById(id) {
|
||||
if (id === null) {
|
||||
if (form.id !== null || form.pipe !== null) resetForm();
|
||||
return;
|
||||
}
|
||||
loadForm(structures.find((entry) => entry.structure_id === id) ?? null);
|
||||
},
|
||||
selectPipeByChainage(chainageM) {
|
||||
if (chainageM === null) {
|
||||
if (form.pipe !== null) resetForm();
|
||||
return;
|
||||
}
|
||||
loadPipeForm(chainageM);
|
||||
},
|
||||
};
|
||||
|
||||
const panelModule = loadTs(
|
||||
path.join(ROOT, "B06_Section", "B06_Section_UI_Page_Structures_Panel.ts"),
|
||||
{
|
||||
"../A00_Common/design/design_shared": {
|
||||
bindDesignView: (_shared, _view, parts) => {
|
||||
binding = parts.structures;
|
||||
},
|
||||
sharedStructures: () => section,
|
||||
},
|
||||
"../B05_Profile/B05_Profile_Api_Structures": {
|
||||
fetchStructureTypes: async () => TYPES,
|
||||
fetchStructures: async () => ({ structures: [] }),
|
||||
readPendingStructures: () => current.structures,
|
||||
pipesToStructureMarks: (pipes) =>
|
||||
pipes.map((pipe) => ({
|
||||
structure_id: `pipe-${pipe.chainage_m.toFixed(2)}`,
|
||||
type_id: pipe.facility,
|
||||
chainage_m: pipe.chainage_m,
|
||||
start_m: null,
|
||||
end_m: null,
|
||||
})),
|
||||
structureAnchorM: (s) => s.chainage_m ?? s.start_m ?? 0,
|
||||
writePendingStructures() {},
|
||||
dropStaleStructureTypes: async () => {}, // 정의 판 확인(322-0)
|
||||
staleDefinitionNotice: () => null, // 판 알림(322-2)
|
||||
},
|
||||
"../B05_Profile/B05_Profile_Api_Pipes_Draft": {
|
||||
readPendingPipes: () => null,
|
||||
writePendingPipes: (_pid, next) => (written = next),
|
||||
},
|
||||
"../B05_Profile/logic/B05_Profile_UI_Drainage_Facility_Merge": {
|
||||
mergeFacilityOptions: (was, next) => ({
|
||||
facility: next.facility,
|
||||
options: { ...(was.options ?? {}), ...(next.options ?? {}) },
|
||||
}),
|
||||
},
|
||||
"../B05_Profile/B05_Profile_UI_Drainage_Ford_Fill": {
|
||||
fordWaitsOf: () => new Set(),
|
||||
waitsFordHeight: () => false,
|
||||
},
|
||||
"../B04_PreProcess/B04_PreProcess_Api_Fetch": {
|
||||
fetchDetailPipePoints: async () => ({ pipe_points: PIPES, basins: [] }),
|
||||
},
|
||||
// 페이지 자료 줄(386-2) — 저장분 한 번
|
||||
"../A00_Common/design/design_page_data": {
|
||||
pageTypesChecked: async () => false,
|
||||
pageStructures: async () => ({ structures: [] }),
|
||||
pagePipes: async () => ({ pipe_points: PIPES, basins: [] }),
|
||||
},
|
||||
"../common_util/common_util_summary_item": {
|
||||
linkFields: () => ({}),
|
||||
readGroupItems: (x) => x ?? {},
|
||||
readSummaryItem: (x) => x ?? null,
|
||||
},
|
||||
"../B05_Profile/B05_Profile_UI_Structure_Pick_Session": { readStructurePick: () => null },
|
||||
"../A00_Common/design/design_selection": selection,
|
||||
"@ui/ui_template_elements": { showToast() {} },
|
||||
// 관 기준 측점 정수 맞춤(234-1) — 일반 측점 없으면 정수 m 만
|
||||
"../common_util/common_util_structure_station": {
|
||||
snapStructureStation: (value) => Math.floor(value + 0.5),
|
||||
},
|
||||
"./B06_Section_UI_Adjust_Dock": {
|
||||
adjustDockRoot: () => ({}),
|
||||
refreshAdjustSlots() {},
|
||||
resetAdjustDock() {},
|
||||
setAdjustSideSlots() {},
|
||||
},
|
||||
"./B06_Section_UI_Page_Structures_Spec": { withSpecDefaults: (o) => o ?? {} },
|
||||
// 옛 소수 관 맞춤(322-3 고침) · 관 세션 id(323-5) — 프로젝트 없음 = 옛 자리 이름
|
||||
"../B05_Profile/B05_Profile_UI_Structures_Snap": {
|
||||
snapNotice() {},
|
||||
snapPlan: () => ({ structures: [], pipes: [], skipped: 0 }),
|
||||
},
|
||||
"../A00_Common/design/design_pipe_ids": {
|
||||
pipeIdAt: (at) => `pipe-${at.toFixed(2)}`,
|
||||
followMovedPipe() {},
|
||||
},
|
||||
// 요구 178 — 알약 측점 짝(진짜 `plantsStation` · 272 측점 심기 한 벌)
|
||||
"../B05_Profile/B05_Profile_UI_Profile_Structures": loadTs(
|
||||
path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Structures.ts"),
|
||||
{
|
||||
"../A00_Common/touch_bridge": { pickPx: (px) => px }, // 기기대응 4-4 — 마우스 반경 그대로
|
||||
"@ui/ui_template_context_menu": { createMapContextMenu() {} },
|
||||
"./B05_Profile_UI_IrregularStations": {
|
||||
irregularStationId: (id) => `irregular:${id}`,
|
||||
isPipeStation: () => false,
|
||||
},
|
||||
"./B05_Profile_Api_Structures": {
|
||||
structureAnchorM: (s) => s.chainage_m ?? s.start_m ?? 0,
|
||||
},
|
||||
"../A00_Common/design/design_selection": selection,
|
||||
"../common_util/common_util_structure_station": loadTs(
|
||||
path.join(ROOT, "common_util", "common_util_structure_station.ts"),
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
const current = { structures: [] };
|
||||
let panel = null;
|
||||
let written = null;
|
||||
const applied = [];
|
||||
|
||||
(async () => {
|
||||
const out = {};
|
||||
const run = async (from, to, options) => {
|
||||
store.clear();
|
||||
PIPES.splice(0, PIPES.length, {
|
||||
chainage_m: 80.17,
|
||||
facility: "pipe",
|
||||
source: "spacing",
|
||||
options: { pipe_diameter_mm: 800 },
|
||||
});
|
||||
written = null;
|
||||
applied.length = 0;
|
||||
panel = panelModule.createB06StructuresPanel({
|
||||
projectId: PID,
|
||||
shared: { view: () => "cross", lane: { set() {}, setTypes() {}, setStored() {} } },
|
||||
detail: () => ({ cross_sections: CARDS }),
|
||||
stationInterval: () => 20,
|
||||
focusChainage() {},
|
||||
markStructure() {},
|
||||
onMarks() {},
|
||||
applyPipeOptions: (at, patch) => applied.push([at, patch.pipe_diameter_mm ?? null]),
|
||||
});
|
||||
await panel.load();
|
||||
binding.onPipeUpdate(from, to, { facility: "pipe", options });
|
||||
return {
|
||||
pipes: (written ?? []).map((pipe) => [
|
||||
pipe.chainage_m,
|
||||
pipe.options?.pipe_diameter_mm ?? null,
|
||||
]),
|
||||
applied: [...applied],
|
||||
};
|
||||
};
|
||||
out.far = await run(80.17, 100.4, { pipe_diameter_mm: 1200 }); // 옮김 + 옵션
|
||||
out.near = await run(80.17, 80, { pipe_diameter_mm: 1000 }); // 0.51 m 안 옮김 + 옵션
|
||||
out.stay = await run(80.17, 80.17, { pipe_diameter_mm: 900 }); // 옵션만
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -198,3 +198,8 @@ def test_쓰는_자리_하나():
|
||||
assert "writeBermSpans" not in sync and "useStructureIds(projectId, structures)" in sync
|
||||
lane = (ROOT / "A00_Common" / "design" / "design_facility_lane.ts").read_text(encoding="utf-8")
|
||||
assert "writeBermSpans(projectId, routeId, next)" in lane
|
||||
|
||||
|
||||
def test_시설_줄_종류_표_전_측점_안_칠함(out):
|
||||
assert out["paint"]["before"] == 0 # 종류 표 전 = 빈 측점을 종단도에 안 씀
|
||||
assert out["paint"]["after"] == 1 # 받은 뒤 한 번
|
||||
|
||||
@@ -107,3 +107,11 @@ def test_화면_선만_바뀌면_낡음_다시_셈() -> None:
|
||||
store = (ROOT / "B06_Section/B06_Section_Section_Store.ts").read_text(encoding="utf-8")
|
||||
fn = store[store.index("export function recrossIfStale(") :]
|
||||
assert "recrossSaved(projectId, key, detail, routeId)" in fn[: fn.index("\n}\n")]
|
||||
|
||||
|
||||
def test_확정_성공도_상세_비움() -> None:
|
||||
# 470 N1 — [확정] 은 다음 페이지(B07)로 바로 감 · [저장]처럼 상세 메모리 · 옛 세션 사본을 비움
|
||||
shared = (ROOT / "A00_Common/design/design_shared.ts").read_text(encoding="utf-8")
|
||||
done = shared[shared.index(" done: async (isConfirm) => {") :]
|
||||
confirm = done[done.index("if (isConfirm) {") : done.index("goToWorkflowStage(")]
|
||||
assert "invalidateSectionDetail(shared.projectId);" in confirm
|
||||
|
||||
@@ -84,7 +84,9 @@ def test_되돌림_정본과_바이트_같음(out):
|
||||
|
||||
def test_격자에_구조물_놓고_뺌_같은_자리_정본(out):
|
||||
g = out["gridStructure"]
|
||||
assert g["placed"]["same"] and not g["placed"]["browser"] # 열쇠 바뀜(구조물) · 같은 자리 = 정본
|
||||
assert (
|
||||
g["placed"]["same"] and not g["placed"]["browser"]
|
||||
) # 열쇠 바뀜(구조물) · 같은 자리 = 정본
|
||||
assert g["removed"]["same"] and not g["removed"]["browser"] # 다시 격자 = 정본 표본 · 기하
|
||||
assert g["removedBytes"]
|
||||
|
||||
@@ -112,7 +114,22 @@ def test_가게_길_초안_있음_버린_직후(out):
|
||||
for at in STRAIGHT: # 초안 차례 = 옛 규칙(기하 새 틀 · 표본 앞 값)
|
||||
assert not s["draft"][at]["same"] and not s["draft"][at]["browser"], at
|
||||
dropped = s["dropped"]
|
||||
for at in CURVES: # 버린 뒤 = 저장 노선 자리 표본(초안 3 cm 자리 표본이 안 남음)
|
||||
assert dropped[at]["browser"], at
|
||||
for at in CURVES: # 버린 뒤 = 저장 정본 자리 · 표본(초안 3 cm 자리 표본이 안 남음 · 467 덧 정본 물러섬)
|
||||
assert dropped[at]["same"] and not dropped[at]["browser"], at
|
||||
for at in STRAIGHT: # 초안이 안 옮긴 측점 = 앞 횡단 표본(서버) 그대로
|
||||
assert not dropped[at]["browser"], at
|
||||
|
||||
|
||||
def test_옮겼다_제자리_관_측점_정본(out):
|
||||
p = out["pipeBack"]
|
||||
assert p["pipe"]["same"] and not p["pipe"]["browser"] # 저장 정본 표본 · 기하
|
||||
assert p["bytes"] # 관 밖 횡단 = 정본 바이트(관 설계 = 옮김 규칙 · 다시 셈 입력은 정본 물러섬)
|
||||
|
||||
|
||||
def test_일반_측점_자리_관_지움_다른_목록(out):
|
||||
g = out["pipeGone"]
|
||||
assert g["placedKind"] == "irregular"
|
||||
assert g["same"] is False # 관 카드 → 일반 측점 = 갈아끼움
|
||||
assert g["sameUntouched"] is True # 일반 측점 그대로 = 같은 목록
|
||||
assert g["culvertLeft"] is False and g["kind"] == "regular" # 관 카드 안 남음
|
||||
assert g["canon"] # 그 자리 정본 표본 · 기하
|
||||
|
||||
@@ -71,10 +71,9 @@ def test_저장_기준_입력이_심는_측점(planted):
|
||||
"both"
|
||||
] # 꺼진 종류도
|
||||
assert planted["noDetail"] is True # 상세 없음 = 종단도 기준
|
||||
# 저장 꼭짓점(양 끝) + 심은 측점마다 세운 꼭짓점(427 · seatBornNodes)
|
||||
born = [c for k, c in planted["both"] if k == "irregular" and 0 < c < 60]
|
||||
assert planted["pviAt"] == sorted([0, 60, *born])
|
||||
assert planted["pvi"] == 2 + len(born)
|
||||
# 저장 꼭짓점(양 끝)만 — 짝 없는 새(심은) 측점엔 꼭짓점 안 세움(계획선 그대로 · 467 · 요구 272)
|
||||
assert planted["pviAt"] == [0, 60]
|
||||
assert planted["pvi"] == 2
|
||||
|
||||
|
||||
def test_늦게_심어도_안_돎(r):
|
||||
|
||||
@@ -35,6 +35,7 @@ from B05_Profile.B05_Profile_Engine_Grade_Profile import ( # noqa: E402
|
||||
rebuild_alignment_profile,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Router_Confirm import _merge_irregular_into_longitudinal # noqa: E402
|
||||
from common_util.common_util_route_profile import design_elevation_from_longitudinal # noqa: E402
|
||||
|
||||
REL = "B06_Section/longitudinal/longitudinal.json"
|
||||
PIPE = 351.0
|
||||
@@ -147,15 +148,20 @@ def test_높이_고친_꼭짓점은_높이째_옮김(tmp_path: Path) -> None:
|
||||
assert after[at] == pytest.approx(value, abs=1e-6), at
|
||||
|
||||
|
||||
def test_짝_없는_새_측점도_세우고_옛_꼭짓점은_지움(tmp_path: Path) -> None:
|
||||
"""관 하나 지우고 둘 놓음(수가 다름) — 새 측점 둘 다 원지반 지남 · 옛 편집 지움."""
|
||||
_write(tmp_path, _longitudinal({"351.000": 0.4}))
|
||||
def test_짝_없는_새_측점은_꼭짓점_안_세우고_옛_꼭짓점은_지움(tmp_path: Path) -> None:
|
||||
"""관 하나 지우고 둘 놓음(수가 다름) — 새 측점 둘 = 꼭짓점 안 세움 · 계획고 = 놓기 전 계획선 높이
|
||||
(브라우저가 보인 값 · [저장]으로 안 바뀜 · 467 · 요구 272) · 옛 편집 지움."""
|
||||
before = _longitudinal({"351.000": 0.4})
|
||||
_write(tmp_path, before)
|
||||
after = _move(tmp_path, 125.0, 287.0)
|
||||
rows = _rows(after, 0, 400)
|
||||
assert PIPE not in rows
|
||||
assert after["profile_alignment"]["edits"]["station_offsets"] == {}
|
||||
nodes = [float(n["chainage_m"]) for n in after["profile_alignment"]["base_pvi"]]
|
||||
for at in (125.0, 287.0):
|
||||
assert rows[at] == pytest.approx(_ground(at), abs=1e-4)
|
||||
assert all(abs(at - s) > 0.006 for s in nodes) # 꼭짓점 없음
|
||||
assert rows[at] == pytest.approx(design_elevation_from_longitudinal(before, at), abs=1e-3)
|
||||
assert abs(rows[at] - _ground(at)) > 1e-3 # 원지반으로 안 내려앉음
|
||||
|
||||
|
||||
def _ts(tmp_path: Path, cases: list[dict]) -> list[list[dict]]:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""PLAN 466 덧(470 조사 4.2 N2) — B06 폼 관 고침: 0.51 m 넘게 옮기며 옵션도 고치면 옵션이 옮긴 관에 붙음.
|
||||
|
||||
못박는 것
|
||||
- 옮김 + 옵션 = 관 초안(가게 pipes)의 옮긴 관에 새 옵션 · 캐시 얹기(`applyPipeOptions`)도 옮긴 자리.
|
||||
- 0.51 m 안 옮김 · 옵션만 = 그 관에 새 옵션 · 얹기 = 지금 자리.
|
||||
짝: `helper_466_pipe_move_options.cjs`(진짜 `createB06StructuresPanel` · 가짜 모듈 틀 = 165 헬퍼).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_466_pipe_move_options.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
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(HERE / "helper_466_pipe_move_options.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert run.returncode == 0, run.stderr
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
def test_멀리_옮기며_옵션_고침(got) -> None:
|
||||
assert got["far"]["pipes"] == [[100, 1200]] # 옛: [[100, 800]] — 옵션이 옛 자리로 찾아 안 붙음
|
||||
assert got["far"]["applied"] == [[100, 1200]] # 옛: 80.17(비운 옛 자리 그림)
|
||||
|
||||
|
||||
def test_가까이_옮김_옵션만(got) -> None:
|
||||
assert got["near"]["pipes"] == [[80, 1000]] and got["near"]["applied"] == [[80, 1000]]
|
||||
assert got["stay"]["pipes"] == [[80.17, 900]] and got["stay"]["applied"] == [[80.17, 900]]
|
||||
Reference in New Issue
Block a user