feat(table): B08 · B09 표 수기 입력 = 더블클릭 · Enter · F2 · 고칠 칸 테두리 늘 표시 (243-1)

- 공용 표: 고칠 수 있는 칸(editOnDouble) td 에 is-can-edit 입력 칸 테두리 · 칸 누른 뒤 Enter · F2 = 그 칸 입력(keyEdit)
- B09 Shell 표 만드는 길 한 곳 = editOnDouble 기본 · 설계내역서는 레벨 키 몫
- B08 자재 집계표 셋 = editOnDouble
- 새 시험 test_243_1 · 235_1 Shell 배선 기대값 갱신

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 20:35:08 +09:00
co-authored by Claude Sonnet 5.5
parent 4689576d7d
commit e3266a4eaa
6 changed files with 169 additions and 5 deletions
+22 -3
View File
@@ -17,6 +17,8 @@
* 나무(PLAN 235-1 · tree) = 접히지 않는 나무 안내선(├ is-tee · └ is-end · │ is-bar) + 폴더 · 파일 아이콘(공용 나무 꼴)
* 더블클릭 입력(PLAN 235-1 · editOnDouble) = 고칠 칸도 글로 그림 · 더블클릭 · t.edit(줄) = 그 칸만 글 상자 ·
* Enter(조합 끝난 뒤) · 초점 떠남 = 입력 끝(onEdit) — 한 번 누름 = 줄 고름 · 끌기
* 고칠 수 있는 칸 = 글일 때도 칸에 입력 칸 테두리 class `is-can-edit`(PLAN 243-1) ·
* 칸 누른 뒤 Enter · F2 = 그 칸 입력 시작(keyEdit false = 표 밖이 맡음)
* 같은 값 세로 병합(PLAN 201-1) = mergeSame [열] → 위아래로 같은 값이 이어지면 한 칸(rowSpan) · 본문만
* 앞 병합 열이 갈리면 뒤 병합 열도 갈림 · 빈 값 · 고칠 칸은 안 합침 · 칸 → 열 = `cellColumn`(병합 표는 data-c)
* ========================================================================== */
@@ -84,6 +86,8 @@ export interface DataTableOptions {
};
/** 참이면 고칠 칸 = 글 · 더블클릭 · edit() 때만 글 상자(235-1) */
editOnDouble?: boolean;
/** 칸 누른 뒤 표에서 Enter · F2 = 그 칸 입력 시작(editOnDouble · 기본 참 · 거짓 = 표 밖이 키를 맡음) */
keyEdit?: boolean;
/** 지금 글 상자인 칸(editOnDouble · 표가 정함) */
editing?: { index: number; key: string } | null;
/** 같은 값 세로 병합할 열 키(표마다 지정) — 위아래로 같은 글이 이어지면 rowSpan 한 칸 */
@@ -180,7 +184,7 @@ function rowHtml(
more: RowMore = {},
): string {
let tr = "";
const { lv, spans } = more;
const { lv, spans, can } = more;
for (let j = 0; j < cols.length; j++) {
const key = cols[j].key;
const span = spans?.[j] ?? 1;
@@ -206,6 +210,7 @@ function rowHtml(
frz && left[j + 1] == null && "is-edge",
empty && "is-empty",
cellClass?.(key),
can?.(key) && "is-can-edit",
lvc.trim(),
span > 1 && "is-merged",
]
@@ -238,6 +243,8 @@ function rowHtml(
interface RowMore {
lv?: { key: string; n: number; node: boolean; tree?: string };
spans?: number[];
/** 고칠 수 있는 칸인지(editOnDouble · 글로 그린 칸에 `is-can-edit` 표시) */
can?: (key: string) => boolean;
}
/** 칸 → 열 번호 — 병합 표는 칸이 빠져 cellIndex 가 밀리므로 data-c */
@@ -353,6 +360,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) } : {}),
},
);
}
@@ -478,10 +486,12 @@ export function createDataTable(opts: DataTableOptions): DataTable {
return [Number(row.dataset.i), column?.key ?? ""];
};
let lastKey = "";
let lastRow: number | null = null; // 마지막 누른 줄 — Enter · F2 입력 시작 자리
root.addEventListener("click", (event) => {
const at = cellAt(event.target);
if (!at) return;
lastKey = at[1];
lastRow = at[0];
if (at[1]) state.onCellClick?.(at[0], at[1]);
state.onRowClick?.(at[0]);
});
@@ -505,9 +515,18 @@ export function createDataTable(opts: DataTableOptions): DataTable {
if (at && state.editable?.(at[0], at[1])) edit(at[0], at[1]);
});
// Enter = 입력 끝(한글 조합 중 Enter 는 조합 끝) — 표에 초점 · 키가 문서까지 가지 않게(다시 입력 시작 방지)
// 표에 초점이 있을 때 Enter · F2 = 누른 칸 입력 시작(keyEdit)
root.addEventListener("keydown", (event) => {
if (event.key !== "Enter" || event.isComposing) return;
if (!(event.target instanceof HTMLInputElement)) return;
if ((event.key !== "Enter" && event.key !== "F2") || event.isComposing) return;
if (!(event.target instanceof HTMLInputElement)) {
if (opts.keyEdit === false || event.target !== root || lastRow === null) return;
if (state.editable?.(lastRow, lastKey)) {
event.preventDefault();
edit(lastRow, lastKey);
}
return;
}
if (event.key !== "Enter") return;
event.preventDefault();
event.stopPropagation();
root.focus();