feat(b09): 원가검토 탭 두 층 — 위 탭 10 · 아래 탭 묶음(B08 꼴) · 위 탭 다시 누름 = 마지막 본 하위(181-1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
This commit is contained in:
@@ -20,11 +20,16 @@
|
||||
.b09-estimation__tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-4);
|
||||
gap: var(--spacing-8); /* B08 위 · 보조 탭 줄과 같은 값(181-1) */
|
||||
padding-bottom: var(--spacing-8);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* 아래 탭 없는 위 탭 — 보이지 않는 단추 한 줄로 높이만 채움(탭 바꿀 때 몸통이 안 튐) */
|
||||
.b09-estimation__tabs--blank > .ui-btn {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* 표 머리 — 좁은 열에서 말줄임 대신 줄바꿈 */
|
||||
[data-b09-table] thead th {
|
||||
white-space: normal;
|
||||
|
||||
@@ -28,6 +28,7 @@ import { computeBase, type Row } from "./B09_Estimation_Calc_Base";
|
||||
import { computeEstimation } from "./B09_Estimation_Calc_Bill";
|
||||
import { computeLabor, daegaPrice } from "./B09_Estimation_Calc_Labor";
|
||||
import { computeTotal } from "./B09_Estimation_Calc_Total";
|
||||
import { createTabBars } from "./B09_Estimation_UI_TabBar";
|
||||
import { buildDaegaBlocks, buildDaegaList } from "./B09_Estimation_UI_Tabs_Labor";
|
||||
import {
|
||||
buildCheck,
|
||||
@@ -150,15 +151,14 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
let sheets: Row = {};
|
||||
let current: TabId = "check";
|
||||
|
||||
const tabBar = document.createElement("div");
|
||||
tabBar.className = "b09-estimation__tabs";
|
||||
tabBar.setAttribute("role", "tablist");
|
||||
// 181-1 위 탭(md) · 아래 탭(sm) 두 줄 — 줄 그리기 = `…_UI_TabBar`
|
||||
const bars = createTabBars((tab) => show(tab));
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "b09-estimation__panel";
|
||||
panel.setAttribute("role", "tabpanel");
|
||||
const body = document.createElement("div");
|
||||
body.className = "b09-estimation";
|
||||
body.append(tabBar, panel);
|
||||
body.append(bars.top, bars.sub, panel);
|
||||
|
||||
const made = document.createElement("p");
|
||||
made.className = "b09-estimation__hint";
|
||||
@@ -181,20 +181,7 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
/** 탭 하나 그림 · focus = 이동해 온 줄 키 */
|
||||
const show = (tab: TabId, focus?: string): void => {
|
||||
current = tab;
|
||||
tabBar.replaceChildren(
|
||||
...TABS.map(([id, label]) => {
|
||||
const button = createButton({
|
||||
label,
|
||||
variant: id === tab ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
onClick: () => id !== current && show(id),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(id === tab));
|
||||
button.dataset.b09Tab = id;
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
bars.draw(tab);
|
||||
if (!projectId) return void panel.replaceChildren(note("프로젝트를 먼저 고르세요."));
|
||||
if (!payload) return void panel.replaceChildren(note("원가 자료를 불러오는 중…"));
|
||||
const build = BUILDERS[tab];
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/* =============================================================================
|
||||
* B09_Estimation_UI_TabBar.ts
|
||||
* B09 탭 줄 두 층(PLAN 181-1) — 위 탭(md) · 아래 탭(sm) · B08 수량산출 페이지 꼴.
|
||||
*
|
||||
* 위 탭 = `TAB_GROUPS`(`…_UI_Tabs`) · 아래 탭 하나뿐인 위 탭 = 보이지 않는 sm 단추 한 줄(높이만 채움)
|
||||
* 위 탭 누름 = 그 묶음에서 마지막으로 본 아래 탭(이 화면 안 Map · 저장 안 함) · 없으면 첫 아래 탭
|
||||
* 탭 이동은 `draw(탭)` — 이동이 묶음을 넘어도 위 탭이 따라 켜짐
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { groupOf, tabName, TAB_GROUPS, type TabId } from "./B09_Estimation_UI_Tabs";
|
||||
|
||||
export interface TabBars {
|
||||
top: HTMLElement;
|
||||
sub: HTMLElement;
|
||||
/** 이 탭이 켜진 꼴로 두 줄을 다시 그림 */
|
||||
draw: (tab: TabId) => 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";
|
||||
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 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);
|
||||
}
|
||||
sub.replaceChildren(
|
||||
...picked.map((id) => {
|
||||
const button = tabButton(tabName(id), id === tab, "sm", () => onPick(id));
|
||||
button.dataset.b09Tab = id;
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
};
|
||||
return { top, sub, draw };
|
||||
}
|
||||
@@ -56,6 +56,24 @@ export const TABS: Array<[TabId, string]> = [
|
||||
|
||||
export const tabName = (id: string): string => TABS.find(([t]) => t === id)?.[1] ?? id;
|
||||
|
||||
/** 위 탭(181-1) — [위 탭 이름, 아래 탭들] · 아래 탭 하나뿐 = 위 탭 단독 · 첫 아래 탭 = 위 탭 키 */
|
||||
export const TAB_GROUPS: Array<[string, TabId[]]> = [
|
||||
["연결 점검", ["check"]],
|
||||
["공사원가계산서", ["cost"]],
|
||||
["총괄설계내역서", ["total"]],
|
||||
["설계내역서", ["bill"]],
|
||||
["일위대가", ["dg_list", "dg"]],
|
||||
["단가산출", ["sg_list", "sg"]],
|
||||
["환율및기초자료", ["basis"]],
|
||||
["중기", ["machine", "machine_blocks", "sum_machine", "expense", "sum_expense"]],
|
||||
["재료", ["material", "compare", "sum_material"]],
|
||||
["노무", ["labor", "sum_labor"]],
|
||||
];
|
||||
|
||||
/** 탭이 든 위 탭 묶음 */
|
||||
export const groupOf = (tab: TabId): [string, TabId[]] =>
|
||||
TAB_GROUPS.find(([, tabs]) => tabs.includes(tab)) ?? TAB_GROUPS[0];
|
||||
|
||||
/** 다른 탭 줄 — 키가 같은 줄로 이동 */
|
||||
export interface Link {
|
||||
tab: TabId;
|
||||
|
||||
Reference in New Issue
Block a user