Files
Aislo/D03_PlanAnalysis/D03_PlanAnalysis_Api.ts
T
eomsangdonandClaude Opus 5.5 cdcdd5f597 feat(562-3 · 563-1): D03 손 긋기 화면 — 도구 여섯 · 그리기 · 거리재기 · 구역 연도 · 횡단 판 · 예상 코리도 걷음
- UI_Map: 바탕(위성 · 등고선) + 겹(그리기 · 거리재기) · 도구줄 부품(되돌리기 · 다시하기 · 3D · 좌 · 우 회전 · 거리재기) · B05 createMapRotation · 왼쪽 끌기 이동 끔(가운데 단추 · 휠 방향 = 설정)
- 새 UI_Draw: 시점에서 점 찍어 직선 · 미리보기 길이 · 기울기 · 제약 칸(자유 · 길이 고정 · 기울기 고정 · 둘 다 · 20 m · 9 %) · 종점 붙기 · 갈래 · 구역 마크 · Enter · Esc · Backspace
- 새 UI_Measure: 두 점 길이 · 기울기 · 높이 차(지형 heightAt)
- logic/Plan: 선 · 갈래 · 조각 꼴 다루기로 다시 씀(옛 갈래 넷 · 이은 누가 걷음)
- UI_Page · Api: 지형 범위 카카오 지도 미리보기 → [이 범위 받기] · [범위 밖 더 받기] · GET · PUT plan/network · [초기화] · 조각 연도 고름 · 캐시 판 2
- UI_3D: 지형 + 그은 선(선 고름) · 예상 코리도 · 체크 칸 걷음
- 지움: UI_Cross · UI_Edit · logic/Expect · logic/Edit · test_544_1(예상 횡단 · 예상 코리도 셈이 화면에서 걷혀 지킬 대상 없음)
- 시험: 530_3 = 손 긋기 꼴 다루기 + 서버 derive 대조로 다시 씀 · 553_4 D03 부품 단언 새 화면으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 20:16:08 +09:00

136 lines
4.8 KiB
TypeScript

/* =============================================================================
* D03_PlanAnalysis_Api.ts
* D03 분석 화면 서버 요청(PLAN 563-1) — 전체 시점 · 종점(D02 정본) · 지형 받기 범위 · 받기 · 진행 ·
* 사업 전체 지형면 한 벌(`models_plan/` · 격자) · 그은 선 정본(562-2 GET · PUT network).
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
import { fetchCachedBytes } from "../A00_Common/b_asset_cache";
import { listSurfaceModels } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import { type DtmGrid, parseDtmGrid } from "../common_util/common_util_surface_sampler";
import type { PlanLine, PlanPiece, Vertex } from "./logic/D03_PlanAnalysis_Plan";
export interface BasePoint {
lon: number;
lat: number;
x: number;
y: number;
}
/** D02 정본 — 전체 시점 · 종점(작업 좌표계 m + 경위도). */
export interface PlanBaseView {
epsg: number | null;
start: BasePoint | null;
end: BasePoint | null;
}
export interface PlanJob {
running: boolean;
percent: number;
message: string | null;
error: string | null;
}
/** 받기 전 범위(561-2) — 상자(작업 좌표계 m · 경위도 네 귀) · 받은 범위 · 도엽 · 칸 수 어림. */
export interface TerrainExtent {
epsg: number;
margin_m: number;
box_m: [number, number, number, number];
box: Array<[number, number]>;
have_m: [number, number, number, number] | null;
have: Array<[number, number]> | null;
covered: boolean;
sheets: Array<{ no: string; ring: Array<[number, number]>; stored: boolean }>;
cells: number;
}
/** 그은 선 서버 꼴(562-2) — 마크 = 조각 경계 · 조각 없는 구역 = 연도 없음. */
export interface ServerNetwork {
epsg: number;
lines: PlanLine[];
marks: Array<{ line: string; at_m: number }>;
pieces: PlanPiece[];
}
export interface PlanNetworkView {
network: ServerNetwork | null;
derived: {
lines: Array<{ id: string; length_m: number; stations: number[] }>;
pieces: Array<PlanPiece & { length_m: number; planned: Vertex[] }>;
} | null;
/** 옛 연도 노선을 선으로 바꾼 것인가. */
legacy: boolean;
/** 사업연도 시작(있으면). */
year_start?: number | null;
}
export interface PlanSurface {
modelId: number;
grid: DtmGrid;
}
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
const response = await fetch(`${API_BASE_URL}${path}`, {
...init,
credentials: "include",
headers: { "Content-Type": "application/json", ...(init.headers ?? {}) },
});
const body = (await response.json().catch(() => ({}))) as T & { detail?: unknown };
if (!response.ok) {
const detail = typeof body.detail === "string" ? body.detail : `HTTP ${response.status}`;
throw new Error(detail);
}
return body;
}
const base = (projectId: string): string => `/projects/${projectId}/plan`;
export const fetchPlanBase = (projectId: string): Promise<PlanBaseView> =>
request(`${base(projectId)}/points`);
/** 받기 전 범위 — `extra` = 더 덮을 점(그은 꺾임점). */
export const fetchTerrainExtent = (
projectId: string,
marginM: number | null,
extra: Vertex[],
): Promise<TerrainExtent> => {
const query = new URLSearchParams();
if (marginM !== null) query.set("margin_m", String(marginM));
if (extra.length) query.set("extra", JSON.stringify(extra));
return request(`${base(projectId)}/terrain/extent?${query}`);
};
export const startTerrain = (
projectId: string,
marginM: number | null,
extra: Vertex[],
): Promise<PlanJob> =>
request(`${base(projectId)}/terrain`, {
method: "POST",
body: JSON.stringify({ margin_m: marginM, extra }),
});
export const fetchPlanProgress = (projectId: string): Promise<{ terrain: PlanJob }> =>
request(`${base(projectId)}/progress`);
export const fetchPlanNetwork = (projectId: string): Promise<PlanNetworkView> =>
request(`${base(projectId)}/network`);
export const savePlanNetwork = (
projectId: string,
network: ServerNetwork,
): Promise<PlanNetworkView> =>
request(`${base(projectId)}/network`, { method: "PUT", body: JSON.stringify(network) });
/** 사업 전체 지형면 — 모델 행 경로 `…/models_plan/…` · 격자 = 평활본 · 없으면 null. */
export async function fetchPlanSurface(projectId: string): Promise<PlanSurface | null> {
const { models } = await listSurfaceModels(projectId);
const model = models.find((entry) => /models_plan\//.test(entry.model_file_path ?? ""));
if (!model) return null;
const stamp = String(model.generation_params?.file_stamp ?? "");
const url =
`${API_BASE_URL}/projects/${projectId}/surface/models/${model.id}/grid` +
`?smooth=true&sig=${encodeURIComponent(stamp)}`;
return { modelId: model.id, grid: parseDtmGrid(await fetchCachedBytes(projectId, url)) };
}