refactor(b09): 옛 탭 옮기기 ① 설계서 구성 — 새 틀 탭 파일로 · 옛 표 도우미(head·note·표·근거 호버)를 UI_Table 로

- 화면 확인: 설계서 구성 13줄 · 요약·법 문구 그대로

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
2026-09-14 01:59:50 +09:00
co-authored by Claude Opus 5
parent 4c6268ffe8
commit 800cb73be0
4 changed files with 155 additions and 1 deletions
@@ -195,6 +195,11 @@ export function injectSheetStyles(): void {
.b09s-title { font-weight:700; font-size:14px; }
.b09s-formula { white-space:pre-wrap; font-size:12px; color:var(--ui-text, #1f2430); }
.b09s-legacy .b09-tabs { display:none; }
.b09s-head { font-weight:600; margin-top:6px; }
.b09s-info td { text-align:right; font-variant-numeric:tabular-nums; }
.b09s-info td.b09s-left, .b09s-info th.b09s-left { text-align:left; white-space:normal; }
.b09s-group { display:flex; flex-direction:column; gap:4px; border-top:1px solid var(--ui-border, #d0d4dc); padding-top:6px; }
.b09s-inline { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.b09s-table td.b09s-adopted { font-weight:700; color:var(--ui-accent, #2f6fed); background:rgba(47,111,237,0.08); }
.b09s-min { display:inline-block; margin-left:4px; font-size:10px; color:#1e8e3e; border:1px solid #1e8e3e; border-radius:8px; padding:0 4px; }
`;
+2 -1
View File
@@ -23,6 +23,7 @@ import { machineTab } from "./B09_Estimation_UI_Tab_Machine";
import { priceCompareTab } from "./B09_Estimation_UI_Tab_PriceCompare";
import { summaryTab } from "./B09_Estimation_UI_Tab_Summary";
import { listsTab } from "./B09_Estimation_UI_Tab_Lists";
import { designDocTab } from "./B09_Estimation_UI_Tab_DesignDoc";
/** 탭 등록 — 한 줄에 탭 하나. 옛 탭(`legacyTab`)은 새 탭 파일이 서면 그 줄만 바꿈. */
const TABS: B09Tab[] = [
@@ -37,7 +38,7 @@ const TABS: B09Tab[] = [
listsTab,
legacyTab("supply", "B09_Estimation_Tab_Supply"),
legacyTab("base_data", "B09_Estimation_Tab_BaseData"),
legacyTab("design_doc", "B09_Estimation_Tab_DesignDoc"),
designDocTab,
legacyTab("basis_sheet", "B09_Estimation_Tab_BasisSheet"),
];
@@ -0,0 +1,82 @@
/* =============================================================================
* B09_Estimation_UI_Tab_DesignDoc.ts
* B09 설계서 구성 탭 — 법이 정한 목차와 우리가 내는 것을 맞대 봄(별표2 (5)(가))
*
* - 옛 `B09_Estimation_UI_BaseData.ts` 의 설계서 구성표를 그대로 옮김(PLAN 12장 옛 탭 옮기기).
* - ⚠ 「없음」과 「우리 몫 아님」을 갈라 보임 — 갈라야 다음에 할 일이 달라짐.
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
import type { B09Tab } from "./B09_Estimation_UI_Shell_Types";
import { L, hint } from "./B09_Estimation_UI_Sheet";
import { head, infoTable, note } from "./B09_Estimation_UI_Table";
interface DesignDocDto {
law: string;
summary: string;
items: Array<{
order: number;
name: string;
status: string;
owner: string;
where: string;
note: string;
}>;
notes: string[];
}
let cached: DesignDocDto | null = null;
function draw(body: HTMLElement, data: DesignDocDto): void {
body.append(head(`설계서 구성 (법이 정한 ${data.items.length})`));
body.append(note(data.summary));
body.append(note(data.law));
body.append(
infoTable(
["차례", "이 름", "상 태", "누가 만드나", "어디서 나오나", "비 고"],
data.items.map((row) => [
String(row.order),
row.name,
row.status,
row.owner,
row.where || "—",
row.note,
]),
[0, 1, 2, 3, 4, 5],
),
);
for (const line of data.notes) body.append(note(line));
}
export const designDocTab: B09Tab = {
key: "design_doc",
label: () => L("B09_Estimation_Tab_DesignDoc"),
render(ctx) {
if (!ctx.projectId) {
ctx.body.append(hint(L("B09_Sheet_NoProject")));
return;
}
// 프로젝트 값이 아니라 **우리가 무엇을 내는가**의 표 — 한 번 받으면 그대로 씀.
if (cached) {
draw(ctx.body, cached);
return;
}
ctx.body.append(hint(L("B09_Sheet_Loading")));
fetch(
`${API_BASE_URL}/projects/${encodeURIComponent(ctx.projectId)}/estimation/design-doc-index`,
{ credentials: "include" },
)
.then((response) => {
if (!response.ok) throw new Error(String(response.status));
return response.json() as Promise<DesignDocDto>;
})
.then((data) => {
cached = data;
ctx.body.replaceChildren();
draw(ctx.body, data);
})
.catch((error: Error) => {
ctx.body.replaceChildren(hint(`${L("B09_Sheet_LoadFailed")} ${error.message}`, true));
});
},
};
+66
View File
@@ -0,0 +1,66 @@
/* =============================================================================
* B09_Estimation_UI_Table.ts
* 옛 B09 화면에서 옮겨 온 **설명 표** 공용 — 머리글 한 줄 · 제목 · 안내 글 · 근거 호버 (PLAN 12장)
*
* - 옛 `B09_Estimation_UI_BaseData.ts` 의 `head`·`note`·`table` 을 그대로 옮김(옛 파일을 지우려고).
* - 칸마다 근거 호버(`keys`·`sheet`) — 사전은 **개발환경에서만** 오고, 없으면 아무 일도 안 함.
* - 숫자 칸은 오른쪽, `leftCols` 칸은 왼쪽(옛 `.b09-left` 와 같음).
* ========================================================================== */
import {
attachProvenance,
markProvenanceCell,
type ProvenanceSheet,
} from "@ui/ui_template_provenance";
import { el } from "./B09_Estimation_UI_Sheet";
/** 금액 글 → 천 단위 쉼표(옛 `money` 그대로 — 소수부를 자르지 않음). */
export function money(value: string | null | undefined): string {
if (value === null || value === undefined || value === "") return "";
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed.toLocaleString("ko-KR") : value;
}
/** 구역 제목. */
export function head(text: string): HTMLElement {
return el("div", "b09s-hint b09s-head", text);
}
/** 안내 한 줄. */
export function note(text: string): HTMLElement {
return el("div", "b09s-hint", text);
}
/** 설명 표 한 장 — 가로로 넘치면 제 칸 안에서만 밀림. */
export function infoTable(
headers: string[],
rows: string[][],
leftCols: number[],
keys?: string[],
sheet?: ProvenanceSheet,
): HTMLElement {
const wrap = el("div", "b09s-wrap");
const table = el("table", "b09s-table b09s-info");
const thead = el("thead");
const headRow = el("tr");
headers.forEach((text, index) => {
headRow.append(el("th", leftCols.includes(index) ? "b09s-left" : "", text));
});
thead.append(headRow);
const tbody = el("tbody");
for (const cells of rows) {
const tr = el("tr");
cells.forEach((text, index) => {
const td = el("td", leftCols.includes(index) ? "b09s-left" : "", text);
const key = keys?.[index];
const column = key ? sheet?.columns[key] : undefined;
if (key && column) markProvenanceCell(td, key, column.tier);
tr.append(td);
});
tbody.append(tr);
}
table.append(thead, tbody);
attachProvenance(table, sheet);
wrap.append(table);
return wrap;
}