From 5f8f909b650fa4e43a9bcbf26c05a1e6ae41c46d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 13:19:34 +0900 Subject: [PATCH] =?UTF-8?q?fix(731-1):=20=EC=9D=BC=EC=9C=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80=ED=91=9C=20=EC=A4=84=20=EB=81=9D=20=E2=8B=AF=20?= =?UTF-8?q?=EB=8B=A8=EC=B6=94=20=E2=80=94=20=EC=9A=B0=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=C2=B7=20=EA=B8=B8=EA=B2=8C=20=EB=88=84=EB=A6=84=20=EC=97=86?= =?UTF-8?q?=EC=9D=B4=EB=8F=84=20=EC=9C=84=EB=A1=9C=20=C2=B7=20=EC=95=84?= =?UTF-8?q?=EB=9E=98=EB=A1=9C=20=C2=B7=20=EB=B3=84=EC=B9=AD=20=C2=B7=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 마우스 우클릭 · 길게 누름 삭제는 개인 · 마스터 층 모두 이미 되었음(재현 0) — 삭제 길이 눈에 안 보이는 것이 까닭 - 공용 표(ui_template_data_table)에 rowMenu 옵션 추가(첫 칸 오른쪽 ⋯ · 누르면 줄 누름 없이 메뉴만) · 일위대가표가 같은 메뉴를 연결 - 지킴 시험 test_ui_data_table 에 rowMenu 한 건 Co-Authored-By: Claude Sonnet 5.5 --- .../M02_MasterTemplete_Labor_Table.ts | 9 ++++++++- resources/tester/helper_ui_data_table.cjs | 18 ++++++++++++++++++ resources/tester/test_ui_data_table.py | 12 ++++++++++++ ui_template/ui_template_data_table.css | 14 ++++++++++++++ ui_template/ui_template_data_table.ts | 18 +++++++++++++++++- 5 files changed, 69 insertions(+), 2 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts index a28183dc6..96516971d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts @@ -5,7 +5,7 @@ * · 옵션 값 = 규격 칸 · 「빈 값 n」 = 비고 칸 붉게(입력 빈 로직 줄) * · 수량 칸 = 확인용 임시 입력(723-1 · 휘발 · 문서 · 저장 · dirty 안 건드림) → 그 줄 금액 · 합계 미리 보기 * (짝 셈 `Labor_Calc.settle` · 서버 안 부름) · 문서 안 줄 수량은 늘 null(쓰는 쪽 알약으로 이음) - * · 머리 단추 = [로직 추가] [자재 추가] · 줄 우클릭 = 위로 · 아래로 · 별칭 · 삭제 + * · 머리 단추 = [로직 추가] [자재 추가] · 줄 우클릭 · 줄 끝 ⋯ = 위로 · 아래로 · 별칭 · 삭제 * · 줄 누름 = 줄 모달(`Labor_RowModal` — 옵션 · 메모 · 켜쌓기율 · 로직 자재 바꾸기 · 세 칸 · 단가산출) · * 사전조합 참조 줄 = 서고 원본 모달(`Labor_RefEdit`) · 사용자 자재 줄 = 자재 모달(세 칸) * · 금액 = 서버 calc(최신 응답만 · 저장 안 함) · [저장]은 UI_Main(서버가 수량 걷고 다시 셈) @@ -155,6 +155,13 @@ export function mountLaborTable( const n = at(i); if (n !== null) openAt(n); }, + // 줄 끝 ⋯(731-1) — 우클릭 · 길게 누름이 없어도 같은 메뉴(위로 · 아래로 · 별칭 · 삭제) + rowMenu: edit + ? { + show: (i) => at(i) !== null, + open: (i, ev) => openTreeMenu(ev, menuAt(at(i) ?? 0)), + } + : undefined, editable: (i, key) => { const n = at(i); if (key !== "qty" || n === null) return false; diff --git a/resources/tester/helper_ui_data_table.cjs b/resources/tester/helper_ui_data_table.cjs index 65a334c15..39df9c92f 100644 --- a/resources/tester/helper_ui_data_table.cjs +++ b/resources/tester/helper_ui_data_table.cjs @@ -96,6 +96,24 @@ console.log( total: { x: 3 }, rowClass: (i) => (i === 0 ? "is-title" : null), }), + rowMenu: m.renderDataTableHtml({ + columns: [ + { key: "a", label: "A" }, + { key: "b", label: "B" }, + ], + rows: [ + { a: "제목", b: 1 }, + { a: "줄", b: 2 }, + { a: "줄2", b: 3 }, + ], + onRowClick: () => undefined, + rowMenu: { show: (i) => i > 0, open: () => undefined }, + }), + noRowMenu: m.renderDataTableHtml({ + columns: [{ key: "a", label: "A" }], + rows: [{ a: "줄" }], + onRowClick: () => undefined, + }), scale: m.SCALE, widths: m.renderDataTableHtml({ columns: [ diff --git a/resources/tester/test_ui_data_table.py b/resources/tester/test_ui_data_table.py index 6b61be801..7467f92ce 100644 --- a/resources/tester/test_ui_data_table.py +++ b/resources/tester/test_ui_data_table.py @@ -122,3 +122,15 @@ def test_155_2_줄_꼴_곁값(r): assert rc.count("is-title") == 1 # 합계 줄엔 안 붙음 assert "white-space-collapse: preserve-spaces;" in r["css"] assert "tr.is-title td" in r["css"] + + +def test_줄_끝_메뉴_단추_731(r): + """rowMenu — show 참인 줄의 첫 칸에만 ⋯ 단추 · 없으면 단추 없음 · 줄 번호가 data-more 로.""" + rows = r["rowMenu"].split("")[1].split("")[0].split("")[:3] + assert "data-more" not in rows[0] + for i in (1, 2): + assert rows[i].count("data-more") == 1 + assert f'data-more="{i}"' in rows[i] + assert rows[i].split("")[0].count("data-more") == 1 # 첫 칸 안 + assert 'class="ui-icon-btn ui-icon-btn--ghost ui-data-table__more"' in rows[1] + assert "data-more" not in r["noRowMenu"] diff --git a/ui_template/ui_template_data_table.css b/ui_template/ui_template_data_table.css index 4d404a9ba..b36eb7fce 100644 --- a/ui_template/ui_template_data_table.css +++ b/ui_template/ui_template_data_table.css @@ -330,3 +330,17 @@ :is(.ui-data-table__icon, .ui-data-table__lamp)[hidden] { display: none !important; } + +/* 줄 끝 ⋯(731-1) — 첫 칸 오른쪽 가운데에 얹음(줄 높이를 안 밀음) · 글은 단추 자리만큼 비워 줄임표 */ +.ui-data-table__table td:has(> .ui-data-table__more) { + padding-right: calc(var(--ui-size-h, 28px) + var(--spacing-4)); +} +.ui-data-table__table td:has(> .ui-data-table__more):not(.is-frozen) { + position: relative; /* 고정 칸은 이미 sticky(자리 기준) */ +} +.ui-data-table__table td > .ui-data-table__more { + position: absolute; + top: 50%; + right: var(--spacing-4); + transform: translateY(-50%); +} diff --git a/ui_template/ui_template_data_table.ts b/ui_template/ui_template_data_table.ts index eb64b3e3a..48189b004 100644 --- a/ui_template/ui_template_data_table.ts +++ b/ui_template/ui_template_data_table.ts @@ -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 => + ``; const ESC: Record = { "&": "&", "<": "<", ">": ">", '"': """ }; const esc = (s: string): string => s.replace(/[&<>"]/g, (c) => ESC[c]); @@ -228,7 +235,8 @@ function rowHtml( lv?.key === key && lv.tree !== undefined ? `${lv.node ? TREE_ICON.folder : TREE_ICON.leaf}` : ""; - tr += `${guide}${icon}${esc(txt)}`; + const menu = j === 0 && more.menu && index !== null ? MORE_BUTTON(index) : ""; + tr += `${menu}${guide}${icon}${esc(txt)}`; } 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("[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];