Files
Aislo/B09_Estimation/draft/B09_Draft_Print_Quantity.ts
T

186 lines
7.8 KiB
TypeScript

/* =============================================================================
* 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<PrintSheet[]>): Promise<PrintSheet[]> =>
make().catch((error: unknown) => [
noteSheet(
id,
name,
`불러오지 못했습니다 — ${error instanceof Error ? error.message : String(error)}`,
),
]);
async function summarySheets(projectId: string): Promise<PrintSheet[]> {
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<PrintSheet[]> {
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<PrintSheet[]> {
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<PrintSheet[]> {
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<string> {
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<PrintSheet[]> {
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();
};