Files
Aislo/B04_PreProcess/B04_PreProcess_Api_Fetch.ts
T
eomsangdonandClaude Fable 5 f7528a4aa4 refactor(B04): B04_wf1_Surface -> B04_PreProcess 전면 개명
- 폴더·내부 파일 51개 접두사 개명 (git mv, 이력 보존)
- 저장소 전체 참조 치환 67파일: import 경로, 라우트 슬러그(b04-preprocess),
  라우트 키(B04_PREPROCESS), storage 경로 상수, locale, SQL 주석
- 로직 변경 없음 (기계적 치환). typecheck·백엔드 import 검증 통과

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 10:01:36 +09:00

534 lines
19 KiB
TypeScript

/* =============================================================================
* B04_PreProcess_Api_Fetch.ts
* 1차 워크플로우(지표면 분석) API 클라이언트
*
* 백엔드 계약 (B04_PreProcess_Router.py):
* POST /api/projects/{project_id}/surface/analyze → 분석 실행 + DB 기록
* GET /api/projects/{project_id}/surface/models → 모델 목록 조회
*
* 규칙:
* - 모든 제어 상수는 config_frontend에서 참조 (하드코딩 금지).
* - 오류 응답 형식 {status:"error", message:"..."}을 Error로 변환.
* ========================================================================== */
import { API_ANALYSIS_TIMEOUT_MS, API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend";
/** 지표면 분석 실행 요청 (SurfaceAnalyzeRequest) */
export interface SurfaceAnalyzeRequest {
input_file_id: number;
source_filters?: string[];
methods?: string[];
force?: boolean;
}
/** 지표면 분석 실행 결과 (SurfaceAnalyzeResponse) */
export interface SurfaceAnalyzeResponse {
status: string;
project_id: string;
ground_summary: Record<string, unknown>;
manifest_status: string;
surface_model_ids: number[];
}
export interface SurfaceConfirmResponse {
status: string;
project_id: string;
model_id: number;
confirmed: boolean;
}
export interface SurfaceConfirmOptions {
smooth: boolean;
contour_interval_m: number;
}
/** 저장된 지표면 모델 요약 (SurfaceModelSummary) */
export interface SurfaceModelSummary {
id: number;
model_type: string;
status: string;
resolution_m: number | null;
model_file_path: string | null;
generation_params: Record<string, unknown> | null;
created_at: string | null;
}
export interface SurfaceInputFileSummary {
id: number;
file_type: string;
original_filename: string;
raw_file_path: string;
file_size_mb: number | null;
crs_epsg: number | null;
status: string | null;
created_at: string | null;
}
export interface SurfaceInputFileListResponse {
status: string;
project_id: string;
files: SurfaceInputFileSummary[];
}
export interface SurfaceBounds {
x_min: number;
x_max: number;
y_min: number;
y_max: number;
z_min: number;
z_max: number;
}
export interface SurfacePointCloudSampleResponse {
status: string;
project_id: string;
point_count: number;
sampled_count: number;
bounds: SurfaceBounds;
points: [number, number, number][];
rgb?: [number, number, number][];
}
export interface SurfaceGroundStatsResponse {
status: string;
project_id: string;
filters: Record<string, Record<string, unknown>>;
}
export interface SurfaceStatusResponse {
project_id: string;
status: "pending" | "in_progress" | "completed" | "failed";
model_count: number;
progress_percent: number;
current_stage: string;
message: string;
}
/** 지표면 모델 목록 응답 (SurfaceModelListResponse) */
export interface SurfaceModelListResponse {
status: string;
project_id: string;
models: SurfaceModelSummary[];
}
/** 확정 지표면 요약 (SurfaceConfirmedResponse).
* 포인트 배열 없이 확정 구성과 지형 가장자리만 담는다 — 진입 판정·준비화면·B05 공용. */
export interface SurfaceConfirmedResponse {
status: string;
project_id: string;
model_id: number | null;
source_filter: string | null;
method: string | null;
smooth: boolean | null;
contour_interval_m: number | null;
/** 확정 구성이 바뀌었는지 한 줄로 비교하기 위한 값. */
signature: string;
point_count: number | null;
bounds: {
x_min: number;
x_max: number;
y_min: number;
y_max: number;
z_min: number;
z_max: number;
} | null;
/** 계획노선(B03 CSV)의 평면 범위. 지도 초기 화면을 도로 중심으로 맞출 때 쓴다. */
route_bounds: { x_min: number; x_max: number; y_min: number; y_max: number } | null;
}
/** 공통 fetch 헬퍼: 타임아웃 + 인증 헤더 + 오류 응답 변환.
*
* `timeoutMs`를 주면 그 값으로 끊는다. 배수유역 격자 해석처럼 수십 초가 걸리는 요청은
* `API_ANALYSIS_TIMEOUT_MS`를 넘긴다 — 기본값으로 두면 계산 도중 abort 된다. */
async function requestJson<T>(
path: string,
init: RequestInit,
timeoutMs: number = API_TIMEOUT_MS,
): Promise<T> {
const controller = new AbortController();
const timeoutId = 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;
} catch (error) {
// AbortError 원문("signal is aborted without reason")은 원인을 알 수 없으니 바꿔 준다.
if (error instanceof DOMException && error.name === "AbortError") {
throw new Error(`요청이 ${Math.round(timeoutMs / 1000)}초 안에 끝나지 않았습니다.`);
}
throw error;
} finally {
window.clearTimeout(timeoutId);
}
}
/** 지표면 분석을 실행한다 (LAS 구조화 → 지면 필터 → 지표면 모델 생성). */
export async function analyzeSurface(
projectId: string,
request: SurfaceAnalyzeRequest,
): Promise<SurfaceAnalyzeResponse> {
return requestJson<SurfaceAnalyzeResponse>(`/projects/${projectId}/surface/analyze`, {
method: "POST",
body: JSON.stringify(request),
});
}
/** 프로젝트의 지표면 모델 목록을 조회한다. */
export async function listSurfaceModels(projectId: string): Promise<SurfaceModelListResponse> {
return requestJson<SurfaceModelListResponse>(`/projects/${projectId}/surface/models`, {
method: "GET",
});
}
/** 선택한 지표면 모델을 확정하고 WF1 단계를 완료한다. */
export async function confirmSurfaceModel(
projectId: string,
modelId: number,
options: SurfaceConfirmOptions,
): Promise<SurfaceConfirmResponse> {
return requestJson<SurfaceConfirmResponse>(`/projects/${projectId}/surface/confirm`, {
method: "POST",
body: JSON.stringify({ model_id: modelId, ...options }),
});
}
export async function listSurfaceInputFiles(
projectId: string,
): Promise<SurfaceInputFileListResponse> {
return requestJson<SurfaceInputFileListResponse>(`/projects/${projectId}/surface/input-files`, {
method: "GET",
});
}
export async function fetchSurfacePointCloud(
projectId: string,
filter?: string,
): Promise<SurfacePointCloudSampleResponse> {
const query = filter ? `?filter=${encodeURIComponent(filter)}` : "";
return requestJson<SurfacePointCloudSampleResponse>(
`/projects/${projectId}/surface/point-cloud${query}`,
{
method: "GET",
},
);
}
/** 확정 지표면 구성 + 지형 가장자리만 조회한다(수 KB).
* 포인트클라우드 전체(수십 MB)를 받지 않고도 3D 좌표 환산에 필요한 값을 얻는다. */
export async function fetchConfirmedSurface(projectId: string): Promise<SurfaceConfirmedResponse> {
return requestJson<SurfaceConfirmedResponse>(`/projects/${projectId}/surface/confirmed`, {
method: "GET",
});
}
export async function fetchSurfaceGroundStats(
projectId: string,
): Promise<SurfaceGroundStatsResponse> {
return requestJson<SurfaceGroundStatsResponse>(`/projects/${projectId}/surface/ground-stats`, {
method: "GET",
});
}
export async function fetchSurfaceStatus(projectId: string): Promise<SurfaceStatusResponse> {
return requestJson<SurfaceStatusResponse>(`/projects/${projectId}/surface/status`, {
method: "GET",
});
}
export interface VWorldMeta {
x_min: number;
x_max: number;
y_min: number;
y_max: number;
width_meters: number;
height_meters: number;
center_x: number;
center_y: number;
lon_min: number;
lon_max: number;
lat_min: number;
lat_max: number;
}
export function getVWorldMapUrl(projectId: string, layerName: string): string {
return `${API_BASE_URL}/projects/${projectId}/vworld-map?layer_name=${layerName}`;
}
export async function fetchVWorldMeta(projectId: string, layerName: string): Promise<VWorldMeta> {
return requestJson<VWorldMeta>(`/projects/${projectId}/vworld-meta?layer_name=${layerName}`, {
method: "GET",
});
}
export async function fetchGisGeoJson(projectId: string, layer: string): Promise<any> {
return requestJson<any>(`/projects/${projectId}/geojson?layer=${encodeURIComponent(layer)}`, {
method: "GET",
});
}
/** 계획노선(B03 업로드 CSV)의 평면 점 목록. 사업지 좌표계(m) — 배경 지도 메타와 같은 좌표계다. */
export interface PlannedRouteResponse {
status: string;
points: Array<{ x: number; y: number }>;
}
/** 2D 지도에 계획선을 겹쳐 그리기 위한 점 목록을 받는다. 없으면 빈 목록이 온다. */
export async function fetchPlannedRoute(projectId: string): Promise<PlannedRouteResponse> {
return requestJson<PlannedRouteResponse>(`/projects/${projectId}/planned-route`, {
method: "GET",
});
}
/* ── 배수유역 분석 (B04_PreProcess_Router_Watershed.py) ────────────────────
* 관리자 확인용. 계획 노선(B03 CSV) + 도엽 등고선·세류선으로 유역을 끝까지 분석하고
* 결과를 영구저장소에 남긴다. 30초 안팎이 걸리므로 여기서 한 번만 돌린다.
* ------------------------------------------------------------------------ */
/** 관 매설 지점 1개. reason: stream=세류 교차, spacing=간격 보충, confirmed=사용자 확정. */
export interface WatershedPipe {
chainage_m: number;
x: number;
y: number;
lon: number;
lat: number;
reason: string;
stream_name: string | null;
}
/** 1차 배수유역 근거(단계 검증용). TIN·흐름 계산 없이 세류 상·하류 판정과 격자 범위만 준다. */
export interface WatershedAnalysis {
status: string;
project_id: string;
/** 분석에 쓴 계획 노선 파일명(B03 업로드). */
route_source: string;
radius_m: number;
/** 도로와 만난 세류선의 상류측 = 1차 영역의 기준선. */
upstream_lines: Array<Array<[number, number]>>;
/** 교차했으나 하류로 판정해 제외한 조각. 판정이 맞는지 눈으로 대조하는 용도. */
downstream_lines: Array<Array<[number, number]>>;
/** 상·하류 어느 망에도 이어지지 않아 제외한 세류 조각 수. */
no_contact_count: number;
/** 노선이 1차 영역 밖으로 나간 길이(m). 크면 반경을 올려야 한다는 신호. */
road_outside_m: number;
/** 1차 영역(상류 세류망 버퍼 합집합)의 외곽 링 목록. */
region_rings: Array<Array<[number, number]>>;
grid: {
cell_m: number;
rows: number;
cols: number;
/** bbox 전체 셀 수(참고값). */
bbox_cells: number;
/** 1차 영역에 걸쳐 실제로 생성된 셀 수. */
cells: number;
width_m: number;
height_m: number;
/** 격자 bbox 링. 화면은 이 사각형을 rows×cols로 나눠 셀 좌표를 얻는다. */
bbox_lonlat: Array<[number, number]>;
/** 실제 생성된 셀 구간 [행, 시작열, 끝열(포함)]. 낱개 셀 대신 구간으로 온다. */
row_spans: Array<[number, number, number]>;
};
/** 최외곽 적색 셀 주변 확장 결과. */
expansion: {
rounds: number;
/** 새로 추가한 셀에 적색이 없어 스스로 멈췄는가. */
closed: boolean;
added_cells: number;
/** 확장 전(1차 영역) 셀 수. */
initial_cells: number;
};
/** 셀별 흐름 방향과 도로 도달 여부. 등고선이 없어 판정을 못하면 null. */
flow: {
encoding: "base64-uint8";
/** 방위 분해능(32). 코드 0 = 화면 오른쪽, 시계방향 증가. */
azimuth_steps: number;
/** 제자리(더 낮은 이웃 없음)를 뜻하는 코드. */
sink_code: number;
/** 표고가 없어 판정 못한 셀 코드. */
invalid_code: number;
cells: number;
reaches_road: number;
no_road: number;
/** 격자에는 있으나 등고선 TIN 밖이라 표고가 없어 판정 못한 셀. */
unanalyzed: number;
/** 확정된 상류 세류망을 따라 흐름을 강제로 새긴 셀 수. */
burned: number;
outer_seeds: number;
interior_seeds: number;
/** 셀당 1바이트. 하위 6비트=32방위 코드(32=제자리, 33=무효), 0x80=도로 도달.
* 순서는 grid.row_spans를 행 → 구간 → 열 오름차순으로 훑은 순서와 같다. */
data: string;
} | null;
/** 2차 전체 배수유역 외곽선(= 분수령). 적색 셀 전체의 외곽. */
basin_polygon_lonlat: Array<[number, number]>;
basin_area_m2: number;
/** 도로 위 흐름 강도 — [누가거리 m, 그 구간으로 모이는 상류 면적 ㎡].
* 1m 간격 **구간 합**이다(평균·리샘플이 아니라 계산 원본 그대로). */
strength_profile: Array<[number, number]>;
/** 유입 집중점 — [누가거리 m, 유입면적 ㎡, 구역 번호, 구역 내 순위].
* 구역은 시점·기본 관·종점으로 자른 구간이며, 구역마다 `floor(길이/관 최대간격)+1`개를 뽑는다. */
inflow_hotspots: Array<[number, number, number, number]>;
/** 기본 관 매설 위치 — 도로 × 세류선 교차점. */
pipes: WatershedPipe[];
/** B05용 평균 흐름 화살표 — [lon, lat, 방위(도), 도로도달, 셀 수].
* 세류·도로 셀을 뺀 10m 블록 평균이라 사면 경향만 남는다. */
flow_arrows: Array<[number, number, number, boolean, number]>;
/** 화살표 사이 실제 간격(m). 화면이 화살표를 이보다 짧게 그려 서로 닿지 않게 한다. */
arrow_spacing_m: number;
/** 계산하지 않고 저장분을 그대로 돌려준 응답인지. */
from_cache: boolean;
/** 영구저장소에 남긴 검증용 GeoJSON 경로. */
saved_to: string | null;
}
/** 배수유역 분석 결과를 받는다.
*
* `refresh`를 주지 않으면 영구저장소에 남은 결과를 그대로 받아 즉시 끝난다.
* `refresh=true`면 처음부터 다시 계산하므로 수십 초가 걸린다. */
export async function fetchWatershedAnalysis(
projectId: string,
refresh = false,
): Promise<WatershedAnalysis> {
return requestJson<WatershedAnalysis>(
`/projects/${projectId}/drainage/primary-region?refresh=${refresh}`,
{ method: "GET" },
refresh ? API_ANALYSIS_TIMEOUT_MS : API_TIMEOUT_MS,
);
}
/** 도로 한 지점으로 들어오는 셀들의 외곽선(검토용). 계산이 아니라 저장된 귀속 배열 조회다. */
export interface RoadInflowResponse {
status: string;
chainage_m: number;
/** 기여 셀을 모은 도로 구간 길이(m). */
span_m: number;
cell_count: number;
area_m2: number;
/** 가장 먼 셀이 이 지점까지 흘러온 물길 길이(m). */
max_path_length_m: number;
/** 기여 셀 덩어리들의 바깥 링(WGS84). 큰 조각부터. */
rings_lonlat: Array<Array<[number, number]>>;
}
/** 취소는 지원하지 않는다(`requestJson`이 자체 타임아웃 신호를 쓴다) — 호출측에서 늦게 온
* 응답을 버리는 방식으로 처리한다. */
export async function fetchRoadInflow(
projectId: string,
chainageM: number,
): Promise<RoadInflowResponse> {
return requestJson<RoadInflowResponse>(
`/projects/${projectId}/drainage/road-inflow?chainage_m=${chainageM}`,
{ method: "GET" },
);
}
/* ── 상세 배수유역(관 매설 지점 + 세부유역 분할) ─────────────────────────── */
/** 관이 그 자리에 있는 이유. 백엔드 `common_util_drainage_pipes`가 정의처다. */
export type PipeSource = "stream" | "spacing" | "user";
export interface DetailPipePoint {
chainage_m: number;
lonlat: [number, number];
source: PipeSource;
}
export interface DetailBasin {
index: number;
chainage_m: number;
outlet_lonlat: [number, number];
polygon_lonlat: Array<[number, number]>;
area_m2: number;
relief_m: number;
flow_length_m: number;
/** 배수 유효직경(합리식 산출, mm). 강우량표가 아직 없으면 null → "미정" 표기. */
pipe_diameter_mm: number | null;
/** 산출 근거 — 홍수도달시간(분), 설계강우강도(mm/hr), 설계유량(m³/s, 2.0배 반영). */
tc_minutes?: number | null;
intensity_mm_hr?: number | null;
design_flow_m3s?: number | null;
/** 유효직경이 관 최대 규격 초과 — 세월교·물넘이·교량 검토 대상(임도설치규정 제12조). */
bridge_required?: boolean;
}
export interface DetailBasinResponse {
status: string;
project_id: string;
/** 종단 Z 출처(design_profile / route_points / surface / csv). */
z_source: string;
route_length_m: number;
max_spacing_m: number;
min_spacing_m: number;
/** 응답의 관 목록이 저장분에서 온 것인지. */
saved: boolean;
pipe_points: DetailPipePoint[];
basins: DetailBasin[];
pipe_count: number;
/** 도로 1m 구간별 유입 면적 — [누가거리, 면적]. 계획선 색칠에 쓴다. */
strength_profile: Array<[number, number]>;
/** 유입 집중점 — [누가거리, 유입면적, 구역번호, 구역 내 순위]. */
inflow_hotspots: Array<[number, number, number, number]>;
/** B04가 분석에 쓴 계획 노선 선형(lon/lat). */
route_lonlat: Array<[number, number]>;
/** 2차 전체 배수유역 외곽선 = 분수령. 해석 결과 그대로. */
main_polygon_lonlat: Array<[number, number]>;
/** B04 해석 격자 한 변(m). */
grid_cell_m: number;
/** 평균 흐름 화살표 — [x, y(사업지 CRS m), 방위(도), 도로도달, 셀 수]. */
flow_arrows: Array<[number, number, number, boolean, number]>;
/** 화살표 사이 실제 간격(m). */
arrow_spacing_m: number;
/** 유역 안쪽 상류 세류망 — 하이라이트 토글용. */
upstream_lonlat: Array<Array<[number, number]>>;
}
/** 저장된 관 매설 지점과 그 세부유역. 저장분이 없으면 백엔드가 자동 생성해 돌려준다. */
export async function fetchDetailPipePoints(projectId: string): Promise<DetailBasinResponse> {
return requestJson<DetailBasinResponse>(`/projects/${projectId}/drainage/pipe-points`, {
method: "GET",
});
}
/** 편집 중인 관 목록으로 세부유역을 다시 나눈다(저장하지 않는다).
*
* `points`를 비우면 저장분을 무시하고 기본 관 + 자동 보충으로 되돌린다. */
export async function computeDetailBasins(
projectId: string,
points: Array<{ chainage_m: number; source: PipeSource }>,
): Promise<DetailBasinResponse> {
return requestJson<DetailBasinResponse>(`/projects/${projectId}/drainage/detail-basins`, {
method: "POST",
body: JSON.stringify({ points }),
});
}
/** 저장된 관 지점을 버리고 기본 관 + 자동 보충 배치로 되돌린다("초기화").
*
* 화면만 되돌리면 다시 들어왔을 때 옛 관이 살아나므로 저장분까지 지운다. */
export async function resetDetailPipePoints(projectId: string): Promise<DetailBasinResponse> {
return requestJson<DetailBasinResponse>(`/projects/${projectId}/drainage/pipe-points`, {
method: "DELETE",
});
}
/** 관 매설 지점을 정본으로 확정하고 세부유역 산출물까지 남긴다(모델 확정 시점). */
export async function saveDetailPipePoints(
projectId: string,
points: Array<{ chainage_m: number; source: PipeSource }>,
): Promise<DetailBasinResponse> {
return requestJson<DetailBasinResponse>(`/projects/${projectId}/drainage/pipe-points`, {
method: "PUT",
body: JSON.stringify({ points }),
});
}