From 084b52001e6c1dd7b60655c90d376c0b3c55a7f1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 8 Oct 2026 12:07:39 +0900 Subject: [PATCH] =?UTF-8?q?feat(M02):=20=EB=8B=A8=EA=B0=80=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=EB=A1=9C=EC=A7=81=20=EC=A4=84=20=EC=88=9C=EC=84=9C?= =?UTF-8?q?=20=E2=80=94=20=E2=96=B2=E2=96=BC=20=EC=9E=91=EC=9D=80=20?= =?UTF-8?q?=EB=8B=A8=EC=B6=94=20+=20=EC=86=90=EC=9E=A1=EC=9D=B4=20?= =?UTF-8?q?=EB=81=8C=EA=B8=B0=20=C2=B7=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=95=A9=20=EC=B9=B4=EB=93=9C=EB=8F=84(85-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4 --- .../M02_MasterTemplete_Labor.css | 28 ++++++++- .../M02_MasterTemplete_Labor_Detail.ts | 10 ++-- .../M02_MasterTemplete_Labor_Rows.ts | 59 +++++++++++++++++-- .../M02_MasterTemplete_Labor_Text.ts | 1 + resources/tester/test_m02_card_shell.py | 7 ++- 5 files changed, 92 insertions(+), 13 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.css b/M02_MasterTemplete/M02_MasterTemplete_Labor.css index bf59b0c4f..e40258c2b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor.css @@ -90,7 +90,33 @@ white-space: nowrap; } -/* 순서 칸 2행(51-14) — 위 [위로] [아래로] / 아래 [옵션]·[수정] [삭제] · 줄 사이도 단추 묶음 간격 */ +/* 순서(85-3) — 손잡이 + ▲▼ 작은 단추 · 끌어 놓을 줄 위 선 */ +.m02-labrow__order { + display: inline-flex; + align-items: center; + gap: 2px; +} + +.m02-labrow__arrow.ui-btn { + min-width: 0; + height: 20px; + padding: 0 5px; + font-size: 10px; + line-height: 1; +} + +.m02-labrow__grip { + padding: 0 4px; + color: var(--color-text-secondary); + cursor: grab; + user-select: none; +} + +.m02-labrow__row.is-dragover td { + box-shadow: inset 0 2px 0 var(--color-primary); +} + +/* 순서 칸 2행(51-14) — 위 손잡이 ▲▼ / 아래 [옵션]·[수정] [삭제] · 줄 사이도 단추 묶음 간격 */ .m02-labrow__tools { display: inline-flex; flex-direction: column; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index 1b4bb5333..5f9415e7b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -186,7 +186,7 @@ export function mountLaborDetail( option: openRow, ref: openRef, qty: openQty, - ...(opt.card ? {} : { move }), + move: reorder, remove: removeRow, alias: setAlias, } @@ -310,10 +310,10 @@ export function mountLaborDetail( } }; - function move(i: number, d: number): void { - const j = i + d; - if (j < 0 || j >= cur.담은로직.length) return; - [cur.담은로직[i], cur.담은로직[j]] = [cur.담은로직[j], cur.담은로직[i]]; + /** 줄 순서(85-3) — from 줄을 to 자리로(▲▼ = 옆 자리 · 끌기 = 놓은 줄 자리) */ + function reorder(from: number, to: number): void { + if (from === to || to < 0 || to >= cur.담은로직.length) return; + cur.담은로직.splice(to, 0, ...cur.담은로직.splice(from, 1)); changed(); } /** 줄 고름(47-5) — 하이라이트 + 하위 행(51-2 · 표만 다시 그림 · 다이어그램 안 다시 그림) + 원문 패널(47-4) · diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts index 4a9d61940..262c577cd 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts @@ -148,6 +148,9 @@ function moneyCell( } /** 이름 칸 글마다 title = 전체 글(한 줄 말줄임 · 45-7) */ +/** 끌고 있는 줄 차례(85-3) — 손잡이 dragstart ~ drop */ +let dragFrom: number | null = null; + const titled = (td: HTMLElement): HTMLElement => { for (const c of td.children) c.setAttribute("title", c.textContent ?? ""); return td; @@ -179,7 +182,8 @@ export interface RowHandlers { ref?: (i: number) => void; /** 수량산출 칸 누름(손 입력 · 엑셀에서 고르기) — 없으면 줄 누름과 같음 */ qty?: (i: number) => void; - move?: (i: number, d: number) => void; + /** 줄 순서(85-3) — from 줄을 to 자리로(▲▼ = 옆 자리 · 손잡이 끌기 = 놓은 줄 자리) */ + move?: (from: number, to: number) => void; remove?: (i: number) => void; /** 별칭 칸 고침(44-6 · 글자마다 · 빈 글 = null) — 없으면 글만 보임 */ alias?: (i: number, value: string | null) => void; @@ -324,6 +328,32 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement run(); }, }); + // 순서(85-3) — ▲▼ 작은 단추(글은 title · aria) + 손잡이 끌기(줄 전체는 안 끎 · 별칭 칸 글 고르기) + const arrow = (mark: string, label: string, to: number): HTMLElement => { + const b = small(mark, () => move?.(i, to)); + b.classList.add("m02-labrow__arrow"); + b.title = label; + b.setAttribute("aria-label", label); + if (to < 0 || to >= data.rows.length) (b as HTMLButtonElement).disabled = true; + return b; + }; + const grip = (): HTMLElement => { + const g = el("span", { + className: "m02-labrow__grip", + text: "⠿", + attrs: { draggable: "true", title: tc("Labor_Drag"), "data-labor-grip": String(i) }, + }); + g.addEventListener("click", (e) => e.stopPropagation()); + g.addEventListener("dragstart", (ev) => { + dragFrom = i; + ev.dataTransfer?.setData("text/plain", String(i)); + if (ev.dataTransfer) ev.dataTransfer.effectAllowed = "move"; + }); + g.addEventListener("dragend", () => { + dragFrom = null; + }); + return g; + }; const sel = data.selected === id ? " is-selected" : ""; const tr = el("tr", { className: (on ? "m02-labrow__row is-open" : "m02-labrow__row") + sel, @@ -373,10 +403,14 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement children: [ ...(move ? [ - group([ - small(tc("Up"), () => move(i, -1)), - small(tc("Down"), () => move(i, 1)), - ]), + el("div", { + className: "m02-labrow__order", + children: [ + grip(), + arrow("▲", tc("Up"), i - 1), + arrow("▼", tc("Down"), i + 1), + ], + }), ] : []), group([ @@ -403,6 +437,21 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement ], }); if (on) tr.addEventListener("click", () => on.open(i)); + if (move) { + tr.addEventListener("dragover", (ev) => { + if (dragFrom === null || dragFrom === i) return; + ev.preventDefault(); + tr.classList.add("is-dragover"); + }); + tr.addEventListener("dragleave", () => tr.classList.remove("is-dragover")); + tr.addEventListener("drop", (ev) => { + ev.preventDefault(); + tr.classList.remove("is-dragover"); + const from = dragFrom; + dragFrom = null; + if (from !== null && from !== i) move(from, i); + }); + } const subs = sel ? subRows(line, wait, tools) : []; if (!note) return [tr, ...subs]; // 옵션 값 줄(44-6) — 이름 ~ 합계 칸에 걸친 한 줄(말줄임 · 전체 글은 title) · 행 높이 안 커짐 diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index 28d5b4286..abdc2f6bc 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -33,6 +33,7 @@ const TEXT = { Col_Move: ["순서", "Move"], Up: ["위로", "Up"], Down: ["아래로", "Down"], + Labor_Drag: ["끌어 순서 바꿈", "Drag to reorder"], Remove: ["삭제", "Delete"], Labor_Option: ["옵션", "Options"], Labor_Edit: ["수정", "Edit"], diff --git a/resources/tester/test_m02_card_shell.py b/resources/tester/test_m02_card_shell.py index 6b03b170e..bed5215ae 100644 --- a/resources/tester/test_m02_card_shell.py +++ b/resources/tester/test_m02_card_shell.py @@ -78,7 +78,7 @@ def test_일위대가_카드는_상세_카드_갈래(): assert "onChanged: (dirty) => laborCard.saver.mark(dirty && opts.editable())," in cards assert "Labor_View" not in cards # 한 줄 표(40-2) 안 씀 detail = _text("M02_MasterTemplete_Labor_Detail.ts") - assert "...(opt.card ? {} : { move })" in detail # 순서 단추 없음 · 빼기는 그대로 + assert "move: reorder," in detail # 85-3 — 카드도 순서(▲▼ · 끌기) · 빼기는 그대로 # 서고 모드(50-3 · lib)도 더하기 · 다이어그램 없음 # 51-11 사전조합 = 일위대가 상세와 같은 [로직 추가] · 같은 목록(걸러냄 없음) assert "opt.card || promote ? null : addLogic" in detail @@ -134,7 +134,10 @@ def test_일위대가_줄_고름과_옵션_단추(): ) # 읽기만도 고름 · [옵션] = 보기 모달 assert "selected: picked" in detail rows = _text("M02_MasterTemplete_Labor_Rows.ts") - up, down = rows.index('small(tc("Up")'), rows.index('small(tc("Down")') + # 85-3 — ▲▼ 작은 단추(글은 title · aria) + 손잡이 끌기 + up, down = rows.index('arrow("▲", tc("Up")'), rows.index('arrow("▼", tc("Down")') + assert rows.index("grip(),") < up and 'b.setAttribute("aria-label", label)' in rows + assert "move: reorder," in detail and "opt.card ? {} : { move }" not in detail # 카드도 순서 opt, rm = ( rows.index('small(tc("Labor_Option")'), rows.index('small(tc("Remove"), () => remove(i), "danger")'),