fix(731-1): 일위대가표 줄 끝 ⋯ 단추 — 우클릭 · 길게 누름 없이도 위로 · 아래로 · 별칭 · 삭제

- 마우스 우클릭 · 길게 누름 삭제는 개인 · 마스터 층 모두 이미 되었음(재현 0) — 삭제 길이 눈에 안 보이는 것이 까닭
- 공용 표(ui_template_data_table)에 rowMenu 옵션 추가(첫 칸 오른쪽 ⋯ · 누르면 줄 누름 없이 메뉴만) · 일위대가표가 같은 메뉴를 연결
- 지킴 시험 test_ui_data_table 에 rowMenu 한 건

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 13:19:34 +09:00
co-authored by Claude Sonnet 5.5
parent 5afe82d430
commit 5f8f909b65
5 changed files with 69 additions and 2 deletions
+17 -1
View File
@@ -21,6 +21,8 @@
* 칸 누른 뒤 Enter · F2 = 그 칸 입력 시작(PLAN 243-1 · keyEdit false = 표 밖이 맡음)
* 같은 값 세로 병합(PLAN 201-1) = mergeSame [열] → 위아래로 같은 값이 이어지면 한 칸(rowSpan) · 본문만
* 앞 병합 열이 갈리면 뒤 병합 열도 갈림 · 빈 값 · 고칠 칸은 안 합침 · 칸 → 열 = `cellColumn`(병합 표는 data-c)
* 줄 끝 메뉴(731-1) = rowMenu { show(줄), open(줄, 누름) } → show 참인 줄의 첫 칸 오른쪽에 ⋯ 단추(우클릭 · 길게 누름이 없는 손가락 · 마우스도 보이는 길) ·
* 누르면 줄 누름 없이 open 만 부름(메뉴는 부르는 쪽이 엶) · 단추 꼴 = 아이콘 단추(28 · ghost) 그대로(DOM 없는 시험도 같은 글)
* 칸 램프(557-2) = cellClass 가 `ui-data-table__lamp` → 칸 글 앞 점 8 · 노랑 = 없음 · `is-linked` = 초록 ✓(색만으로 안 가름) ·
* `ui-data-table__lamp--only` = 글 숨기고 점만 가운데 · 대신하는 옛 변종 = B08 `b08-summary__lamp` · `b08-milestone__lamp` · B09 `b09-bill__lamp`
* ========================================================================== */
@@ -65,6 +67,8 @@ export interface DataTableOptions {
onRowClick?: (index: number) => void;
/** 칸 누름(본문 줄만) — 주면 줄에 `data-i`·`is-clickable` 이 붙음 · 줄 누름보다 먼저 부름 */
onCellClick?: (index: number, key: string) => void;
/** 줄 끝 ⋯ 메뉴(731-1) — show 참인 줄의 첫 칸에 ⋯ 단추 · 누르면 줄 누름 없이 open(줄 · 누름 이벤트) */
rowMenu?: { show: (index: number) => boolean; open: (index: number, event: MouseEvent) => void };
/** 강조할 줄(`focusRow` 가 정함) */
focus?: number | null;
/** 고칠 칸인지(본문 줄만 · 합계 제외) — 참이면 칸이 글 상자 */
@@ -113,6 +117,9 @@ export const TREE_ICON = {
};
const EMPTY = "-";
/** ⋯ 단추(731-1) — 아이콘 단추(`createIconButton` icon "dots" · 28 · ghost)와 같은 글 · 표 글은 DOM 없이 만들어서 글로 둠 */
const MORE_BUTTON = (i: number): string =>
`<button type="button" class="ui-icon-btn ui-icon-btn--ghost ui-data-table__more" data-size="28" data-more="${i}" title="더 보기" aria-label="더 보기"><span class="ui-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/><path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/><path d="M19 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/></svg></span></button>`;
const ESC: Record<string, string> = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" };
const esc = (s: string): string => s.replace(/[&<>"]/g, (c) => ESC[c]);
@@ -228,7 +235,8 @@ function rowHtml(
lv?.key === key && lv.tree !== undefined
? `<span class="ui-data-table__icon">${lv.node ? TREE_ICON.folder : TREE_ICON.leaf}</span>`
: "";
tr += `<td${attrs}>${guide}${icon}${esc(txt)}</td>`;
const menu = j === 0 && more.menu && index !== null ? MORE_BUTTON(index) : "";
tr += `<td${attrs}>${menu}${guide}${icon}${esc(txt)}</td>`;
}
const cls = [alt && "is-alt", index !== null && "is-clickable", focus && "is-focus", extra]
.filter(Boolean)
@@ -247,6 +255,8 @@ interface RowMore {
spans?: number[];
/** 고칠 수 있는 칸인지(editOnDouble · 글로 그린 칸에 `is-can-edit` 표시 · 258-2) */
can?: (key: string) => boolean;
/** 줄 끝 ⋯ 단추(731-1) — 첫 칸 앞에(오른쪽 맞춤 · 글 앞에 놓아야 한 줄에 앉음) */
menu?: boolean;
}
/** 칸 → 열 번호 — 병합 표는 칸이 빠져 cellIndex 가 밀리므로 data-c */
@@ -363,6 +373,7 @@ export function renderDataTableHtml(o: DataTableOptions, win?: RowWindow): strin
...(o.level ? { lv: { key: o.level.key, n: lvN, node, tree: tree?.[i] } } : {}),
...(spans ? { spans: spans[i - from] } : {}),
...(o.editOnDouble && o.editable ? { can: (key: string) => o.editable!(i, key) } : {}),
...(o.rowMenu?.show(i) ? { menu: true } : {}),
},
);
}
@@ -492,6 +503,11 @@ export function createDataTable(opts: DataTableOptions): DataTable {
let lastKey = "";
let lastRow: number | null = null; // 마지막 누른 줄 — Enter · F2 입력 시작 자리
root.addEventListener("click", (event) => {
const more = (event.target as Element).closest<HTMLElement>("[data-more]");
if (more && root.contains(more)) {
state.rowMenu?.open(Number(more.dataset.more), event); // 줄 누름 없이 메뉴만(731-1)
return;
}
const at = cellAt(event.target);
if (!at) return;
lastKey = at[1];