feat(B08·B09): 표 칸 안내 · 경고 줄 맨 아래 · 박스보기 · 가로 열 수 보조 탭 줄 오른쪽(202-1)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jonfdxn1Xit9bdkNJpE4G
This commit is contained in:
2026-10-10 14:25:18 +09:00
co-authored by Claude Sonnet 5.5
parent 84ae5100f8
commit d1f7a0d76e
17 changed files with 376 additions and 99 deletions
+12 -12
View File
@@ -36,30 +36,30 @@
overflow-wrap: anywhere;
}
/* 171-3 묶음 탭 머리 — 안내 글 + 보기 단추 */
.b09-estimation__bar {
/* 202-1 보조 탭 줄 오른쪽 도구(박스보기 · 가로 열 수) — 빈 보조 줄에서도 보임 · 단추 · 글 한 줄 */
.b09-estimation__tools {
display: flex;
align-items: flex-start;
justify-content: space-between;
flex: none;
flex-wrap: nowrap;
align-items: center;
gap: var(--spacing-8);
margin-left: auto;
}
/* 185-2 보기 단추 글 한 줄 */
.b09-estimation__bar .ui-btn {
.b09-estimation__tools .ui-btn,
.b09-estimation__tools .ui-group-cards__label {
flex: none;
white-space: nowrap;
}
/* 200-1 묶음 탭 도구 줄 — 안내 글 = 남는 자리 한 줄 · 넘치면 줄임표(전문 = 공용 호버) */
.b09-estimation__bar {
flex-wrap: nowrap;
align-items: center;
/* 202-1 안내 줄 = 표 · 카드 판 아래(묶음 탭은 한 줄 줄임표 · 전문 = 공용 호버) */
.b09-estimation .ui-data-table + .ui-general-block__empty,
.b09-estimation .ui-group-cards + .ui-general-block__empty {
margin: var(--spacing-8) 0 0;
}
.b09-estimation__line-hint {
flex: 1 1 0;
min-width: 0;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+12 -2
View File
@@ -184,6 +184,7 @@ export function renderB09Estimation(root: HTMLElement): void {
const show = (tab: TabId, focus?: string): void => {
current = tab;
bars.draw(tab);
bars.setTools([]); // 보기 단추 · 열 수는 묶음 탭만 다시 채움(202-1)
if (!projectId) return void panel.replaceChildren(note("프로젝트를 먼저 고르세요."));
if (!payload) return void panel.replaceChildren(note("원가 자료를 불러오는 중…"));
const build = BUILDERS[tab];
@@ -246,11 +247,20 @@ export function renderB09Estimation(root: HTMLElement): void {
if (isGroupTab(tab)) {
const redraw = (): void => show(tab);
return void panel.replaceChildren(
...renderGroupPanel({ tab, spec, hint, focus, goto, makeTable, redraw }),
...renderGroupPanel({
tab,
spec,
hint,
focus,
goto,
makeTable,
redraw,
setTools: bars.setTools,
}),
);
}
const table = makeTable();
panel.replaceChildren(...(hint ? [hint] : []), table.root);
panel.replaceChildren(table.root, ...(hint ? [hint] : [])); // 202-1 안내 줄 = 표 아래
if (focus !== undefined) {
const index = rowIndex(spec.rows, focus);
if (index >= 0) requestAnimationFrame(() => table.focusRow(index));
+10 -3
View File
@@ -5,6 +5,7 @@
* 위 탭 = `TAB_GROUPS`(`…_UI_Tabs`) · 아래 탭 하나뿐인 위 탭 = 보이지 않는 sm 단추 한 줄(높이만 채움)
* 위 탭 누름 = 그 묶음에서 마지막으로 본 아래 탭(이 화면 안 Map · 저장 안 함) · 없으면 첫 아래 탭
* 탭 이동은 `draw(탭)` — 이동이 묶음을 넘어도 위 탭이 따라 켜짐
* 보기 단추 · 가로 열 수(202-1) = 아래 줄 오른쪽 맞춤 자리(`setTools`) · 빈 줄에서도 보임
* ========================================================================== */
import { createButton } from "@ui/ui_template_elements";
@@ -15,6 +16,8 @@ export interface TabBars {
sub: HTMLElement;
/** 이 탭이 켜진 꼴로 두 줄을 다시 그림 */
draw: (tab: TabId) => void;
/** 아래 줄 오른쪽 도구(박스보기 · 가로 열 수) — 빈 목록 = 비움 */
setTools: (tools: HTMLElement[]) => void;
}
function tabButton(label: string, on: boolean, size: "sm" | undefined, pick: () => void) {
@@ -35,6 +38,8 @@ export function createTabBars(onPick: (tab: TabId) => void): TabBars {
top.setAttribute("role", "tablist");
const sub = document.createElement("div");
sub.className = "b09-estimation__tabs";
const tools = document.createElement("div");
tools.className = "b09-estimation__tools";
const lastSeen = new Map<TabId, TabId>();
const draw = (tab: TabId): void => {
@@ -53,11 +58,11 @@ export function createTabBars(onPick: (tab: TabId) => void): TabBars {
const single = picked.length === 1;
sub.classList.toggle("b09-estimation__tabs--blank", single);
sub.setAttribute("role", single ? "presentation" : "tablist");
sub.setAttribute("aria-hidden", String(single));
if (single) {
const ghost = createButton({ label: tabName(picked[0]), variant: "ghost", size: "sm" });
ghost.tabIndex = -1;
return void sub.replaceChildren(ghost);
ghost.setAttribute("aria-hidden", "true"); // 도구는 보이는 줄이라 줄 전체를 숨기지 않음
return void sub.replaceChildren(ghost, tools);
}
sub.replaceChildren(
...picked.map((id) => {
@@ -65,7 +70,9 @@ export function createTabBars(onPick: (tab: TabId) => void): TabBars {
button.dataset.b09Tab = id;
return button;
}),
tools,
);
};
return { top, sub, draw };
const setTools = (items: HTMLElement[]): void => tools.replaceChildren(...items);
return { top, sub, draw, setTools };
}
+10 -10
View File
@@ -7,7 +7,8 @@
* 머리 줄 = 묶음 첫 줄 · 합계 줄 = 키 끝 `|합계` · `|총계` · 그 밖 = 몸 줄(표 줄 번호 그대로)
* 목록표에서 온 호표(181-2) = 박스 붉은 외곽선 · 표 보기 = 그 호표 줄 구간 전체 붉은 외곽선
* 보기 · 가로 열 수 기억 = `b_page_state`(`b09-group-view` · `b09-group-cols` · 탭마다) · 못 읽으면 카드 · 2
* 도구 줄(200-1) = 안내 글 + 가로 열 수 + 보기 단추 한 줄 · 안내 글 길면 줄임표 + 공용 호버로 전문
* 보기 단추 · 가로 열 수(202-1) = 페이지 보조 탭 줄 오른쪽(`setTools`) · 안내 글 = 표 · 카드 판 맨 아래
* 안내 글 길면 한 줄 줄임표 + 공용 호버로 전문(200-1)
* ========================================================================== */
import type { DataRow, DataTable } from "@ui/ui_template_data_table";
@@ -176,7 +177,7 @@ function numberFormat(spec: TabTable, key: string): (v: number | null | undefine
/* ── 그리기 ───────────────────────────────────────────────────────────── */
/** 안내 글 = 도구 줄 한 칸(줄임표) · 잘렸을 때만 마우스 올리면 전문(공용 호버 185-3) */
/** 안내 글 = 한 줄(줄임표) · 잘렸을 때만 마우스 올리면 전문(공용 호버 185-3) */
function lineHint(hint: HTMLElement | null): HTMLElement | null {
if (!hint) return null;
hint.classList.add("b09-estimation__line-hint");
@@ -187,7 +188,7 @@ function lineHint(hint: HTMLElement | null): HTMLElement | null {
/** 이 화면 안 보기(기억이 안 돼도 바꾼 보기 유지) */
const chosen = new Map<TabId, GroupView>();
/** 묶음 탭 — 카드 = 카드 판(안내 · 가로 열 수 · 보기 단추 한 줄) · 표 = 머리(안내 + 보기 단추 한 줄) + 표 */
/** 묶음 탭 — 카드 = 카드 판 + 안내 글 · 표 = 표 + 안내 글 · 보기 단추 · 열 수 = `setTools`(보조 탭 줄 오른쪽) */
export function renderGroupPanel(o: {
tab: TabId;
spec: TabTable;
@@ -196,6 +197,7 @@ export function renderGroupPanel(o: {
goto: (link: Link | null) => void;
makeTable: (rowClass?: (index: number) => string | null) => DataTable;
redraw: () => void;
setTools: (tools: HTMLElement[]) => void;
}): HTMLElement[] {
const view = chosen.get(o.tab) ?? readGroupView(o.tab);
const lead = lineHint(o.hint);
@@ -221,17 +223,15 @@ export function renderGroupPanel(o: {
focus: at,
money: numberFormat(o.spec, "sum"),
qty: numberFormat(o.spec, "qty"),
lead, // 200-1 안내 글도 같은 줄
actions: [toggle], // 185-2 가로 열 수 단추와 한 줄
actions: [toggle],
placeTools: o.setTools, // 202-1 가로 열 수 · 보기 단추 = 보조 탭 줄 오른쪽
});
cards.dataset.b09Table = o.tab;
return [cards];
return [cards, ...(lead ? [lead] : [])];
}
const bar = document.createElement("div");
bar.className = "b09-estimation__bar";
bar.append(...(lead ? [lead] : []), toggle);
o.setTools([toggle]);
const range = pickRange(o.spec.rows, o.focus);
const table = o.makeTable((i) => pickClass(range, i));
if (at >= 0) requestAnimationFrame(() => table.focusRow(at));
return [bar, table.root];
return [table.root, ...(lead ? [lead] : [])];
}