/* ============================================================================= * B09_Estimation_UI_Tab_RateTable.ts * 제비율 요율표 탭 — STmate 「원가계산기준」(wM_KAN_RATE)을 본뜸 (PLAN 12장 · 랩탑 메인). * * - 좌측 = 도구줄 자리: `수정기준선택 ☞` [기본제비율] · `수 정` (지금은 잠김 — 덮어쓰기 칸 전). * - 본문 = 첫 탭 「☞ 공사원가계산 제잡비율」 묶음 차례 · 아래 탭 넷(첫 탭만 켜짐). * - ⚠ 요율은 서버(`/estimation/rate-table`)가 요율 데이터에서 그대로 냄 — 보기 전용. * ========================================================================== */ import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton } from "@ui/ui_template_elements"; import { API_BASE_URL } from "@config/config_frontend"; import type { B09Tab, B09TabContext } from "./B09_Estimation_UI_Shell_Types"; function L(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; } interface RateSection { title: string; formula: string; columns: string[]; rows: (string | number)[][]; } interface RateTableDto { status: string; message?: string; rate_version: { dataset_id: string; effective_date: string; sha256: string }; sections: RateSection[]; } /** DFM 아래 탭 — 화면에 보이는 묶음 제목 그대로. */ const RATE_TABS = [ "☞ 공사원가계산 제잡비율", "표준시장단가 제잡비율", "☞ 행정자치부 적용 제잡비율", "☞ 실적공사비 적용시 제경비율 조정계수", ]; const STYLE_ID = "b09-rate-table-styles"; function injectStyles(): void { if (document.getElementById(STYLE_ID)) return; const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = ` .b09rt { display: flex; flex-direction: column; gap: 8px; height: 100%; min-height: 0; } .b09rt__meta { font-size: 12px; color: var(--color-text-secondary); } .b09rt__scroll { flex: 1; overflow: auto; min-height: 0; display: flex; flex-direction: column; gap: 12px; } .b09rt__section { border: 1px solid var(--color-border); padding: 6px 8px; } .b09rt__title { font-weight: 600; font-size: 13px; } .b09rt__formula { font-size: 12px; color: var(--color-text-secondary); margin-left: 6px; font-weight: normal; } .b09rt__table { border-collapse: collapse; font-size: 12px; margin-top: 4px; } .b09rt__table th, .b09rt__table td { border: 1px solid var(--color-border); padding: 2px 8px; } .b09rt__table td { text-align: right; font-variant-numeric: tabular-nums; } .b09rt__table td:first-child, .b09rt__table td.b09rt__text { text-align: left; } .b09rt__tabs { display: flex; gap: 2px; border-top: 1px solid var(--color-border); padding-top: 4px; } .b09rt__tab { font-size: 12px; padding: 2px 10px; border: 1px solid var(--color-border); border-top: none; background: none; } .b09rt__tab[aria-selected="true"] { font-weight: 600; } .b09rt__panel { display: flex; flex-direction: column; gap: 6px; font-size: 12px; } `; document.head.append(style); } function el( tag: K, className = "", text = "", ): HTMLElementTagNameMap[K] { const node = document.createElement(tag); if (className) node.className = className; if (text) node.textContent = text; return node; } async function fetchRates(projectId: string): Promise { const response = await fetch( `${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/estimation/rate-table`, { credentials: "include" }, ); const body = (await response.json()) as RateTableDto; if (!response.ok) throw new Error(body.message ?? `HTTP ${response.status}`); return body; } function drawPanel(ctx: B09TabContext): void { const box = el("div", "b09rt__panel"); const pick = el("label", "b09rt__panel"); pick.append(el("span", "", "수정기준선택 ☞")); const select = el("select"); for (const [i, name] of ["기본제비율", "표준시장", "행자부기준"].entries()) { const option = el("option", "", i ? `${name} (준비 중)` : name); option.disabled = i > 0; select.append(option); } pick.append(select); const edit = createButton({ label: "수 정", variant: "ghost", disabled: true }); edit.title = "요율 수정은 원가계산이 프로젝트 요율을 받는 칸이 선 뒤에 열림"; box.append( pick, edit, el("span", "b09rt__meta", "요율은 조달청 제비율 판 그대로 — 지금은 보기 전용"), createButton({ label: "원가계산서", variant: "ghost", onClick: () => ctx.open("cost_sheet"), }), ); ctx.panel.append(box); } function drawBody(ctx: B09TabContext, data: RateTableDto): void { const wrap = el("div", "b09rt"); wrap.append( el( "div", "b09rt__meta", `원가계산기준 — 요율 판 ${data.rate_version.effective_date} (${data.rate_version.dataset_id})`, ), ); const scroll = el("div", "b09rt__scroll"); for (const section of data.sections) { const box = el("div", "b09rt__section"); const title = el("div", "b09rt__title", section.title); if (section.formula) title.append(el("span", "b09rt__formula", section.formula)); box.append(title); const table = el("table", "b09rt__table"); const head = el("tr"); for (const column of section.columns) head.append(el("th", "", column)); table.append(head); for (const row of section.rows) { const tr = el("tr"); for (const cell of row) { tr.append(el("td", typeof cell === "number" ? "" : "b09rt__text", String(cell))); } table.append(tr); } box.append(table); scroll.append(box); } wrap.append(scroll); const tabs = el("div", "b09rt__tabs"); RATE_TABS.forEach((name, i) => { const tab = el("button", "b09rt__tab", name); tab.setAttribute("aria-selected", String(i === 0)); tab.disabled = i > 0; if (i > 0) tab.title = "준비 중"; tabs.append(tab); }); wrap.append(tabs); ctx.body.append(wrap); } function render(ctx: B09TabContext): void { injectStyles(); if (!ctx.projectId) { ctx.body.append(el("div", "b09rt__meta", "프로젝트를 고르세요")); return; } drawPanel(ctx); ctx.body.append(el("div", "b09rt__meta", "요율표 받는 중…")); fetchRates(ctx.projectId) .then((data) => { ctx.body.replaceChildren(); drawBody(ctx, data); }) .catch((error: unknown) => { ctx.body.replaceChildren( el( "div", "b09rt__meta", `요율표를 받지 못함 — ${error instanceof Error ? error.message : ""}`, ), ); }); } export const rateTableTab: B09Tab = { key: "rate_table", label: () => L("B09_Estimation_Tab_RateTable"), render, };