/* ============================================================================= * M01_MasterData_UI_RateSheet.ts * 요율 화면 — 오른쪽 위 「시트 | 리스트」 보기 단추 · 한 번에 하나만 보임(기본 시트 · 고른 보기는 세션에 기억). * 시트 = 조달청 제비율 엑셀(내장 엑셀 · 읽기만 · 확대 · 축소) / 리스트 = 기존 RT 표(Rows). * 오른쪽 클릭 — 리스트 줄 「시트에서 보기」 → 대응 칸으로 가서 칠함 / 시트 칸 「리스트에서 보기」 → 그 RT 줄 강조. * 대응이 없으면 메뉴 줄이 흐림(대응 요율 없음). 요율 셈 로직(B09)은 이 책을 읽지 않음 — 표현만. * 책 = 서버 `/api/m01/rate-sheet`(빌더 `build_제비율책.py` 가 만든 파일) · 798-1 · 805-1 * ⚠ 내장 엑셀은 `import()` 로만 불러옴(시트 보기를 처음 열 때만 받음). * ========================================================================== */ import { createButton, el, showToast } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { createMapContextMenu } from "@ui/ui_template_context_menu"; import { createSegment } from "@ui/ui_template_segment"; import { wheelZoomsIn } from "../A00_Common/touch_bridge"; import { fetchRateSheet, type RateSheetData } from "./M01_MasterData_Api_Fetch"; import { renderRows } from "./M01_MasterData_UI_Rows"; import { keyAt, type ParseArea, type RateArea } from "./M01_MasterData_UI_RateSheet_Map"; 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; } type View = "sheet" | "list"; const VIEW_KEY = "aislo_m01_rate_view"; const savedView = (): View => (sessionStorage.getItem(VIEW_KEY) === "list" ? "list" : "sheet"); /** 요율 파일이면 시트 · 리스트 보기 · 돌려받은 함수 = 이 화면을 걷을 때 부를 해제. */ 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 menu = createMapContextMenu("m01-rate"); const zoomIn = createButton({ label: L("M01_RateZoomIn"), variant: "ghost", size: "sm" }); const zoomOut = createButton({ label: L("M01_RateZoomOut"), variant: "ghost", size: "sm" }); const zoomBox = el("span", { className: "m01-rate__zoom", children: [zoomOut, zoomIn] }); const seg = createSegment({ options: [ { value: "sheet", label: L("M01_RateViewSheet") }, { value: "list", label: L("M01_RateViewList") }, ], value: savedView(), ariaLabel: L("M01_RateViewLabel"), onChange: (v) => setView(v as View), }); const bar = el("div", { className: "ui-toolbar m01-rate__bar", children: [ el("span", { className: "m01-rate__title", text: L("M01_RateSheetTitle") }), zoomBox, seg.root, ], }); const root = el("div", { className: "m01-rate", children: [bar, sheetHost, rowsHost, menu.element], }); host.replaceChildren(root); let closed = false; let view: View = savedView(); let offRows: (() => void) | null = null; let book: RateSheetData | null = null; let parse: ParseArea | null = null; let sheet: Handle | null = null; let mark: { sheet: string; r0: number; c0: number; r1: number; c1: number } | null = null; const fail = (error: unknown): void => { if (!closed) showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error"); }; /** 책(대응표) + 칸 주소 읽기 — 한 번만 */ let bookP: Promise | null = null; const loadBook = (): Promise => (bookP ??= (async () => { const [data, addr] = await Promise.all([ fetchRateSheet(), import("../A00_Common/spreadsheet/spreadsheet_address"), ]); book = data; parse = addr.parseRange; })()); /** 내장 엑셀 만들기 — 시트 보기를 처음 열 때 한 번 */ let sheetP: Promise | null = null; const loadSheet = (): Promise => (sheetP ??= (async () => { const [, sheetMod] = await Promise.all([ loadBook(), import("../A00_Common/spreadsheet/spreadsheet"), ]); if (closed || !book) return; sheet = sheetMod.createSpreadsheet(sheetHost, book.책, { readOnly: true, 칠: (id, r, c) => mark && mark.sheet === id && r >= mark.r0 && r <= mark.r1 && c >= mark.c0 && c <= mark.c1 ? "danger mine" : null, cellMenu: (id, r, c) => { const key = book && parse ? keyAt(book.대응, parse, book.책.시트.find((s) => s.id === id)?.병합, id, r, c) : null; return key ? [[L("M01_RateGoList"), () => setView("list", key)]] : [[`${L("M01_RateGoList")} (${L("M01_RateNoRate")})`, () => {}, { disabled: true }]]; }, }); })()); /** 보기 바꿈 · 리스트는 보일 때마다 새로 그림(`focusKey` 줄을 찾아 강조) · 시트는 만들어 둔 것을 다시 보임 */ function setView(next: View, focusKey?: string): void { if (next === view && !focusKey && (next === "sheet" ? sheet : offRows)) return; view = next; sessionStorage.setItem(VIEW_KEY, next); seg.set(next); sheetHost.hidden = next !== "sheet"; rowsHost.hidden = next !== "list"; zoomBox.hidden = next !== "sheet"; offRows?.(); offRows = null; if (next === "list") offRows = renderRows(rowsHost, pick, q, focusKey); else void loadSheet().then(() => sheet?.repaint(), fail); } 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 줄 → 시트 보기로 바꾸고 대응 칸으로 가서 칠함 */ const showInSheet = async (key: string): Promise => { await loadSheet(); const spot = book?.대응[key]; const area: RateArea | null = spot && parse ? parse(spot.범위) : null; if (!spot || !area || !sheet) return; mark = { sheet: spot.시트, ...area }; setView("sheet"); requestAnimationFrame(() => { sheet?.select(spot.칸, spot.시트); sheet?.repaint(); }); }; /** 리스트 줄 오른쪽 클릭 */ rowsHost.addEventListener("contextmenu", (ev) => { const key = (ev.target as HTMLElement).closest("tr[data-key]")?.dataset.key; if (!key) return; ev.preventDefault(); const [x, y] = [ev.clientX, ev.clientY]; void loadBook().then(() => { if (closed) return; const box = root.getBoundingClientRect(); menu.open( x - box.left, y - box.top, book?.대응[key] ? [[L("M01_RateGoSheet"), () => void showInSheet(key).catch(fail)]] : [ [ `${L("M01_RateGoSheet")} (${L("M01_RateNoCellShort")})`, () => {}, { disabled: true }, ], ], ); }, fail); }); setView(view); return () => { closed = true; menu.close(); offRows?.(); sheet?.destroy(); }; }