Files
Aislo/B06_wf3_ProfileCross/B06_wf3_ProfileCross_Api_Fetch.ts
T
2026-07-18 23:59:28 +09:00

149 lines
4.5 KiB
TypeScript

/* =============================================================================
* B06_wf3_ProfileCross_Api_Fetch.ts
* 3차 워크플로우(종·횡단 생성) API 클라이언트
*
* 백엔드 계약 (B06_wf3_ProfileCross_Router.py):
* GET /api/projects/{project_id}/sections/context → 확정 경로 + 기본 옵션
* GET /api/projects/{project_id}/sections/{route_id} → 종단 요약 조회
* GET /api/projects/{project_id}/sections/{route_id}/detail → 종횡단 원시 샘플 조회
* POST /api/projects/{project_id}/sections/{route_id}/confirm → 종횡단 확정
*
* 규칙:
* - 모든 제어 상수는 config_frontend에서 참조 (하드코딩 금지).
* - 오류 응답 형식 {status:"error", message:"..."}을 Error로 변환.
* ========================================================================== */
import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend";
export interface SectionOptionDefaults {
station_interval_m: number;
cross_half_width_m: number;
cross_sample_interval_m: number;
long_sample_interval_m: number;
vertical_exaggeration: number;
}
export interface SectionContextResponse {
project_id: string;
route_id: number | null;
filter_key: string | null;
method: string | null;
smooth: boolean | null;
crs_epsg: number | null;
defaults: SectionOptionDefaults;
}
/** 종단 요약 조회 결과 (SectionSummaryResponse) */
export interface SectionSummaryResponse {
status: string;
project_id: string;
route_id: number;
longitudinal: Record<string, unknown> | null;
length_m: number | null;
cross_section_count: number;
}
export interface SectionSample {
chainage_m?: number;
offset_m?: number;
elevation_m?: number | null;
z?: number | null;
valid: boolean;
}
export interface SectionStation {
station_id: string;
chainage_m: number;
label: string;
kind: "bp" | "ep" | "regular";
center_z: number | null;
azimuth_deg: number | null;
center_x: number;
center_y: number;
frame: { left_xy: [number, number] };
}
export interface LongitudinalSection {
length_m: number;
samples: SectionSample[];
stations: SectionStation[];
}
export interface CrossSection extends SectionStation {
samples: SectionSample[];
}
export interface SectionDetailResponse {
longitudinal: LongitudinalSection;
cross_sections: CrossSection[];
}
/** 종횡단 확정 결과 (SectionConfirmResponse) */
export interface SectionConfirmResponse {
status: string;
project_id: string;
route_id: number;
confirmed: boolean;
}
/** 공통 fetch 헬퍼: 타임아웃 + 인증 헤더 + 오류 응답 변환. */
async function requestJson<T>(path: string, init: RequestInit): Promise<T> {
const controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), API_TIMEOUT_MS);
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(timeoutId);
}
}
/** 최신 확정 경로와 B04 확정값, config 기반 생성 기본값을 조회한다. */
export async function fetchSectionContext(projectId: string): Promise<SectionContextResponse> {
return requestJson<SectionContextResponse>(`/projects/${projectId}/sections/context`, {
method: "GET",
});
}
/** 경로의 종단면 요약을 조회한다. */
export async function getSections(
projectId: string,
routeId: number,
): Promise<SectionSummaryResponse> {
return requestJson<SectionSummaryResponse>(`/projects/${projectId}/sections/${routeId}`, {
method: "GET",
});
}
/** 경로의 SVG 렌더링용 종단·횡단 원시 샘플을 조회한다. */
export async function fetchSectionDetail(
projectId: string,
routeId: number,
): Promise<SectionDetailResponse> {
return requestJson<SectionDetailResponse>(`/projects/${projectId}/sections/${routeId}/detail`, {
method: "GET",
});
}
/** 경로의 종·횡단면을 확정한다. */
export async function confirmSections(
projectId: string,
routeId: number,
): Promise<SectionConfirmResponse> {
return requestJson<SectionConfirmResponse>(`/projects/${projectId}/sections/${routeId}/confirm`, {
method: "POST",
});
}