feat(798-1): M01 요율 조달청 제비율 내장 엑셀 — 빌더 · 읽기 전용 화면 · RT 줄 → 칸 이동
- build_제비율책.py: 조달청 xlsx → 내장 엑셀 책 JSON(병합 · 채움 · 글꼴 · 테두리 · 열 폭 · 행 높이 · 시트 둘 · 수식 결과) · --check(판 바뀜 · RT 칸 = 정본 값) - 제비율엑셀/조달청_제비율_책.json · /api/m01/rate-sheet · 요율 화면 위 엑셀 + 아래 RT 표 · 확대 · 축소 - 정본 요율_조달청제비율.json · B09 요율 셈 그대로 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
/* =============================================================================
|
||||
* 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<HTMLElement>(".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<HTMLElement>("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();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user