Files
Aislo/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts
T
eomsangdonandClaude Opus 5 18c174036b feat(B07): 계획평면도(라이다) — 지표면 음영기복 배경 1차 배치
빈 도각이던 라이다 계획평면도에 지표면 탑뷰 그림을 얹음.

- 확정 DTM 격자를 도곽 범위로 잘라 음영기복 PNG 를 만들고 Image 엔티티로 실음
  (북서 315도·고도 45도, 한 변 최대 1,600 px). 점구름 4,900만 점을 그대로
  그리지 않음.
- 어느 지표면을 쓸지는 1단계 확정값을 따름 — DrainageContext 에 surface_params
  를 실어 전달.
- 축척·도곽·장 나눔은 계획평면도와 같음(1/1,200) — 노선이 같은 자리에 섬.
- entities_bbox 가 꼭짓점 배열(points)을 세도록 고침. 세지 않으면 그림이 도곽
  계산에서 통째로 빠짐.

검증(용화_LAS): 콘텐츠 726.1x487.2 mm ≤ A1 작도영역, 그림 범위 안에 노선이
완전히 들어감, 음영기복 준비 0.4초·자료 214 KB. 능선·계곡이 눈으로 구분됨.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 19:00:38 +09:00

214 lines
5.6 KiB
TypeScript

/* B07 상세 설계 도면 목록·단건 API 클라이언트. */
import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend";
export interface DesignDrawingItem {
id: string;
// blank: 아직 내용을 만들지 않은 도면 — 도각만 실려 온다.
kind:
| "cover"
| "longitudinal"
| "cross"
| "mass_haul"
| "watershed"
| "plan"
| "landuse"
| "plan_lidar"
| "blank";
label: string;
chainage_m: number | null;
confirmed: boolean;
}
export interface CadDrawing {
entities: Record<string, unknown>[];
/** 횡단도 측점별 실좌표(m) → 종이(mm) 변환값. 구조물을 같은 자리에 얹는 데 쓴다. */
cross_placements?: {
chainage_m: number;
ox: number;
oy: number;
dy: number;
mm_per_m: number;
x0: number;
x1: number;
}[];
layers: {
id: string;
name: string;
isVisible: boolean;
isLocked: boolean;
}[];
}
export interface DesignDrawingListResponse {
status: string;
project_id: string;
route_id: number;
drawings: DesignDrawingItem[];
}
/** 수량 산출표 값 (미산정 항목은 null). 백엔드 `_quantity_table`의 키와 대응. */
export type QuantityTable = Record<string, number | null>;
/** 측구 형식별 규격 (B06 엔진 ditch_spec 신구조와 1:1). */
export type DitchSpec =
| { type: "none" }
| {
type: "standard";
top_width_m: number;
bottom_width_m: number;
depth_m: number;
}
| { type: "l_type"; width_m: number; depth_m: number };
/** B06에서 지정한 설계(지반정보·계획정보). 횡단도에만 존재. status로 잠정/확정 구분. */
export interface CrossDesignInfo {
ground_type: "soil" | "ripping_rock" | "blasting_rock";
geometry_preset: "soil" | "rock";
section_mode: "left_cut" | "right_cut" | "both_cut" | "both_fill";
ditch_side: "left" | "right";
ditch_type?: "standard" | "l_type" | null;
ditch_enabled?: boolean;
cut_slope_ratio: number;
fill_slope_ratio: number;
roadbed_width_m: number;
carriageway_width_m?: number;
cross_slope_pct?: number;
paved?: boolean;
ditch: DitchSpec;
road_edges?: Record<
"left" | "right",
{ offset_m: number; elevation_m: number }
>;
design_elevation_m: number;
cut_area_m2: number;
fill_area_m2: number;
status?: "provisional" | "confirmed";
}
export interface DesignDrawingResponse {
status: string;
project_id: string;
route_id: number;
id: string;
// blank: 아직 내용을 만들지 않은 도면 — 도각만 실려 온다.
kind:
| "cover"
| "longitudinal"
| "cross"
| "mass_haul"
| "watershed"
| "plan"
| "landuse"
| "plan_lidar"
| "blank";
label: string;
drawing: CadDrawing;
confirmed: boolean;
quantity_table?: QuantityTable | null;
design?: CrossDesignInfo | null;
}
export interface DesignDrawingConfirmResponse {
status: string;
project_id: string;
id: string;
confirmed: boolean;
all_confirmed: boolean;
design?: CrossDesignInfo | null;
}
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" },
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);
}
}
export function fetchDesignDrawingList(
projectId: string,
): Promise<DesignDrawingListResponse> {
return requestJson(`/projects/${projectId}/design-drawings`);
}
export function fetchDesignDrawing(
projectId: string,
drawingId: string,
): Promise<DesignDrawingResponse> {
return requestJson(
`/projects/${projectId}/design-drawings/${encodeURIComponent(drawingId)}`,
);
}
export function confirmDesignDrawing(
projectId: string,
drawingId: string,
drawing: CadDrawing,
quantityTable?: QuantityTable | null,
): Promise<DesignDrawingConfirmResponse> {
return requestJson(
`/projects/${projectId}/design-drawings/${encodeURIComponent(drawingId)}/confirm`,
{
method: "PUT",
body: JSON.stringify({ drawing, quantity_table: quantityTable ?? null }),
},
);
}
export function invalidateDesignDrawing(
projectId: string,
drawingId: string,
): Promise<void> {
return requestJson(
`/projects/${projectId}/design-drawings/${encodeURIComponent(drawingId)}/invalidate`,
{ method: "POST" },
);
}
/** 도각 편집 화면이 싣는 도각 한 장 (실치수 1:1, 잠금 없는 도면층). */
export interface FrameTemplateResponse {
status: string;
project_id: string;
drawing: CadDrawing;
/** 회사가 고친 도각을 쓰고 있으면 true, 프로그램 기본 도각이면 false. */
customized: boolean;
}
export function fetchFrameTemplate(
projectId: string,
): Promise<FrameTemplateResponse> {
return requestJson(`/projects/${projectId}/frame-template`);
}
export function saveFrameTemplate(
projectId: string,
drawing: CadDrawing,
): Promise<void> {
return requestJson(`/projects/${projectId}/frame-template`, {
method: "PUT",
body: JSON.stringify({ drawing }),
});
}
/** 회사 도각을 지우고 프로그램 기본 도각으로 되돌린다. */
export function resetFrameTemplate(projectId: string): Promise<void> {
return requestJson(`/projects/${projectId}/frame-template`, {
method: "DELETE",
});
}