refactor(E05): 갱신 띠 · PDF 미리보기 창 · 안내 글 · 빈 자리를 부품으로 (PLAN 553-4)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 22:28:30 +09:00
co-authored by Claude Sonnet 5.5
parent 3e4fcd8845
commit ac24415327
3 changed files with 67 additions and 63 deletions
+48 -21
View File
@@ -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")),
);
}
});
+7 -42
View File
@@ -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%;
+12
View File
@@ -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