무슨 일이 있었나 랩탑 줄의 병합 `20ba886c`(Merge origin/main_desktop_1·main_laptop_1·sub_desktop_1 into sub_laptop_1)가 우리 파일 22개를 떨구고 35개 파일의 내용을 옛것으로 되돌림. 손으로 지운 커밋은 없고 **병합 자체가 떨군 것**임. 그것이 `origin/dev`·`main_laptop_1`·`sub_laptop_1`· `CODEX` 까지 퍼졌고(데스크탑 둘만 무사), 이 창의 병합 `d92c1f2b` 로 들어옴. 잃었던 것 - 공용 — `common_util_provenance.py` · `ui_template_provenance.ts` - B08 — 근거 사전 · 좌측 패널 상자 모듈 · 토량환산계수 칸 - B09 — 근거 사전 셋 - B05 — 계획노선 편집 모듈 아홉 · 지형 라우터 · B04 지도 모듈 - 시험 셋과, 35개 파일 안의 최근 작업(환산계수 고르기 · 근거 호버 배선 등) 어떻게 되살렸나 `611a2b40`(병합 직전, 전부 온전)에서 `git show <커밋>:<경로>` 로 내용만 꺼내 되돌림. 이력은 안 건드림. ⚠ HEAD 에만 있던 「추가 816줄」은 랩탑의 새 작업이 아니라 **되살아난 옛 코드**였음(B05 편집은 모듈로 쪼개기 전 덩어리 · B08 라우터는 환산계수 고르기 전 옛 상수판). 되돌릴 시점 이후의 **진짜 새 커밋은 둘뿐**이라 그 둘만 패치로 다시 얹음 — `9f827bf6`(리로드 빌드 고리 끊기, 데스크탑 보조) · `b9bca6b3`(B06 조정창 1px, 랩탑). 위키 여덟은 코덱스 몫이라 손대지 않음. 자체검증 — 양쪽 작업이 다 살아 있음을 짚어 확인: `main.py` 의 「개발 서버는 살려 둔다」 · `B05_Profile_Engine_Grade.py` 의 `plan_curve_length_limit_m` · `B08_..._EarthworkGrid.ts` 의 `attachProvenance`. `tsc --noEmit` 통과 · `pytest -q` **1317 passed, 28 skipped** (되살리기 전에는 시험 둘이 수집 단계에서 깨져 있었음). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RANEBHns1S4tkmsYwewtk
197 lines
8.0 KiB
TypeScript
197 lines
8.0 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_Api_Replan.ts
|
|
* 계획노선 두 벌(예상노선·계획노선) 읽기와 노선 갈아 끼우기 요청.
|
|
*
|
|
* GET /projects/{id}/route/plan → 예상노선·계획노선 정점(사업지 좌표계 m)
|
|
* POST /projects/{id}/route/replan → 고친 계획노선으로 갈아 끼우고 재계산
|
|
* POST /projects/{id}/route/replan/reset → 계획노선을 예상노선으로 되돌리고 재계산
|
|
*
|
|
* 재계산은 배수유역부터 전 단계를 다시 도는 무거운 작업이라(용화 67측점 기준 3분대)
|
|
* 타임아웃을 길게 잡는다 — 기본값으로 두면 중간에 끊긴다.
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
|
|
/** 노선 재계산 대기 상한 — 배수유역 분석(90초대)까지 포함해 넉넉히 잡는다. */
|
|
const REPLAN_TIMEOUT_MS = 15 * 60 * 1000;
|
|
|
|
/** 사용자가 잡아 옮기는 제어점 하나 — 서버가 이 노드로 폴리라인을 만든다. */
|
|
export interface RoutePlanNode {
|
|
x: number;
|
|
y: number;
|
|
/** 직전·직후 구간이 이루는 내각(도). 끝점은 null. */
|
|
inner_angle_deg: number | null;
|
|
/** 이 자리에 끼운 원호 반지름(m). 곡선을 지운 자리는 null. */
|
|
radius_m: number | null;
|
|
tangent_m: number | null;
|
|
/** 법정 기준 위반 표시 — 값은 내되 막지 않는다. */
|
|
violations: string[];
|
|
}
|
|
|
|
/** 직선 사이에 놓인 **곡선 성분 하나** — 화면이 손잡이와 R 칸을 그리는 재료. */
|
|
export interface RoutePlanCurve {
|
|
/** 앞뒤 직선을 늘려 만나는 자리(교각점). **반지름을 바꿔도 여기는 안 움직인다.** */
|
|
apex: [number, number];
|
|
radius_m: number;
|
|
tangent_m: number;
|
|
inner_angle_deg: number;
|
|
/** 곡선 시작점 — 직선이 곡선에 닿는 자리. 사용자가 잡는 손잡이다. */
|
|
start: [number, number];
|
|
/** 곡선 끝점. */
|
|
end: [number, number];
|
|
/** 이 곡선이 대신하는 꺾임점 구간(첫·끝) — 편집이 어느 노드를 건드리는지 알려 준다. */
|
|
node_first: number;
|
|
node_last: number;
|
|
violations: string[];
|
|
}
|
|
|
|
export interface RoutePlanResponse {
|
|
status: string;
|
|
project_id: string;
|
|
/** 예상노선(원본) **점 묶음** [[x, y], …] — 사업지 좌표계(m). 폴리라인이 아니다. */
|
|
expected: Array<[number, number]>;
|
|
/** 계획노선 폴리라인(원호 포함) — 그려 보이는 선. 잡는 대상이 아니다. */
|
|
planned: Array<[number, number]>;
|
|
/** 잡아 옮기는 노드(꺾임점). 편집은 이것으로 한다(2026-09-06 사용자 지시). */
|
|
nodes: RoutePlanNode[];
|
|
/** 직선·곡선 성분 — 곡선 시작·끝점과 반지름. 화면이 이것으로 손잡이를 그린다. */
|
|
curves: RoutePlanCurve[];
|
|
/** 이 프로젝트에 적용한 법정 최소곡선반지름(m) — **기본값·위반 표시 기준**. */
|
|
min_radius_m: number;
|
|
/** **못 넘는** 곡선반지름 하한(m). 0이면 제한 없음(작업임도). 기본값과 다른 값이다. */
|
|
limit_radius_m?: number;
|
|
/** **못 넘는** 곡선 길이 하한(m). 0이면 제한 없음 — 지금은 전부 0(법에 값이 없음). */
|
|
limit_curve_length_m?: number;
|
|
curve_count: number;
|
|
violation_count: number;
|
|
/** 사용자가 고친 계획노선이 저장돼 있으면 true. */
|
|
edited: boolean;
|
|
}
|
|
|
|
export interface RouteReplanResponse {
|
|
status: string;
|
|
project_id: string;
|
|
route_id: number | null;
|
|
total_length_m: number | null;
|
|
vertex_count: number;
|
|
}
|
|
|
|
async function requestJson<T>(path: string, init: RequestInit, timeoutMs: number): Promise<T> {
|
|
const controller = new AbortController();
|
|
const timer = window.setTimeout(() => controller.abort(), timeoutMs);
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}${path}`, {
|
|
...init,
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json", ...(init.headers ?? {}) },
|
|
signal: controller.signal,
|
|
});
|
|
const payload = (await response.json()) as T & { message?: string };
|
|
if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`);
|
|
return payload;
|
|
} finally {
|
|
window.clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
/** 예상노선·계획노선을 함께 읽는다(편집 모달이 점선·실선으로 그린다). */
|
|
export async function fetchRoutePlan(projectId: string): Promise<RoutePlanResponse> {
|
|
return requestJson<RoutePlanResponse>(
|
|
`/projects/${projectId}/route/plan`,
|
|
{ method: "GET" },
|
|
60000,
|
|
);
|
|
}
|
|
|
|
/** 꺾임점 하나에 실어 보내는 편집값 — 곡선을 둘지, 반지름을 못박을지. */
|
|
export interface RouteReplanVertex {
|
|
x: number;
|
|
y: number;
|
|
/** 이 자리에 곡선을 둘지. 끄면 직선이 그대로 꺾인다(곡선 삭제). */
|
|
curve?: boolean;
|
|
/** 못박을 반지름(m). 없으면 서버가 고른다. */
|
|
radius_m?: number | null;
|
|
}
|
|
|
|
/** 고친 계획노선으로 갈아 끼우고 배수유역부터 다시 계산한다.
|
|
*
|
|
* 편집 세 가지가 모두 이 한 목록으로 나간다(2026-09-07 사용자 지시) —
|
|
* **직선 삭제·추가**는 점을 빼거나 더하는 것, **곡선 삭제·추가**는 `curve` 를 끄고 켜는 것,
|
|
* **반지름 변경**은 `radius_m` 을 주는 것. */
|
|
export async function replanRoute(
|
|
projectId: string,
|
|
vertices: Array<[number, number]> | RouteReplanVertex[],
|
|
): Promise<RouteReplanResponse> {
|
|
const payload = (vertices as Array<[number, number] | RouteReplanVertex>).map((vertex) =>
|
|
Array.isArray(vertex) ? { x: vertex[0], y: vertex[1] } : vertex,
|
|
);
|
|
return requestJson<RouteReplanResponse>(
|
|
`/projects/${projectId}/route/replan`,
|
|
{ method: "POST", body: JSON.stringify({ vertices: payload }) },
|
|
REPLAN_TIMEOUT_MS,
|
|
);
|
|
}
|
|
|
|
/** 점 묶음의 **지반고**를 묻는다(계획서 0-9 ⑤·⑧).
|
|
*
|
|
* 새 계산이 아니라 확정된 지표면을 **읽기만** 하므로 편집 중에 불러도 된다 — 다만 끄는 동안
|
|
* 프레임마다 부르지는 않는다(찍는 순간에만). 지표면 밖은 `null` 로 온다. */
|
|
export async function fetchRouteElevations(
|
|
projectId: string,
|
|
points: Array<[number, number]>,
|
|
): Promise<Array<number | null>> {
|
|
const payload = await requestJson<{ z: Array<number | null> }>(
|
|
`/projects/${projectId}/route/elevations`,
|
|
{ method: "POST", body: JSON.stringify({ points }) },
|
|
60000,
|
|
);
|
|
return payload.z;
|
|
}
|
|
|
|
/** 횡단 미리보기 한 장 — 고치던 노선 그대로 그 측점만 서버가 셈해 준다(계획서 0-9 ⑧). */
|
|
export interface CrossPreviewResponse {
|
|
status: string;
|
|
chainage_m: number;
|
|
label: string | null;
|
|
uphill_side: string | null;
|
|
plan_radius_m: number | null;
|
|
curve_widening_m: number | null;
|
|
/** 원지반 횡단 샘플. */
|
|
samples: Array<{ offset_m?: number; elevation_m?: number | null; valid: boolean }>;
|
|
/** 기본 계획 횡단 — B06 `compute_cross_design` 이 낸 것. 계획고를 못 세우면 null. */
|
|
design: {
|
|
design_line: Array<{ offset_m: number; elevation_m: number }>;
|
|
cut_area_m2: number;
|
|
fill_area_m2: number;
|
|
[key: string]: unknown;
|
|
} | null;
|
|
}
|
|
|
|
export interface CrossPreviewRequest {
|
|
vertices: Array<{ x: number; y: number; curve: boolean; radius_m: number | null }>;
|
|
chainage_m: number;
|
|
min_radius_m: number;
|
|
station_interval_m: number;
|
|
}
|
|
|
|
/** 한 측점 횡단을 묻는다. 종·횡단을 한 번 돌리므로 **한두 초** 걸린다(사용자 확정: 괜찮음). */
|
|
export async function fetchCrossPreview(
|
|
projectId: string,
|
|
request: CrossPreviewRequest,
|
|
): Promise<CrossPreviewResponse> {
|
|
return requestJson<CrossPreviewResponse>(
|
|
`/projects/${projectId}/route/cross-preview`,
|
|
{ method: "POST", body: JSON.stringify(request) },
|
|
120000,
|
|
);
|
|
}
|
|
|
|
/** 계획노선을 예상노선으로 되돌리고 같은 재계산을 돈다(노선 초기화). */
|
|
export async function resetRoutePlan(projectId: string): Promise<RouteReplanResponse> {
|
|
return requestJson<RouteReplanResponse>(
|
|
`/projects/${projectId}/route/replan/reset`,
|
|
{ method: "POST" },
|
|
REPLAN_TIMEOUT_MS,
|
|
);
|
|
}
|