실무 탭이 「위 치수조서 + 아래 수량산출서」인데 1단계에서 아래 표가 섰고 이번이 위 그림.
그림이 붙으면서 한 면에 넉 장을 쌓을 수 없게 돼 **장 나눔(㉢)도 같이** 했음.
장 나눔 — 계획평면도가 쓰는 본 그대로
`standard_sheet` / `standard_sheet_2` … 제원 조합 하나가 한 장.
좌측 목록은 `idPrefix` 묶음(계획평면도와 같은 짜임)이고 kind 는 새로 `standard`.
구조물이 없어도 한 장은 남김 — 단추가 사라지면 「없어진 것」처럼 보이고 사유를 읽을
자리도 없어짐.
그림 — `B07_DesignDetail_Engine_Cad_StandardFigure.py`
⚠ **기울기를 값으로 받음. 0.3 을 그림에 박지 않았음.** `face_slope_ratio` 가 오면 그
값으로 기울고, 비어 있으면 교본 7-3 기본 0.3 으로 그리되 **「자동 판정 전」이라고 그림에
적음**. 자동 판정이 붙는 대로 그림이 따라감.
⚠ 치수는 수량이 쓰는 그 상수(`STONE_MASONRY`)를 직접 읽음 — 여기서 다시 적으면 그림과
표가 갈림. 상부 0.45+0.10H · 하부 0.45+0.40H · 터파기 폭 평균두께+0.2 · 되메우기 0.2.
ⓘ 그 상수는 **뒷면이 수직인 1:0.3 벽**을 뜻함(하부−상부 = 0.30H = 전면이 기우는 양).
기울기가 0.3 이 아니면 그 차이만큼 뒷면이 기욺 — 상수에서 따라 나오는 결과라 그대로 그림.
⚠ 치수가 없는 것은 그리되 치수를 안 적음 — 막자갈은 「입적 − 몸통 − 고임돌」이라 폭·높이로
정의된 도형이 아님. 자리만 보이고 값은 표를 가리킴.
죽은 길 걷어냄 — `BLANK_DRAWINGS` 가 「표준도」 하나뿐이었는데 그것이 내용 있는 장이 되어
빈 도각 경로가 통째로 안 쓰이게 됨. 상수·목록 루프·분기·import 를 지움(700줄 제한도 걸렸음).
자체검증 — 회귀 573 통과 · 0 실패, `tsc --noEmit` 0.
⚠ 병합 뒤 깨진 시험 둘을 고침(내 폴더 `tmp/tests`, 랩탑 메인 `11394d40` 의 여파)
· `stone_supply='채집'` 기본값을 확정 ② 근거와 함께 허용 목록에 등록
· 바닥막이 「모든 옵션이 b05」 단정 → 조달이 detail 로 들어와 B05 셋만 보게 좁힘
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
246 lines
7.4 KiB
TypeScript
246 lines
7.4 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"
|
|
| "cross_standard"
|
|
| "standard"
|
|
| "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"
|
|
| "cross_standard"
|
|
| "standard"
|
|
| "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;
|
|
/** 자리표에 보여 줄 실제 값 — 편집 화면 전용이고 저장값은 토큰 그대로다. */
|
|
fields?: Record<string, string>;
|
|
}
|
|
|
|
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 }),
|
|
});
|
|
}
|
|
|
|
/** 외부 도각 파일(DXF·DWG)을 읽어 편집 화면에 실을 도면으로 받는다 — 아직 저장하지 않는다. */
|
|
export async function importFrameTemplate(
|
|
projectId: string,
|
|
file: File,
|
|
): Promise<{ drawing: CadDrawing; entity_count: number }> {
|
|
const form = new FormData();
|
|
form.append("file", file);
|
|
// 파일 전송이라 requestJson(JSON 헤더·짧은 시한)을 쓰지 않는다.
|
|
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/frame-template/import`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
body: form,
|
|
});
|
|
const payload = (await response.json()) as {
|
|
drawing: CadDrawing;
|
|
entity_count: number;
|
|
message?: string;
|
|
};
|
|
if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`);
|
|
return payload;
|
|
}
|
|
|
|
/** 지금 도면을 DXF·DWG 파일로 받는다 (2026-09-06 사용자 지시) — 파일은 서버가 만든다. */
|
|
export async function exportDrawing(
|
|
projectId: string,
|
|
drawing: CadDrawing,
|
|
fileFormat: "dxf" | "dwg",
|
|
name: string,
|
|
): Promise<{ blob: Blob; skipped: number }> {
|
|
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/drawing-export`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ drawing, file_format: fileFormat, name }),
|
|
});
|
|
if (!response.ok) {
|
|
const detail = (await response.json().catch(() => ({}))) as { message?: string };
|
|
throw new Error(detail.message ?? `HTTP ${response.status}`);
|
|
}
|
|
return {
|
|
blob: await response.blob(),
|
|
skipped: Number(response.headers.get("X-Aislo-Skipped") ?? 0),
|
|
};
|
|
}
|
|
|
|
/** 회사 도각을 지우고 프로그램 기본 도각으로 되돌린다. */
|
|
export function resetFrameTemplate(projectId: string): Promise<void> {
|
|
return requestJson(`/projects/${projectId}/frame-template`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|