feat(M02): 구조물 조합 표 — 머리 이름 고침 = 나무 이름 바꾸기 · 단위 줄 숨김 · 열 폭 자동 · 마지막 분류 칸 강조 · 안내글 없앰 (PLAN 31-1)

- 표 부품 combo 선택(구조물 조합 표만): 단위 줄 · 옛 안내글 없음 · 열 폭 = 머리 이름 · 일위대가 키 · 도번 등 번호가 한 줄에 들게(저장 폭 안 씀 · 번호 줄 줄바꿈 없음) · 고른 열의 가장 깊은 이름 칸 강조(나무에서 고를 때도) · 방향키가 숨은 단위 줄을 안 밟음
- 머리 이름 칸 두 번 누르기 · F2 · Enter → headRenameEdit(가장 깊은 칸 = 항목 · 위 칸 = 폴더) → 나무 editFromTable(메뉴 이름 바꾸기와 같은 길 — 저장 물음 · 같은 이름 막음 안내 · 다시 그림) · 쓸 수 있는 층만
- 시험 test_m02_combo_table 3 · test_m02_combo_screen 머리 잠금 기대값

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
This commit is contained in:
2026-09-30 20:13:18 +09:00
co-authored by Claude Opus 5.5
parent 3913258df9
commit f7c91f2f12
9 changed files with 211 additions and 14 deletions
+22 -4
View File
@@ -47,6 +47,10 @@ export interface RenderState {
pick?: boolean;
/** 머리 · 층 이름을 못 고침(`lockHead`) — 칸 겉모양만. */
headLocked?: boolean;
/** 구조물 조합 표(`combo`) — 단위 줄 없음 · 열 폭 = 머리 · 번호 글이 한 줄에 들게(저장 폭 안 씀) ·
* `headEditable` = 머리 이름 칸을 고칠 수 있음(나무 이름 바꾸기로). */
combo?: boolean;
headEditable?: boolean;
}
/** 도면 · 면적산출 · 재료산출 줄 칸 글. */
@@ -64,6 +68,18 @@ const columnWidth = (doc: SheetDoc, col: SheetColumn, mins: Map<string, number>)
mins.get(col.id) ?? 0,
);
/** 구조물 조합 — 일위대가 · 도면 · 면적산출 · 재료산출 줄 글(키 · 번호)이 줄바꿈 없이 들게 최소 폭을 넓힘
* (`.ui-sheet__meta td` 글꼴과 같게 잼). */
function fitMeta(state: RenderState, cols: SheetColumn[], mins: Map<string, number>): void {
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
const font = `400 ${rem * 0.76}px ${getComputedStyle(document.body).fontFamily}`;
for (const col of cols) {
const texts = [col.일위대가 ?? "", ...REF_ROWS.map((key) => refText(state, key, col.id))];
const need = Math.ceil(Math.max(...texts.map((t) => textWidth(t, font)))) + 14;
mins.set(col.id, Math.max(mins.get(col.id) ?? 0, need));
}
}
/** 머리 칸 글꼴 — 폭 재기용(`.ui-sheet__table thead` 와 같게). */
function headFont(): string {
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
@@ -97,6 +113,8 @@ export function renderSheet(state: RenderState): HTMLElement {
const names = columnNames(cols);
const font = headFont();
const mins = headMinWidths(cols, depth, font);
if (state.combo) fitMeta(state, cols, mins);
const widthDoc = state.combo ? { ...doc, 보기: { ...doc.보기, 열너비: {} } } : doc;
// 줄 머리 칸 폭 — 층 이름 · 단위 · 일위대가 · 도면 · 면적산출 · 재료산출 · 합계 이름이 안 잘리게
const GUTTER = Math.min(
GUTTER_MAX,
@@ -120,7 +138,7 @@ export function renderSheet(state: RenderState): HTMLElement {
let left = GUTTER;
for (const col of cols) {
lefts.push(left);
left += columnWidth(doc, col, mins);
left += columnWidth(widthDoc, col, mins);
}
const freeze = (cell: HTMLElement, from: number, span = 1): void => {
@@ -196,7 +214,7 @@ export function renderSheet(state: RenderState): HTMLElement {
const thead = (): HTMLElement => {
const rows = head.map((cells, level) => {
const heads = mode === "master" && !state.headLocked;
const heads = mode === "master" && (!state.headLocked || !!state.headEditable);
const layer = gutter("th", doc.층?.[level] ?? "", heads ? "is-editable" : "");
layer.dataset.layer = String(level);
const tr = el("tr", { children: [layer] });
@@ -227,7 +245,7 @@ export function renderSheet(state: RenderState): HTMLElement {
th.append(el("span", { className: "ui-sheet__resize", attrs: { "data-resize": col.id } }));
unit.append(th);
});
return el("thead", { children: [...rows, unit] });
return el("thead", { children: state.combo ? rows : [...rows, unit] });
};
const totalRows = (): HTMLElement[] =>
@@ -251,7 +269,7 @@ ${formulaLabel(formula, names)}`
el("col", { attrs: { style: `width:${GUTTER}px` } }),
...cols.map((col) =>
el("col", {
attrs: { style: `width:${columnWidth(doc, col, mins)}px`, "data-col": col.id },
attrs: { style: `width:${columnWidth(widthDoc, col, mins)}px`, "data-col": col.id },
}),
),
],