/* ============================================================================= * B06_Section_Api_Fetch.ts * 3차 워크플로우(종·횡단 생성) API 클라이언트 * * 백엔드 계약 (B06_Section_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}/regenerate → 횡단 반폭 재생성 * POST /api/projects/{project_id}/sections/{route_id}/confirm → 종횡단 확정 * * 규칙: * - 모든 제어 상수는 config_frontend에서 참조 (하드코딩 금지). * - 오류 응답 형식 {status:"error", message:"..."}을 Error로 변환. * - **타입 선언은 `B06_Section_Api_Types.ts`에 있다**(700줄 제한, 2026-09-02). * 기존 호출 코드가 깨지지 않도록 여기서 그대로 다시 내보낸다. * ========================================================================== */ import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend"; import type { CrossDesign, CrossDesignRequest, CrossDesignResponse, CrossSectionPatch, SectionConfirmResponse, SectionContextResponse, SectionDetailResponse, SectionSummaryResponse, StandardCrossSection, } from "./B06_Section_Api_Types"; // 타입 정본은 `B06_Section_Api_Types.ts`. 기존 호출부가 여기서 가져오던 것을 // 그대로 쓰도록 다시 내보낸다 — 이동은 파일만 나눈 것이고 계약은 그대로다. export type * from "./B06_Section_Api_Types"; /** 공통 fetch 헬퍼: 타임아웃 + 인증 헤더 + 오류 응답 변환. */ async function requestJson(path: string, init: RequestInit): Promise { 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 { return requestJson(`/projects/${projectId}/sections/context`, { method: "GET", }); } /** 경로의 종단면 요약을 조회한다. */ export async function getSections( projectId: string, routeId: number, ): Promise { return requestJson(`/projects/${projectId}/sections/${routeId}`, { method: "GET", }); } /** 경로의 SVG 렌더링용 종단·횡단 원시 샘플을 조회한다. */ export async function fetchSectionDetail( projectId: string, routeId: number, ): Promise { return requestJson(`/projects/${projectId}/sections/${routeId}/detail`, { method: "GET", }); } /** 횡단 반폭을 반영해 종횡단을 재생성·저장하고 갱신된 상세를 반환한다. */ export async function regenerateSections( projectId: string, routeId: number, crossHalfWidthM: number, ): Promise { return requestJson( `/projects/${projectId}/sections/${routeId}/regenerate`, { method: "POST", body: JSON.stringify({ cross_half_width_m: crossHalfWidthM }) }, ); } /** 측점 표준횡단 설계(지반유형·단면유형)를 즉시 계산·저장하고 잠정 결과를 반환한다. */ export async function computeCrossDesign( projectId: string, routeId: number, request: CrossDesignRequest, ): Promise { return requestJson( `/projects/${projectId}/sections/${routeId}/cross-design`, { method: "POST", body: JSON.stringify(request) }, ); } /** 경로의 종·횡단면을 확정한다. 표준 횡단면 설정값·측점별 세션 보관값을 함께 저장할 수 있다. */ export async function confirmSections( projectId: string, routeId: number, standardCrossSection?: StandardCrossSection, crossPatches?: CrossSectionPatch[], /** 프론트에서 계산한 유토곡선 결과. 확정 시점에만 영구 저장한다. */ massHaul?: Record, ): Promise { const body: Record = {}; if (standardCrossSection) body.standard_cross_section = standardCrossSection; if (crossPatches?.length) body.cross_patches = crossPatches; if (massHaul) body.mass_haul = massHaul; return requestJson(`/projects/${projectId}/sections/${routeId}/confirm`, { method: "POST", body: JSON.stringify(body), }); } /** * 편집 중인 종·횡단을 **확정하지 않고** 영구저장소에만 남긴다(임시 저장). * 저장 내용은 확정과 같지만 경로 상태·워크플로 단계를 건드리지 않아 페이지 이동도 없다. */ export async function saveSections( projectId: string, routeId: number, standardCrossSection?: StandardCrossSection, crossPatches?: CrossSectionPatch[], massHaul?: Record, ): Promise { const body: Record = {}; if (standardCrossSection) body.standard_cross_section = standardCrossSection; if (crossPatches?.length) body.cross_patches = crossPatches; if (massHaul) body.mass_haul = massHaul; return requestJson(`/projects/${projectId}/sections/${routeId}/save`, { method: "POST", body: JSON.stringify(body), }); } /** 같은 회사에서 표준횡단 설계값을 불러올 수 있는 프로젝트 항목. */ export interface CompanyStandardProject { project_id: string; name: string; } export interface CompanyStandardListResponse { status: string; projects: CompanyStandardProject[]; } export interface CompanyStandardResponse { status: string; project_id: string; standard_cross_section: StandardCrossSection; } /** 같은 회사에서 설계값을 불러올 수 있는 프로젝트 목록을 조회한다(회사 스코프). */ export async function listCompanyStandards( projectId: string, ): Promise { return requestJson( `/projects/${projectId}/sections/company-standards`, { method: "GET" }, ); } /** 특정 프로젝트의 표준횡단 설계값을 미리보기용으로 조회한다(적용 전, 현재 값 불변). */ export async function getCompanyStandard( projectId: string, sourceProjectId: string, ): Promise { return requestJson( `/projects/${projectId}/sections/company-standards/${sourceProjectId}`, { method: "GET" }, ); } /** 계획선 편집 델타(자동 선형 대비 계획고 델타 + 종단곡선 반경). B05 편집 스토어와 같은 모양. */ export interface ProfileAlignmentEdits { station_offsets: Record; curve_radii: Record; } /** * 계획선 편집 프리뷰 결과 — **바뀌는 것만** 온다. * 횡단 지반선 원시 샘플은 계획고와 무관해 그대로이므로 싣지 않는다(응답 1MB → 수십 KB). */ export interface CrossDesignPreviewResponse { status: string; /** 유토곡선이 읽는 설계 필드만 담긴 부분 갱신값(설계선 좌표 등은 오지 않는다). */ designs: Array<{ chainage_m: number; design: Partial }>; } /** * 계획선 편집을 반영해 **계획선 + 전 측점 횡단 설계**를 다시 계산해 받는다(서버 저장 없음). * * B05에서 계획고를 끄는 동안 횡단 단면적이 함께 움직여야 횡단 기준 유토곡선이 따라온다. * 측점마다 따로 부르면 수십 번 왕복하므로 한 번에 계산한다. 영속화는 각 페이지의 * 임시저장·확정이 맡는다. */ export async function previewCrossDesigns( projectId: string, routeId: number, edits: ProfileAlignmentEdits, standardCrossSection?: StandardCrossSection, options?: { /** true면 설계 전체(설계선 좌표 포함)를 받는다 — B06 진입 시 stale 일괄 재계산용. */ fullDesigns?: boolean; /** 측점별 암 경계 오프셋 세션값(chainage 키 → m). DB 저장분보다 우선한다. */ rockBoundaryOffsets?: Record; }, ): Promise { return requestJson( `/projects/${projectId}/sections/${routeId}/cross-design/preview`, { method: "POST", body: JSON.stringify({ ...edits, standard_cross_section: standardCrossSection ?? null, full_designs: options?.fullDesigns ?? false, rock_boundary_offsets: options?.rockBoundaryOffsets ?? null, }), }, ); }