/* ============================================================================= * M01_MasterData_UI_RateSheet.ts * 요율 화면 — 위 = 조달청 제비율 엑셀(내장 엑셀 · 읽기만 · 확대 · 축소) / 아래 = 기존 RT 표(Rows). * RT 줄을 누르면 엑셀의 대응 칸으로 가서 칠함. 요율 셈 로직(B09)은 이 책을 읽지 않음 — 표현만. * 책 = 서버 `/api/m01/rate-sheet`(빌더 `build_제비율책.py` 가 만든 파일) · 798-1 * ⚠ 내장 엑셀은 `import()` 로만 불러옴(요율 화면을 열 때만 받음). * ========================================================================== */ import { createButton, el, showToast } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { wheelZoomsIn } from "../A00_Common/touch_bridge"; import { fetchRateSheet, type RateSheetData } from "./M01_MasterData_Api_Fetch"; import { renderRows } from "./M01_MasterData_UI_Rows"; import type { Pick } from "./M01_MasterData_UI_Side"; import "./M01_MasterData_UI_RateSheet.css"; interface Handle { select(range: string, sheet?: string): boolean; repaint(): void; destroy(): void; } /** 요율 파일이면 엑셀 + 표 · 돌려받은 함수 = 이 화면을 걷을 때 부를 해제. */ export function renderRateRows(host: HTMLElement, pick: Pick, q: string): () => void { const sheetHost = el("div", { className: "m01-rate__sheet" }); const rowsHost = el("div", { className: "m01-rate__rows" }); const zoomIn = createButton({ label: L("M01_RateZoomIn"), variant: "ghost", size: "sm" }); const zoomOut = createButton({ label: L("M01_RateZoomOut"), variant: "ghost", size: "sm" }); const bar = el("div", { className: "ui-toolbar m01-rate__bar", children: [ el("span", { className: "m01-rate__title", text: L("M01_RateSheetTitle") }), zoomOut, zoomIn, ], }); host.replaceChildren(bar, sheetHost, rowsHost); const offRows = renderRows(rowsHost, pick, q); let closed = false; let book: RateSheetData | null = null; let sheet: Handle | null = null; let mark: { sheet: string; r0: number; c0: number; r1: number; c1: number } | null = null; let parse: ((a1: string) => { r0: number; c0: number; r1: number; c1: number } | null) | null = null; const zoom = (dir: 1 | -1): void => { const grid = sheetHost.querySelector(".ss-grid-host"); if (!grid) return; const probe = new WheelEvent("wheel", { ctrlKey: true, deltaY: -120 }); const sign = wheelZoomsIn(probe) ? -1 : 1; // ☰ [설정] 휠 방향을 따라감 grid.dispatchEvent( new WheelEvent("wheel", { ctrlKey: true, deltaY: sign * dir * 120, bubbles: true, cancelable: true, }), ); }; zoomIn.addEventListener("click", () => zoom(1)); zoomOut.addEventListener("click", () => zoom(-1)); /** RT 줄 눌림 → 대응 칸으로 가서 칠함 */ rowsHost.addEventListener("click", (ev) => { const key = (ev.target as HTMLElement).closest("tr[data-key]")?.dataset.key; if (!key || !book || !sheet || !parse) return; const spot = book.대응[key]; const area = spot ? parse(spot.범위) : null; if (!spot || !area) return showToast(L("M01_RateNoCell"), "info"); mark = { sheet: spot.시트, ...area }; sheet.select(spot.칸, spot.시트); sheet.repaint(); }); void (async () => { try { const [data, sheetMod, addr] = await Promise.all([ fetchRateSheet(), import("../A00_Common/spreadsheet/spreadsheet"), import("../A00_Common/spreadsheet/spreadsheet_address"), ]); if (closed) return; book = data; parse = addr.parseRange; sheet = sheetMod.createSpreadsheet(sheetHost, data.책, { readOnly: true, 칠: (id, r, c) => mark && mark.sheet === id && r >= mark.r0 && r <= mark.r1 && c >= mark.c0 && c <= mark.c1 ? "danger mine" : null, }); } catch (error) { if (!closed) showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error"); } })(); return () => { closed = true; offRows(); sheet?.destroy(); }; }