refactor(B08·B09): 자체 변종 → 부품 상자 — 탭 줄 · 분할 고르기 · 배지 칩 · 도구줄 · 떠 있는 판 · 보조 글 (PLAN 553-3)

- 위 · 보조 탭 줄 = 부품 탭(fillTabs · 밑줄 · 알약) · B09 좌측 스위치 = 분할 고르기
- 연결 칩 · 검사 탭 이름 · 끌기 태그 = 배지 · 이정표 도구 · 머리 줄 · 저장 줄 = 도구줄 · 구간 고침 · 행 고름 창 = 떠 있는 판
- 안내 · 빈 줄 · 상태 글 = 보조 글 · 운반 칸 = 카드 · 인쇄 미리보기 좌측 줄 = B09 dock 한 벌 · 쪽 체크 = 라디오 · 체크 부품
- 시험 흉내 틀 · 문구 맞춤

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 22:33:35 +09:00
co-authored by Claude Sonnet 5.5
parent b114616505
commit 361ce51659
38 changed files with 392 additions and 478 deletions
+8 -9
View File
@@ -8,6 +8,8 @@
* ========================================================================== */
import { attachHoverInfo } from "@ui/ui_template_hover_info";
import { createBadge } from "@ui/ui_template_badge";
import { createText } from "@ui/ui_template_text";
import type { Row } from "./B09_Estimation_Calc_Base";
import { tabName, type Link, type TabId } from "./B09_Estimation_UI_Tabs";
@@ -60,9 +62,8 @@ export function createCheckPanel(refresh: HTMLElement, goto: (link: Link) => voi
refresh.classList.toggle("is-pending", Boolean(next.stale));
const made = checkItems(sheets, next);
items = made.items;
const head = document.createElement("p");
head.className = "b09-check__head";
head.textContent = `연결 없음 ${items.length}곳`;
const head = createText(`연결 없음 ${items.length}곳`, "muted");
head.classList.add("b09-check__head");
const rows = document.createElement("div");
rows.className = "ui-sidebar-list";
rows.append(
@@ -73,18 +74,16 @@ export function createCheckPanel(refresh: HTMLElement, goto: (link: Link) => voi
row.dataset.i = String(i);
const name = document.createElement("span");
name.textContent = item.name;
const tab = document.createElement("span");
tab.className = "b09-check__tab";
tab.textContent = item.tab;
const tab = createBadge(item.tab, "neutral", { size: 18 });
tab.classList.add("b09-check__tab");
row.append(name, tab);
row.addEventListener("click", () => goto(item.link));
return row;
}),
);
const notes = made.notes.map((text) => {
const p = document.createElement("p");
p.className = "b09-check__note";
p.textContent = text;
const p = createText(text, "muted");
p.classList.add("b09-check__note");
return p;
});
list.replaceChildren(head, ...(items.length ? [rows] : []), ...notes);
+9 -27
View File
@@ -9,7 +9,7 @@
* 스위치 값 = pref global(계정 공용 · [저장] 필요 없음) · 바닥 고정 = 공용 `.ui-sidebar-actions` 한 벌
* ========================================================================== */
import { createButton } from "@ui/ui_template_elements";
import { createSegment } from "@ui/ui_template_segment";
import { readState, writeState } from "../A00_Common/b_page_state";
import { DRAFT } from "./B09_Estimation_UI_Tabs";
import {
@@ -40,7 +40,7 @@ function setLinkMode(mode: LinkMode): void {
}
}
/** 단추 둘 스위치 — 켜진 쪽 filled · aria-pressed */
/** 단추 둘 스위치 — 부품 「분할 고르기」(32) · 고른 쪽 aria-checked */
function switchRow<T extends string>(
name: string,
values: T[],
@@ -48,32 +48,14 @@ function switchRow<T extends string>(
current: () => T,
pick: (value: T) => void,
): HTMLElement {
const row = document.createElement("div");
row.className = "ui-btn-group b09-dock__row";
row.dataset.b09Switch = name;
const buttons = values.map((value) => {
const button = createButton({
label: label(value),
variant: "ghost",
onClick: () => {
if (value === current()) return;
pick(value);
sync();
},
});
button.dataset.value = value;
return button;
const segment = createSegment({
options: values.map((value) => ({ value, label: label(value) })),
value: current(),
size: 32,
onChange: (value) => pick(value as T),
});
const sync = (): void =>
buttons.forEach((button, k) => {
const on = values[k] === current();
button.classList.toggle("ui-btn--filled", on);
button.classList.toggle("ui-btn--ghost", !on);
button.setAttribute("aria-pressed", String(on));
});
sync();
row.append(...buttons);
return row;
segment.root.dataset.b09Switch = name;
return segment.root;
}
/** 하단 고정 줄 — onView = 보기 바뀜(묶음 탭이면 다시 그림) */
@@ -4,10 +4,8 @@
flex-direction: column;
gap: var(--spacing-12);
}
.b09-haulstep {
border: 1px solid var(--color-border);
border-radius: var(--radius-cards);
background-color: var(--color-surface-raised);
.b09-haulstep.ui-card {
padding: 0; /* 부품 카드 · 머리 · 바닥 줄이 가장자리까지 */
color: var(--color-text-body);
font-size: var(--text-body-sm);
}
@@ -83,7 +83,7 @@ const group = (label: string, rows: StepRow[], ats?: number[]): HTMLElement[] =>
/** 칸 하나 — 제목 줄(제목 · 곁글) · 몸 · 바닥(굵게) */
function box(title: string, sub: string, body: HTMLElement[], foot: HTMLElement[]): HTMLElement {
return el("section", {
className: "b09-haulstep",
className: "b09-haulstep ui-card",
children: [
el("div", {
className: "b09-haulstep__head",
+35 -48
View File
@@ -8,22 +8,17 @@
padding: var(--spacing-16) var(--spacing-24); /* B08 본문과 같은 값 */
}
.b09-estimation .ui-general-block__empty,
.b09-estimation__hint {
color: var(--color-text-secondary);
}
.b09-estimation__hint {
margin: 0 0 var(--spacing-8);
font-size: var(--text-caption);
}
/* 탭 줄 모양 = 부품 탭(밑줄 · 보조 알약) · 여기는 줄바꿈 · 아래 틈만(B08 과 같은 값) */
.b09-estimation__tabs {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-8); /* B08 위 · 보조 탭 줄과 같은 값(181-1) */
}
.b09-estimation > .b09-estimation__tabs:first-child {
margin-bottom: var(--spacing-8);
}
.b09-estimation__tabs.ui-subtabs {
padding-bottom: var(--spacing-8);
border-bottom: 1px solid var(--color-border);
}
/* ☰ 들여쓰기(7-2) */
@@ -59,12 +54,13 @@ html.is-bm-route .b09-estimation > .b09-estimation__tabs:first-child {
}
/* 202-1 안내 줄 = 표 · 카드 판 아래 · 203 줄 수 변수만큼 보임 · 넘치면 영역 안 스크롤 */
.b09-estimation .ui-data-table + .ui-general-block__empty,
.b09-estimation .ui-group-cards + .ui-general-block__empty {
.b09-estimation .ui-data-table + .ui-text,
.b09-estimation .ui-group-cards + .ui-text {
margin: var(--spacing-8) 0 0;
}
.b09-estimation__line-hint {
display: block; /* 부품 보조 글은 flex — 줄 수 · 스크롤은 블록에서 */
min-width: 0;
max-height: calc(var(--b09-hint-lines) * 1lh);
overflow-y: auto;
@@ -192,15 +188,11 @@ html.is-bm-route .b09-estimation > .b09-estimation__tabs:first-child {
/* 연결 점검 목록 — 이름 · 탭(작게) · 알림 글 */
.b09-check__head,
.b09-check__note {
margin: var(--spacing-8) 0 0;
color: var(--color-text-secondary);
font-size: var(--text-caption);
margin-top: var(--spacing-8);
}
.b09-check__tab {
flex: none;
color: var(--color-text-secondary);
font-size: var(--text-caption);
}
/* 줄 끌기(204-1) — 놓을 자리 · 레벨 미리 보기 선 · 못 놓는 자리 = 빨간 점선 */
@@ -232,15 +224,11 @@ html.is-bm-route .b09-estimation > .b09-estimation__tabs:first-child {
background: var(--color-canvas);
}
.b09-estimation__drop-tag {
.b09-estimation__drop > .b09-estimation__drop-tag.ui-badge {
position: absolute;
top: -11px;
padding: 1px var(--spacing-8);
border: 1px solid;
border-radius: var(--radius-pills);
border: 1px solid; /* 놓을 자리 색(--drop-c)을 따름 */
background: var(--color-surface-raised);
font-size: var(--text-caption);
white-space: nowrap;
}
.b09-estimation [data-b09-table="bill"] tbody tr.is-drop-into td {
@@ -266,44 +254,43 @@ html.is-bm-route .b09-estimation > .b09-estimation__tabs:first-child {
.b09-dock .b09-dock__row {
display: flex;
gap: var(--btn-group-gap);
min-width: 0;
}
/* 보기 · 행 누름 스위치 = 부품 분할 고르기 · 줄 폭을 나눠 가짐 */
.b09-dock > .ui-segment {
display: flex;
}
.b09-dock > .ui-segment > .ui-segment__item {
flex: 1 1 0;
min-width: 0;
}
/* 줄 폭을 나눠 가짐 · 가로 넘침 없음(min-width 0 = 공용 단추 줄의 auto 보다 앞섬 · 글 줄임표) — 인쇄 미리보기 좌측 줄도 같은 규칙 */
.b09-dock .b09-dock__row > .ui-btn:not(.ui-help-btn, .ui-step-nav-btn) {
flex: 1 1 0;
min-width: auto;
min-width: 0;
padding-inline: var(--spacing-4);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 303-2 행 고름 팝업 — 누른 줄 곁 표 조각 창(겹쳐 쌓임 · 창 안 b09 표 꼴 그대로 · 빨강 테두리) */
.b09-dock .b09-dock__row > .ui-btn[hidden] {
display: none;
}
/* 303-2 행 고름 팝업 — 누른 줄 곁 표 조각 창(겹쳐 쌓임) = 부품 떠 있는 판 · 여기는 자리 · 크기만 */
.b09-estimation.b09-popup {
position: fixed;
z-index: var(--z-modal);
gap: var(--spacing-4);
width: max-content;
max-width: 90vw;
max-height: 80vh;
padding: var(--spacing-8);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-surface-raised);
box-shadow: var(--shadow-lg);
}
.b09-popup__head {
display: flex;
flex: none;
align-items: center;
gap: var(--spacing-4);
}
.b09-popup__title {
margin-right: auto;
font-weight: var(--font-weight-bold);
padding: 0;
}
.b09-popup .ui-data-table {
flex: 1 1 auto;
min-height: 0;
max-height: none;
}
+12 -18
View File
@@ -12,6 +12,7 @@
import { createDataTable, type DataTableOptions } from "@ui/ui_template_data_table";
import { createButton } from "@ui/ui_template_elements";
import { createFloatPanel } from "@ui/ui_template_float_panel";
import { placeHoverInfo } from "@ui/ui_template_hover_info";
import { tableOptionsOf } from "./B09_Estimation_UI_Builders";
import { rowIndex, tabName, type Link, type TabId, type TabTable } from "./B09_Estimation_UI_Tabs";
@@ -135,35 +136,28 @@ export function openRowPopup(o: PopupOpen): void {
if (!spec || !range) return o.go(o.link); // 보일 줄 없음 = 옮김
listen();
const level = stack.length;
const box = document.createElement("div");
box.className = "b09-estimation b09-popup ui-popup";
// 떠 있는 판(부품) — 머리 = 제목 + [탭 이동] + 닫기 ✕ · 몸 = 표 조각
const panel = createFloatPanel({ title: tabName(tab), onClose: () => closePopups(level) });
const box = panel.root;
box.classList.add("b09-estimation", "b09-popup", "ui-popup");
box.setAttribute("role", "dialog");
box.setAttribute("aria-label", tabName(tab));
box.dataset.b09Popup = tab;
const title = document.createElement("span");
title.className = "b09-popup__title";
title.textContent = tabName(tab);
const head = box.querySelector<HTMLElement>(".ui-float-panel__head") as HTMLElement;
head.classList.add("b09-popup__head");
const closeButton = head.querySelector<HTMLElement>(".ui-icon-btn") as HTMLElement;
closeButton.dataset.b09PopupClose = "";
const goButton = createButton({
label: "탭 이동",
variant: "ghost",
size: "sm",
size: 24,
onClick: () => {
closePopups();
o.go(o.link);
},
});
goButton.dataset.b09PopupGo = "";
const closeButton = createButton({
label: "×",
variant: "ghost",
size: "sm",
onClick: () => closePopups(level),
});
closeButton.dataset.b09PopupClose = "";
closeButton.setAttribute("aria-label", "닫기");
const head = document.createElement("div");
head.className = "b09-popup__head";
head.append(title, goButton, closeButton);
closeButton.before(goButton);
const from = range[0];
const table = createDataTable({
...popupOptions(spec, tab, range),
@@ -181,7 +175,7 @@ export function openRowPopup(o: PopupOpen): void {
},
});
table.root.dataset.b09Table = tab;
box.append(head, table.root);
panel.body.append(table.root);
document.body.append(box);
stack.push(box);
const view = { width: window.innerWidth, height: window.innerHeight };
+2 -4
View File
@@ -14,6 +14,7 @@ import "@ui/ui_template_general_layout.css";
import "./B09_Estimation_UI_Page.css";
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
import { createText } from "@ui/ui_template_text";
import {
createButton,
hideLoadingOverlay,
@@ -79,10 +80,7 @@ async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
}
function note(textValue: string): HTMLElement {
const p = document.createElement("p");
p.className = "ui-general-block__empty";
p.textContent = textValue;
return p;
return createText(textValue, "muted", 14);
}
/** 산근(145-1) = 서버 단독 — 받은 그대로 두는 칸(+ sg_hand = 손 줄 로직 → 산근 키 · 166-2) */
+35 -43
View File
@@ -1,14 +1,15 @@
/* =============================================================================
* B09_Estimation_UI_TabBar.ts
* B09 탭 줄 두 층(PLAN 181-1) — 위 탭(md) · 아래 탭(sm) · B08 수량산출 페이지 꼴.
* B09 탭 줄 두 층(PLAN 181-1) — 위 탭(밑줄 · 32) · 아래 탭(보조 알약) · B08 수량산출 페이지 꼴(부품 「탭」).
*
* 위 탭 = `TAB_GROUPS`(`…_UI_Tabs`) · 아래 탭 하나뿐인 위 탭 = 보이지 않는 sm 단추 한 줄(높이만 채움)
* 위 탭 = `TAB_GROUPS`(`…_UI_Tabs`) · 아래 탭 하나뿐인 위 탭 = 보이지 않는 보조 탭 한 칸(높이만 채움)
* 위 탭 누름 = 그 묶음에서 마지막으로 본 아래 탭(이 화면 안 Map · 저장 안 함) · 없으면 첫 아래 탭
* 탭 이동은 `draw(탭)` — 이동이 묶음을 넘어도 위 탭이 따라 켜짐
* 보기 단추 · 가로 열 수(202-1) = 아래 줄 오른쪽 맞춤 자리(`setTools`) · 빈 줄에서도 보임
* ========================================================================== */
import { createButton } from "@ui/ui_template_elements";
import { createToolbar } from "@ui/ui_template_toolbar";
import { fillTabs } from "../A00_Common/b_page_tabs";
import { groupOf, tabName, TAB_GROUPS, type TabId } from "./B09_Estimation_UI_Tabs";
export interface TabBars {
@@ -20,58 +21,49 @@ export interface TabBars {
setTools: (tools: HTMLElement[]) => void;
}
function tabButton(label: string, on: boolean, size: "sm" | undefined, pick: () => void) {
const button = createButton({
label,
variant: on ? "filled" : "ghost",
...(size ? { size } : {}),
onClick: () => !on && pick(),
});
button.setAttribute("role", "tab");
button.setAttribute("aria-selected", String(on));
return button;
}
export function createTabBars(onPick: (tab: TabId) => void): TabBars {
const top = document.createElement("div");
top.className = "b09-estimation__tabs";
top.setAttribute("role", "tablist");
const sub = document.createElement("div");
sub.className = "b09-estimation__tabs ui-subtabs";
const tools = document.createElement("div");
tools.className = "b09-estimation__tools ui-subtabs__tools";
sub.className = "b09-estimation__tabs";
const tools = createToolbar({ items: [], size: 24 });
tools.classList.add("b09-estimation__tools", "ui-subtabs__tools");
const lastSeen = new Map<TabId, TabId>();
const draw = (tab: TabId): void => {
const [, picked] = groupOf(tab);
lastSeen.set(picked[0], tab);
top.replaceChildren(
...TAB_GROUPS.map(([label, tabs]) => {
const button = tabButton(label, tabs === picked, undefined, () =>
onPick(lastSeen.get(tabs[0]) ?? tabs[0]),
);
button.dataset.b09Group = tabs[0];
if (tabs.length === 1) button.dataset.b09Tab = tabs[0];
return button;
}),
);
const groupTab = new Map(TAB_GROUPS.map(([, tabs]) => [tabs[0], tabs]));
fillTabs(top, {
tabs: TAB_GROUPS.map(([label, tabs]) => ({ key: tabs[0], label })),
value: picked[0],
kind: "line",
size: 32,
data: "b09Group",
onChange: (key) => onPick(lastSeen.get(key as TabId) ?? (key as TabId)),
}).forEach((button) => {
const tabs = groupTab.get(button.dataset.tab as TabId);
if (tabs?.length === 1) button.dataset.b09Tab = tabs[0];
});
const single = picked.length === 1;
sub.classList.toggle("b09-estimation__tabs--blank", single);
const buttons = fillTabs(sub, {
tabs: single
? [{ key: picked[0], label: tabName(picked[0]) }]
: picked.map((id) => ({ key: id, label: tabName(id) })),
value: single ? picked[0] : tab,
kind: "pill",
data: "b09Tab",
onChange: (id) => onPick(id as TabId),
});
sub.setAttribute("role", single ? "presentation" : "tablist");
if (single) {
const ghost = createButton({ label: tabName(picked[0]), variant: "ghost", size: "sm" });
ghost.tabIndex = -1;
ghost.setAttribute("aria-hidden", "true"); // 도구는 보이는 줄이라 줄 전체를 숨기지 않음
return void sub.replaceChildren(ghost, tools);
}
sub.replaceChildren(
...picked.map((id) => {
const button = tabButton(tabName(id), id === tab, "sm", () => onPick(id));
button.dataset.b09Tab = id;
return button;
}),
tools,
);
if (single)
for (const button of buttons) {
button.tabIndex = -1;
button.setAttribute("aria-hidden", "true"); // 도구는 보이는 줄이라 줄 전체를 숨기지 않음
delete button.dataset.b09Tab;
}
sub.append(tools);
};
const setTools = (items: HTMLElement[]): void => tools.replaceChildren(...items);
return { top, sub, draw, setTools };
@@ -9,6 +9,7 @@
* 잡기 = 글 칸 · 단추 밖 아무 칸 · 6px 넘게 움직여야 끌기(그냥 누름 = 줄 고름)
* ========================================================================== */
import { createBadge } from "@ui/ui_template_badge";
import type { DataRow } from "@ui/ui_template_data_table";
import type { BillTable } from "./B09_Estimation_UI_Tabs_Bill_Edit";
@@ -68,8 +69,8 @@ export function attachDrag(root: HTMLElement, table: BillTable, done: (key: stri
const step = parseFloat(getComputedStyle(tr).getPropertyValue("--dt-indent")) || 14;
const line = document.createElement("div");
line.className = "b09-estimation__drop";
const tag = document.createElement("span");
tag.className = "b09-estimation__drop-tag";
const tag = createBadge("", "neutral", { size: 18 });
tag.classList.add("b09-estimation__drop-tag");
line.append(tag);
const rowEl = (i: number): HTMLElement | null => root.querySelector(`tr[data-i="${i}"]`);
let into: HTMLElement | null = null;
@@ -16,6 +16,7 @@
import type { DataRow } from "@ui/ui_template_data_table";
import { createButton } from "@ui/ui_template_elements";
import { createText } from "@ui/ui_template_text";
import { attachHoverInfo } from "@ui/ui_template_hover_info";
import { attachDrag } from "./B09_Estimation_UI_Tabs_Bill_Drag";
import type { BillTable } from "./B09_Estimation_UI_Tabs_Bill_Edit";
@@ -149,9 +150,8 @@ export function levelBar(
button.dataset.b09Level = label;
return button;
});
const guide = document.createElement("p");
guide.className = "ui-general-block__empty b09-estimation__line-hint";
guide.textContent = GUIDE;
const guide = createText(GUIDE, "muted");
guide.classList.add("b09-estimation__line-hint");
attachHoverInfo(guide, (el) => (el.scrollWidth > el.clientWidth ? el.textContent : null)); // 236-2 잘릴 때만
const sync = (): void => {
const acts = menuActs(table, index());
+2 -47
View File
@@ -10,14 +10,7 @@
}
.b09-print__status {
margin: 0;
min-height: 1.4em;
font-size: var(--text-caption);
color: var(--color-text-secondary);
}
.b09-print__status.is-error {
color: var(--color-danger);
min-height: 1.4em; /* 글이 비어도 줄 높이 유지 */
}
.b09-print__frame {
@@ -30,9 +23,7 @@
}
.b09-print__areas {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-4);
flex-wrap: wrap; /* 부품 보조 탭 줄 */
}
.b09-print__list {
@@ -42,39 +33,3 @@
max-height: 50vh;
overflow: auto;
}
.b09-print__item {
display: flex;
align-items: center;
gap: var(--spacing-8);
font-size: var(--text-body-sm);
cursor: pointer;
}
/* 좌측 하단 두 줄(302-5 · B09 Dock 꼴) — 단추는 줄 폭을 나눠 가짐 · 가로 넘침 없음 */
.b09-print__actions.ui-sidebar-actions {
flex-direction: column;
}
.b09-print__actions.ui-sidebar-actions > * {
flex: none;
}
.b09-print__actions .b09-print__row {
display: flex;
gap: var(--btn-group-gap);
min-width: 0;
}
.ui-sidebar-actions.b09-print__actions .b09-print__row > .ui-btn:not(.ui-help-btn) {
flex: 1 1 0;
min-width: 0;
padding-inline: var(--spacing-4);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.b09-print__actions .b09-print__row > .ui-btn[hidden] {
display: none;
}
+27 -31
View File
@@ -11,14 +11,18 @@
* ========================================================================== */
import "@ui/ui_template_general_layout.css";
import "../B09_Estimation_UI_Page.css"; // 좌측 하단 단추 줄(b09-dock) 규칙 한 벌
import "./B09_Draft_Print_Page.css";
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible";
import { createCheckbox } from "@ui/ui_template_checkbox";
import { createButton } from "@ui/ui_template_elements";
import { createText } from "@ui/ui_template_text";
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
import type { MountedPage } from "../../A00_Common/router";
import { navigateTo } from "../../A00_Common/router";
import { workflowSteps } from "../../A00_Common/b_page_scaffold";
import { fillTabs } from "../../A00_Common/b_page_tabs";
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../../A00_Common/b_workflow_nav";
import { inlineImages, pdfBody, xlsxBody } from "./B09_Draft_Print_Table";
import type {
@@ -114,8 +118,8 @@ function save(blob: Blob, name: string): void {
export function renderB09Print(root: HTMLElement): MountedPage {
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
const status = document.createElement("p");
status.className = "b09-print__status";
const status = createText("", "muted");
status.className += " b09-print__status";
status.dataset.b09PrintStatus = "";
const frame = document.createElement("iframe");
frame.className = "b09-print__frame";
@@ -144,6 +148,8 @@ export function renderB09Print(root: HTMLElement): MountedPage {
const say = (text: string, error = false) => {
status.textContent = text;
status.classList.toggle("is-error", error);
status.classList.toggle("ui-text--error", error);
status.classList.toggle("ui-text--muted", !error);
};
const picked = () => (area?.items ?? []).map((it) => it.id).filter((id) => !off.has(id));
const clearFrame = () => {
@@ -213,21 +219,16 @@ export function renderB09Print(root: HTMLElement): MountedPage {
const drawList = () => {
list.replaceChildren(
...(area?.items ?? []).map((it) => {
const row = document.createElement("label");
row.className = "b09-print__item";
const box = document.createElement("input");
box.type = "checkbox";
box.checked = !off.has(it.id);
box.dataset.b09PrintItem = it.id;
box.addEventListener("change", () => {
if (box.checked) off.delete(it.id);
else off.add(it.id);
later();
const row = createCheckbox(it.name, {
checked: !off.has(it.id),
onChange: (on) => {
if (on) off.delete(it.id);
else off.add(it.id);
later();
},
});
const name = document.createElement("span");
name.textContent = it.name;
row.append(box, name);
return row;
row.input.dataset.b09PrintItem = it.id;
return row.root;
}),
);
};
@@ -238,18 +239,13 @@ export function renderB09Print(root: HTMLElement): MountedPage {
off = new Set();
const my = ++seq;
clearFrame();
areaRow.replaceChildren(
...AREAS.map((a) => {
const b = createButton({
label: a.label,
size: "sm",
variant: a.id === id ? "filled" : "ghost",
onClick: () => a.id !== areaId && pickArea(a.id),
});
b.dataset.b09PrintArea = a.id;
return b;
}),
);
fillTabs(areaRow, {
tabs: AREAS.map((a) => ({ key: a.id, label: a.label })),
value: id,
kind: "pill",
data: "b09PrintArea",
onChange: (key) => pickArea(key as AreaId),
});
xlsxBtn.hidden = id === "drawings";
drawList();
if (!projectId) return say("현재 프로젝트가 없습니다.", true);
@@ -270,14 +266,14 @@ export function renderB09Print(root: HTMLElement): MountedPage {
// 두 줄(302-5) — 위 = 내려받기 · 아래 = [?](도움말 줄) · 돌아가기 — 패널 폭 넘침 없음
const files = document.createElement("div");
files.className = "b09-print__row";
files.className = "b09-dock__row b09-print__row";
files.append(pdfBtn, xlsxBtn);
const nav = document.createElement("div");
nav.className = "b09-print__row";
nav.className = "b09-dock__row b09-print__row";
nav.dataset.helpRow = "";
nav.append(back);
const actions = document.createElement("div");
actions.className = "ui-sidebar-actions b09-print__actions";
actions.className = "ui-sidebar-actions b09-dock b09-print__actions";
actions.append(files, nav);
const left = document.createElement("div");
left.className = "ui-sidebar-panel b09-print__panel";