refactor(E05): 갱신 띠 · PDF 미리보기 창 · 안내 글 · 빈 자리를 부품으로 (PLAN 553-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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")),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user