/* ============================================================================= * B09_Draft_Print_Quantity.ts * 인쇄 미리보기(PLAN 302-3 · 초안) 「수량산출」 영역 — B08 탭 표를 화면 없이 PrintSheet 로 모음. * * 차례 = B08 탭 차례 — 구조물 집계표 → 자재 셋(총괄 · 콘크리트소요 · 자재수량) → 토공 넷(토공집계표 · * 토적표 · 평균운반거리 · 종방향 무대운반거리) → 수리집수면적표(배수지점 측점마다 한 장) → 거리이정표 * 값 = B08 화면과 같은 길(서버 GET + 화면 캐시 · [저장] 전 값이면 화면에 보이는 그 값) · * 표 짜기 = B08 화면이 쓰는 같은 함수(`B08_Quantity_*_Table(s)`) * 수리 = 화면 계산서(두 카드)를 구분 · 항목 · 값 표로 · 계산 = 서버(화면과 같은 POST) * 거리이정표 = 표 + 경로 지도 그림(사용자 그림 우선 · 없으면 서버 캡처 PNG · data: 주소) * 영역 하나가 못 읽히면 그 자리에 까닭 한 장(나머지는 그대로) * 구조물 조합 도면은 여기 없음(도면 영역 · 302-2) * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import type { DataTableOptions } from "@ui/ui_template_data_table"; import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale"; import { stationLabel } from "../../common_util/common_util_station"; import { EARTHWORK_SHEETS, earthworkSheetOptions, earthworkSummaryOptions, HAUL_SHEETS, haulOptions, loadEarthwork, } from "../../B08_Quantity/B08_Quantity_Earthwork_Tables"; import { hydroCalc, hydroInputs, hydroTable, loadHydro, } from "../../B08_Quantity/B08_Quantity_Hydro_Table"; import { loadMaterials, materialOptions, materialTable, type MaterialSheetId, } from "../../B08_Quantity/B08_Quantity_Material_Tables"; import { milestoneTable } from "../../B08_Quantity/B08_Quantity_Milestone_Table"; import { loadSummary, summaryTable } from "../../B08_Quantity/B08_Quantity_Summary_Table"; import { liveMilestone, loadMilestone, routeImages, siteInfo, } from "../../B08_Quantity/B08_Quantity_UI_Milestone_State"; import type { CollectSheets, PrintSheet } from "./B09_Draft_Print_Types"; function L(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; } /** 자재 보조 탭 차례(B08 화면 `SHEETS`) */ const MATERIAL_SHEETS: Array<[MaterialSheetId, keyof typeof ui_locales]> = [ ["material", "B08_Quantity_Tab_MaterialTotal"], ["concrete", "B08_Quantity_Tab_ConcreteMaterial"], ["quantity", "B08_Quantity_Tab_MaterialQuantity"], ]; /** 표 한 장 — 열이 많으면 가로 쪽 */ const sheet = (id: string, name: string, table: DataTableOptions): PrintSheet => ({ id, name, table, orientation: table.columns.length > 8 ? "landscape" : "portrait", }); /** 까닭 한 장 — 못 읽은 영역 · 빈 표 자리 */ const noteSheet = (id: string, name: string, text: string): PrintSheet => sheet(id, name, { columns: [{ key: "note", label: "알림", width: 480 }], rows: [{ note: text }], }); /** 영역 하나 — 실패하면 까닭 한 장 */ const area = (id: string, name: string, make: () => Promise): Promise => make().catch((error: unknown) => [ noteSheet( id, name, `불러오지 못했습니다 — ${error instanceof Error ? error.message : String(error)}`, ), ]); async function summarySheets(projectId: string): Promise { const name = L("B08_Quantity_Tab_StructureSummary"); const view = await loadSummary(projectId); if (!view.stations.length) return [noteSheet("summary", name, L("B08_Quantity_Summary_NoStation"))]; return [sheet("summary", name, summaryTable(view).options)]; } async function materialSheets(projectId: string): Promise { const { payload, overrides, calc } = await loadMaterials(projectId); const sheets = calc(payload, overrides); return MATERIAL_SHEETS.map(([id, label]) => sheet(`material:${id}`, L(label), materialOptions(materialTable(id, sheets, overrides))), ); } async function earthworkSheets(projectId: string): Promise { const { table, interval } = await loadEarthwork(projectId); return EARTHWORK_SHEETS.map(({ id, label }) => { const key = `earthwork:${id}`; if (id === "summary") return sheet(key, label, earthworkSummaryOptions(table)); if (id === "sheet") return table.rows.length ? sheet(key, label, earthworkSheetOptions(table, interval)) : noteSheet(key, label, "측점 단면적이 아직 없습니다 — 횡단 설계를 먼저 마치세요."); const { groups, caption } = HAUL_SHEETS[id](table); const { options, averages } = haulOptions(groups, caption); // 화면 표 아래 평균운반거리 줄 = 쪽엔 본문 끝 줄(번호 칸 글) return sheet(key, label, { ...options, rows: [...options.rows, ...averages.map((no) => ({ no }))], }); }); } async function hydroSheets(projectId: string): Promise { const head = L("B08_Quantity_Tab_Hydrology"); const { sheet: payload, interval, drafts } = await loadHydro(projectId); if (!payload.stations.length) return [ noteSheet( "hydro", head, "저장된 배수지점(관 지점)이 없습니다 — 배수유역 · 종단에서 관을 저장하세요.", ), ]; return Promise.all( payload.stations.map(async (station) => { const inputs = hydroInputs(payload, station, drafts); const result = await hydroCalc(projectId, inputs).catch(() => null); const label = stationLabel(station.chainage_m, interval); return sheet( `hydro:${station.chainage_m.toFixed(3)}`, `${head} ${label}${station.kind ? ` ${station.kind}` : ""}`, hydroTable(label, station.kind, inputs, result, payload), ); }), ); } /** 그림 → data: 주소(서버 PDF 가 쿠키 없이 그림) */ async function dataUrl(url: string): Promise { const response = await fetch(url, { credentials: "include" }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const blob = await response.blob(); const bytes = new Uint8Array(await blob.arrayBuffer()); let text = ""; for (let i = 0; i < bytes.length; i += 0x8000) text += String.fromCharCode(...bytes.subarray(i, i + 0x8000)); return `data:${blob.type || "image/png"};base64,${btoa(text)}`; } async function milestoneSheets(projectId: string): Promise { const name = L("B08_Quantity_Tab_Milestone"); await loadMilestone(projectId, true); const data = liveMilestone(projectId); const { options } = milestoneTable(data, siteInfo()?.mid_default_m ?? null); const images = routeImages(); const pictures = await Promise.all( data.routes .filter((route) => images[route.id]) .map(async (route, i) => ({ src: await dataUrl( `${API_BASE_URL}/projects/${projectId}/quantity/milestone/image?route=${encodeURIComponent(route.id)}`, ).catch(() => ""), caption: route.name.trim() || route.rows[0]?.item || `거래처 ${i + 1}`, })), ); return [{ ...sheet("milestone", name, options), images: pictures.filter((p) => p.src) }]; } export const collectQuantitySheets: CollectSheets = async (projectId) => { const parts = await Promise.all([ area("summary", L("B08_Quantity_Tab_StructureSummary"), () => summarySheets(projectId)), area("material", L("B08_Quantity_Tab_Materials"), () => materialSheets(projectId)), area("earthwork", L("B08_Quantity_Tab_Earthwork"), () => earthworkSheets(projectId)), area("hydro", L("B08_Quantity_Tab_Hydrology"), () => hydroSheets(projectId)), area("milestone", L("B08_Quantity_Tab_Milestone"), () => milestoneSheets(projectId)), ]); return parts.flat(); };