/* ============================================================================= * 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 { clearState, readState, writeState } from "../A00_Common/b_page_state"; 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 기반 생성 기본값을 조회한다. */ /** 종횡단 설정값 — ④ 계산 결과라 세션에 담아 두고 화면을 오갈 때마다 다시 묻지 않는다 * (2026-09-06 호출 정리). 노선이 바뀌면 `clearSectionContextCache` 로 버린다. */ export async function fetchSectionContext(projectId: string): Promise { const cached = readState("section-context", projectId); if (cached) return seedStandardCross(projectId, cached); const fresh = await requestJson( `/projects/${projectId}/sections/context`, { method: "GET" }, ); writeState("section-context", fresh, projectId); return seedStandardCross(projectId, fresh); } /** * 저장된 표준 횡단면을 **세션이 비어 있을 때만** 채운다(2026-09-07). * * 브라우저 횡단 계산은 `세션 ?? config 기본값` 으로 서는데, 표준단면 세션값은 그 탭에서만 * 산다. 그래서 **탭을 새로 열면** 화면은 config 기본값으로, 서버는 저장분으로 계산해 * 같은 측점이 갈렸다(실측 — 용화 route 169 저장분은 암반 횡단경사 **5%**·측구 상단폭 * **0.9m**, config 기본값은 **3%**·**0.69m**). * * 여기가 두 화면(B05·B06)이 함께 지나는 유일한 자리라 이 한 곳에서 채운다. 사용자가 * 그 탭에서 고친 값이 있으면 **건드리지 않는다** — 초안이 언제나 우선이다. */ function seedStandardCross( projectId: string, context: SectionContextResponse, ): SectionContextResponse { const stored = context.stored_standard_cross_section; if (stored && readState("std-cross", projectId) === null) { writeState("std-cross", stored, projectId); } return context; } export function clearSectionContextCache(projectId: string): void { clearState("section-context", projectId); } /** 경로의 종단면 요약을 조회한다. */ 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; /** 측점별 소단 제원(chainage 키 → 폭·간격·기울기). 값이 없는 측점은 소단 없음. */ berms?: Record; /** 측점별 암 절토 경사비(chainage 키 → 1:n 의 n). 0 은 「표준값을 씀」이다. */ cutSlopeRatios?: 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, berms: options?.berms ?? null, cut_slope_ratios: options?.cutSlopeRatios ?? null, }), }, ); }