From ac24415327f6be7b08078c0ca7dfbd0bc21d6534 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 26 Oct 2026 22:28:30 +0900 Subject: [PATCH] =?UTF-8?q?refactor(E05):=20=EA=B0=B1=EC=8B=A0=20=EB=9D=A0?= =?UTF-8?q?=20=C2=B7=20PDF=20=EB=AF=B8=EB=A6=AC=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=EC=B0=BD=20=C2=B7=20=EC=95=88=EB=82=B4=20=EA=B8=80=20=C2=B7=20?= =?UTF-8?q?=EB=B9=88=20=EC=9E=90=EB=A6=AC=EB=A5=BC=20=EB=B6=80=ED=92=88?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20(PLAN=20553-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- E05_StabReport/E05_StabReport_UI_Page.ts | 69 +++++++++++++++------- E05_StabReport/E05_StabReport_UI_Style.css | 49 +++------------ resources/tester/test_553_4_de_parts.py | 12 ++++ 3 files changed, 67 insertions(+), 63 deletions(-) diff --git a/E05_StabReport/E05_StabReport_UI_Page.ts b/E05_StabReport/E05_StabReport_UI_Page.ts index d700dd885..1d4fcaf12 100644 --- a/E05_StabReport/E05_StabReport_UI_Page.ts +++ b/E05_StabReport/E05_StabReport_UI_Page.ts @@ -18,7 +18,11 @@ import { showLoadingOverlay, showToast, } from "@ui/ui_template_elements"; +import { createBanner } from "@ui/ui_template_banner"; +import { createEmpty } from "@ui/ui_template_empty"; import { t } from "@ui/ui_template_locale"; +import { openModal } from "@ui/ui_template_modal"; +import { createText } from "@ui/ui_template_text"; import { mountGroupStep } from "../A00_Common/group_workflow"; import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet"; import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet_types"; @@ -71,14 +75,24 @@ function download(blob: Blob, name: string): void { setTimeout(() => URL.revokeObjectURL(url), 1000); } +/** 안내 글 한 줄 — 걸이 이름만 얹음 */ +function note(text: string, hook: string): HTMLElement { + const node = createText(text, "muted"); + node.classList.add(hook); + return node; +} + +function emptyBox(text: string): HTMLElement { + const node = createEmpty({ text, small: true }); + node.classList.add("e05-empty"); + return node; +} + export function renderE05StabReport(root: HTMLElement): void { const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY); const main = el("div", { className: "e05-page" }); - const staleBar = el("div", { className: "e05-stale" }); - staleBar.hidden = true; const sheetHost = el("div", { className: "e05-sheet" }); - main.append(staleBar, sheetHost); - const status = el("p", { className: "e05-status" }); + const status = el("div", { className: "e05-status" }); const button = (label: string, variant: "filled" | "ghost" | "danger-ghost", run: () => void) => createButton({ label, variant, onClick: run }); @@ -89,7 +103,15 @@ export function renderE05StabReport(root: HTMLElement): void { const pdfButton = button(t("E05_Btn_Pdf"), "ghost", () => void exportFile("pdf")); const previewButton = button(t("E05_Btn_Preview"), "ghost", () => void preview()); const staleButton = button(t("E05_Btn_Refresh"), "filled", () => void refresh()); - staleBar.append(el("span", { text: `${t("E05_Stale")} —` }), staleButton); + const staleBar = createBanner({ + tone: "warning", + text: `${t("E05_Stale")} —`, + actions: [staleButton], + }); + staleBar.classList.add("e05-stale"); + staleBar.hidden = true; + main.prepend(staleBar); + main.append(sheetHost); const all = [saveButton, resetButton, refreshButton, xlsxButton, pdfButton, previewButton]; for (const b of all) b.disabled = true; // 읽어 온 뒤 켬 @@ -98,28 +120,23 @@ export function renderE05StabReport(root: HTMLElement): void { const actions = el("div", { className: "ui-sidebar-actions" }); actions.append(refreshButton, resetButton, saveButton); const panel = el("div", { className: "ui-sidebar-panel e05-panel" }); - panel.append(el("p", { className: "e05-hint", text: t("E05_Note") }), status, exports, actions); + panel.append(note(t("E05_Note"), "e05-hint"), status, exports, actions); const frame = document.createElement("iframe"); frame.className = "e05-preview__frame"; frame.title = t("E05_Btn_Preview"); - const previewBox = el("div", { className: "e05-preview" }); - previewBox.hidden = true; let previewUrl: string | null = null; + let previewModal: { close: () => void } | null = null; const closePreview = (): void => { - previewBox.hidden = true; + previewModal?.close(); + previewModal = null; frame.removeAttribute("src"); if (previewUrl) URL.revokeObjectURL(previewUrl); previewUrl = null; }; - const head = el("div", { className: "e05-preview__head" }); - head.append(button(t("Common_Btn_Close"), "ghost", closePreview)); - previewBox.append(head, frame); - main.append(previewBox); mountGroupStep(root, ROUTES.E05_STAB_REPORT, { mainContent: main, leftPanel: panel }); - if (!projectId) - return void sheetHost.append(el("p", { className: "e05-empty", text: t("E05_NoProject") })); + if (!projectId) return void sheetHost.append(emptyBox(t("E05_NoProject"))); let sheet: SpreadsheetHandle | null = null; let state: ReportResponse | null = null; @@ -134,7 +151,7 @@ export function renderE05StabReport(root: HTMLElement): void { if (dirty) lines.push(t("E05_Unsaved")); if (state?.meta?.built_at) lines.push(`${t("E05_BuiltAt")} ${state.meta.built_at.replace("T", " ")}`); - status.textContent = lines.join("\n"); + status.replaceChildren(...(lines.length ? [note(lines.join("\n"), "e05-status__text")] : [])); const ready = Boolean(state?.book) && !busy; saveButton.disabled = !ready || !editable || !dirty; resetButton.disabled = !ready || !editable || !state?.has_initial; @@ -281,7 +298,20 @@ export function renderE05StabReport(root: HTMLElement): void { closePreview(); previewUrl = URL.createObjectURL(blob); frame.src = previewUrl; - previewBox.hidden = false; + previewModal = openModal({ + title: t("E05_Btn_Preview"), + closeLabel: t("Common_Btn_Close"), + wide: true, + dialogClass: "e05-preview", + mount: (body) => body.append(frame), + beforeClose: () => { + previewModal = null; + frame.removeAttribute("src"); + if (previewUrl) URL.revokeObjectURL(previewUrl); + previewUrl = null; + return true; + }, + }); }); } @@ -290,10 +320,7 @@ export function renderE05StabReport(root: HTMLElement): void { await adopt(await request(`/projects/${projectId}/stab/report`), true); } catch (error) { sheetHost.replaceChildren( - el("p", { - className: "e05-empty", - text: error instanceof Error ? error.message : t("Common_Status_Error"), - }), + emptyBox(error instanceof Error ? error.message : t("Common_Status_Error")), ); } }); diff --git a/E05_StabReport/E05_StabReport_UI_Style.css b/E05_StabReport/E05_StabReport_UI_Style.css index fda06c9da..cdf24e680 100644 --- a/E05_StabReport/E05_StabReport_UI_Style.css +++ b/E05_StabReport/E05_StabReport_UI_Style.css @@ -11,68 +11,33 @@ box-sizing: border-box; } -.e05-stale { - display: flex; - align-items: center; - gap: var(--spacing-12); - padding: var(--spacing-8) var(--spacing-16); - background: color-mix(in srgb, var(--color-warning) 18%, var(--color-surface)); - color: var(--color-text); - border-bottom: 1px solid var(--color-border); -} - +/* 띠 알림 · 보조 글 · 빈 자리 · 모달은 부품 — 여기는 자리만 */ .e05-stale[hidden] { display: none; } +.e05-stale .ui-banner__body { + flex: none; +} + .e05-sheet { flex: 1; min-height: 0; } -.e05-empty, -.e05-status, +.e05-status__text, .e05-hint { - margin: 0; white-space: pre-line; - color: var(--color-text-muted); } .e05-empty { - padding: var(--spacing-16); -} - -.e05-status.is-error { - color: var(--color-danger); + margin: var(--spacing-16); } .e05-panel .ui-sidebar-actions { flex-wrap: wrap; } -.e05-preview { - position: fixed; - inset: 4vh 4vw; - z-index: 50; - display: flex; - flex-direction: column; - gap: var(--spacing-8); - padding: var(--spacing-12); - border-radius: var(--radius-sm); - background: var(--color-surface-raised); - box-shadow: 0 8px 32px rgb(0 0 0 / 35%); -} - -.e05-preview[hidden] { - display: none; -} - -.e05-preview__head { - display: flex; - justify-content: flex-end; - gap: var(--spacing-8); -} - .e05-preview__frame { flex: 1; width: 100%; diff --git a/resources/tester/test_553_4_de_parts.py b/resources/tester/test_553_4_de_parts.py index 48cf90f58..cc714073b 100644 --- a/resources/tester/test_553_4_de_parts.py +++ b/resources/tester/test_553_4_de_parts.py @@ -133,3 +133,15 @@ def test_e04_parts(): ".e04-section:hover", ): assert dead not in css, dead + + +def test_e05_parts(): + ts = _src("E05_StabReport/E05_StabReport_UI_Page.ts") + css = _src("E05_StabReport/E05_StabReport_UI_Style.css") + for part in ("createBanner(", "openModal(", "createEmpty(", "createText("): + assert part in ts, part + assert 'el("p"' not in ts and "previewBox" not in ts + assert 'classList.add("e05-stale")' in ts # 촬영 장면이 `.e05-stale .ui-btn` 으로 잡음 + assert ".e05-stale[hidden]" in css # 띠 알림은 flex 라 hidden 을 덮음 + for dead in (".e05-preview {", ".e05-preview__head", ".e05-status.is-error"): + assert dead not in css, dead