Files
Aislo/B09_Estimation/B09_Estimation_UI_Tab_RateTable.ts
T
eomsangdonandClaude Opus 5 cb33e81d98 feat(b09): 원가계산서(일반형식)·제비율 요율표 탭 파일 — STmate 배치 · 값은 우리 엔진
- 서버: /estimation/cost-sheet(GET·PUT) · /estimation/rate-table — 새 라우터 파일
- 직접비 = 설계내역서 합계 · 기준 입력 = estimation.cost_sheet · 폐기물처리비 = 임목폐기물 톤 × 수동 단가
- 서식 줄: 나.순공사원가 … 바.총공사비 (DFM TWM_KANJUB 차례·표기) · 경비 세목 번호 차례
- 기준 입력판: 형식(일반만) · 2.공사의종류 · 3.공사기간 · 6.관/사급 · 7.산업안전 · 8.환경보전 · 9.건설기계 — 선택지는 요율 데이터에서
- 요율표: 첫 탭 묶음 10개(보기 전용 · 수정은 잠김)
- 탭 계약 파일은 서브 314c0040 과 같은 내용(blob 동일) · 사전은 새 벌 b4
- 시험: 서식 차례·엔진 값 일치·폐기물 자리·저장값 거름·구간 표기

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
2026-09-14 01:32:30 +09:00

183 lines
6.7 KiB
TypeScript

/* =============================================================================
* 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<K extends keyof HTMLElementTagNameMap>(
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<RateTableDto> {
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: "outlined", disabled: true });
edit.title = "요율 수정은 원가계산이 프로젝트 요율을 받는 칸이 선 뒤에 열림";
box.append(
pick,
edit,
el("span", "b09rt__meta", "요율은 조달청 제비율 판 그대로 — 지금은 보기 전용"),
createButton({
label: "원가계산서",
variant: "outlined",
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,
};