Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
186 lines
7.8 KiB
TypeScript
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();
|
|
};
|