Files
Aislo/B09_Estimation/B09_Estimation_UI_Tabs.ts
T
eomsangdonandClaude Opus 5.5 0db53a9032 feat(B09): 264-1 왼쪽 패널 「공기 옵션」 카드
- draft 새 파일 B09_Draft_Schedule_Panel — 착공일 · 기후 자료 · 쉬는 날 · 공휴일 넣기 · 일 작업시간 · 투입 기본 · 준비 · 정리 · 기타 · 반영률 · 주 장비 기준 · 중복 C 끝수 · 마지막 달
- 값 = user.schedule(캐시 → [저장]) · 표 칸과 같은 값 · 셈마다 카드 맞춤
- 훅 = DRAFT.panels(기본 빈 목록) · Shell 「데이터 점검」 아래 한 줄
- 공기집계 비고 = 고른 옵션을 따름 · 착공 칸 「연.월.일」

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
2026-10-11 16:40:29 +09:00

179 lines
6.8 KiB
TypeScript

/* =============================================================================
* B09_Estimation_UI_Tabs.ts
* B09 탭 목록 · 표 짜기 공용(PLAN 144-1) — 탭 하나 = 화면 표 하나(실무 엑셀 탭 차례).
*
* 탭 = 원가 · 총괄 · 내역 · 대가 둘 · 산근 둘 · 기초 · 중기 둘 · 단가 넷 · 집계표 넷
* 표 = 공용 `createDataTable` · 「연결 없음」 칸 = 빈칸 · 호표 칸 누름 = 그 탭 그 줄로 이동(`link`)
* 탭별 표 = `…_UI_Tabs_Base`(144) · `…_UI_Tabs_Cost`(145) · 여기는 차례 · 꼴만
* 연결 점검(옛 첫 탭) = 좌측 「데이터 점검」 묶음(203-1 `…_UI_Check`)
* ========================================================================== */
import type { MapContextMenuItem } from "@ui/ui_template_context_menu";
import type { DataColumn, DataRow, DataTableOptions } from "@ui/ui_template_data_table";
import type { Row } from "./B09_Estimation_Calc_Base";
export type TabId =
| "cost"
| "total"
| "bill"
| "dg_list"
| "dg"
| "sg_list"
| "sg"
| "basis"
| "machine"
| "machine_blocks"
| "material"
| "labor"
| "expense"
| "compare"
| "sum_material"
| "sum_labor"
| "sum_expense"
| "sum_machine";
/** 엑셀 탭 차례 — [id, 탭 이름] */
export const TABS: Array<[TabId, string]> = [
["cost", "공사원가계산서"],
["total", "총괄설계내역서"],
["bill", "설계내역서"],
["dg_list", "일위대가목록표"],
["dg", "일위대가표"],
["sg_list", "단가산출근거목록표"],
["sg", "단가산출근거"],
["basis", "환율및기초자료"],
["machine", "중기목록표"],
["machine_blocks", "중기사용료"],
["material", "재료비목록표"],
["labor", "노무비목록표"],
["expense", "경비목록표"],
["compare", "자재단가대비표"],
["sum_material", "재료비수량금액집계표"],
["sum_labor", "노무비수량금액집계표"],
["sum_expense", "경비수량금액집계표"],
["sum_machine", "중기시간금액집계표"],
];
export const tabName = (id: string): string => TABS.find(([t]) => t === id)?.[1] ?? id;
/** 위 탭(181-1) — [위 탭 이름, 아래 탭들] · 아래 탭 하나뿐 = 위 탭 단독 · 첫 아래 탭 = 위 탭 키 */
export const TAB_GROUPS: Array<[string, TabId[]]> = [
["공사원가계산서", ["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;
key: string;
}
/** 페이지가 탭 짜기에 주는 것 */
export interface TabContext {
sheets: Row;
payload: Row;
user: Row;
/** 사용자 값 고침 → 캐시 · 바로 다시 셈 */
setUser: (user: Row) => void;
}
/** 탭 한 장 = 표 하나 */
export interface TabTable {
columns: DataColumn[];
rows: DataRow[];
total?: DataRow | null;
frozen?: number;
/** 표 위 안내 글(근거 · 끝수) */
hint?: string;
/** 칸 누름 → 갈 줄(호표 칸) */
link?: (index: number, key: string) => Link | null;
editable?: (index: number, key: string) => boolean;
/** 고칠 칸 = 더블클릭 · Enter · F2 때만 글 상자(235-1 · 공용 표 editOnDouble) */
editOnDouble?: boolean;
onEdit?: (index: number, key: string, text: string) => void;
/** 칸 누름 → 그 자리 동작(예 관급 · 별도 표시 바꿈) */
onCell?: (index: number, key: string) => void;
/** 줄 꼴 class(공용 표 `rowClass` · 예 제목 줄 굵게) */
rowClass?: (index: number) => string | null | undefined;
/** 칸 풍선 글(157-3 · 예 「이름 규격」 칸의 원문번호) */
tip?: (index: number, key: string) => string | null | undefined;
/** 칸 꼴 class(183-3 공용 표 `cellClass` · 예 고른 출처 칸 강조) */
cellClass?: (index: number, key: string) => string | null | undefined;
/** 칸 누름 고르기 메뉴(183-3 · 칸 아래 공용 메뉴 · 빈 것 = 안 엶) */
choices?: (index: number, key: string) => MapContextMenuItem[] | null;
/** 칸 우클릭 메뉴(257-3 · 줄 메뉴가 없는 표 · 빈 것 = 안 엶) */
cellMenu?: (index: number, key: string) => MapContextMenuItem[] | null;
/** 레벨(201-1 · 공용 표 level · 보기 전용) — 그 열 들여쓰기 */
level?: DataTableOptions["level"];
/** 같은 값 세로 병합할 열(201-1 · 공용 표 mergeSame) */
mergeSame?: string[];
}
export type TabBuild = (ctx: TabContext) => TabTable | string;
/** 초안 탭(252 · `draft/*_UI.ts` 가 TABS · TAB_GROUPS 와 함께 넣음 · 폴더째 지우면 빈 채로) —
* builders = 탭 짜기 · extend = 브라우저 셈 곁(셈 결과에 더함) ·
* panels = 왼쪽 패널 카드(264 · 「데이터 점검」 아래 · 지금 사용자 값 · 고침 → 캐시 · 다시 셈) */
export const DRAFT: {
builders: Record<string, TabBuild>;
extend: Array<(payload: Row, user: Row, sheets: Row) => void>;
panels: Array<(user: () => Row, setUser: (next: Row) => void) => HTMLElement>;
} = { builders: {}, extend: [], panels: [] };
/** 201-1 호표 묶음 레벨(보기 전용) — 머리 · 합계(제목 줄) = 1 · 그 밖 = 2 · key 열을 들여씀 */
export const blockLevel = (key: string, top: (index: number) => boolean): TabTable["level"] => ({
key,
of: (i) => (top(i) ? 1 : 2),
});
/* ── 열 꼴 ────────────────────────────────────────────────────────────── */
export const text = (key: string, label: string, width?: number, group?: string[]): DataColumn => ({
key,
label,
width,
group,
});
export const money = (key: string, label: string, group?: string[], decimals = 0): DataColumn => ({
key,
label,
group,
kind: "number",
decimals,
width: 96,
});
export const qty = (key: string, label: string, decimals = 4, group?: string[]): DataColumn => ({
key,
label,
group,
kind: "number",
decimals,
trim: true,
width: 76,
});
/** 글 → 값 · 빈칸 = null · 수가 아니면 undefined(무시) */
export function parseNumber(value: string): number | null | undefined {
const t = value.replace(/,/g, "").trim();
if (!t) return null;
const v = Number(t);
return Number.isFinite(v) ? v : undefined;
}
/** 줄 키로 줄 번호 찾기(이동) */
export function rowIndex(rows: DataRow[], key: string): number {
return rows.findIndex((row) => row.key === key);
}