From 800cb73be0124bdf06327d91fb390f7119888270 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 01:59:50 +0900 Subject: [PATCH] =?UTF-8?q?refactor(b09):=20=EC=98=9B=20=ED=83=AD=20?= =?UTF-8?q?=EC=98=AE=EA=B8=B0=EA=B8=B0=20=E2=91=A0=20=EC=84=A4=EA=B3=84?= =?UTF-8?q?=EC=84=9C=20=EA=B5=AC=EC=84=B1=20=E2=80=94=20=EC=83=88=20?= =?UTF-8?q?=ED=8B=80=20=ED=83=AD=20=ED=8C=8C=EC=9D=BC=EB=A1=9C=20=C2=B7=20?= =?UTF-8?q?=EC=98=9B=20=ED=91=9C=20=EB=8F=84=EC=9A=B0=EB=AF=B8(head=C2=B7n?= =?UTF-8?q?ote=C2=B7=ED=91=9C=C2=B7=EA=B7=BC=EA=B1=B0=20=ED=98=B8=EB=B2=84?= =?UTF-8?q?)=EB=A5=BC=20UI=5FTable=20=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 화면 확인: 설계서 구성 13줄 · 요약·법 문구 그대로 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- B09_Estimation/B09_Estimation_UI_Sheet.ts | 5 ++ B09_Estimation/B09_Estimation_UI_Shell.ts | 3 +- .../B09_Estimation_UI_Tab_DesignDoc.ts | 82 +++++++++++++++++++ B09_Estimation/B09_Estimation_UI_Table.ts | 66 +++++++++++++++ 4 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 B09_Estimation/B09_Estimation_UI_Tab_DesignDoc.ts create mode 100644 B09_Estimation/B09_Estimation_UI_Table.ts diff --git a/B09_Estimation/B09_Estimation_UI_Sheet.ts b/B09_Estimation/B09_Estimation_UI_Sheet.ts index 76c88b56..89231aa9 100644 --- a/B09_Estimation/B09_Estimation_UI_Sheet.ts +++ b/B09_Estimation/B09_Estimation_UI_Sheet.ts @@ -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; } `; diff --git a/B09_Estimation/B09_Estimation_UI_Shell.ts b/B09_Estimation/B09_Estimation_UI_Shell.ts index 8cacd05a..41858b0f 100644 --- a/B09_Estimation/B09_Estimation_UI_Shell.ts +++ b/B09_Estimation/B09_Estimation_UI_Shell.ts @@ -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"), ]; diff --git a/B09_Estimation/B09_Estimation_UI_Tab_DesignDoc.ts b/B09_Estimation/B09_Estimation_UI_Tab_DesignDoc.ts new file mode 100644 index 00000000..c2668719 --- /dev/null +++ b/B09_Estimation/B09_Estimation_UI_Tab_DesignDoc.ts @@ -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; + }) + .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)); + }); + }, +}; diff --git a/B09_Estimation/B09_Estimation_UI_Table.ts b/B09_Estimation/B09_Estimation_UI_Table.ts new file mode 100644 index 00000000..4ead614c --- /dev/null +++ b/B09_Estimation/B09_Estimation_UI_Table.ts @@ -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; +}