From a7cc8a304e2c3b8c868043debf1e70ba9658e029 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 13 Oct 2026 13:45:53 +0900 Subject: [PATCH] =?UTF-8?q?feat(device):=20=EB=8B=A8=EC=B6=95=ED=82=A4?= =?UTF-8?q?=EB=A1=9C=EB=A7=8C=20=EB=90=98=EB=8D=98=20=EC=9D=BC=EC=97=90=20?= =?UTF-8?q?=ED=84=B0=EC=B9=98=20=EB=8B=A8=EC=B6=94=20=E2=80=94=20=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=85=EB=A0=A5=EC=A4=84=20=EB=8B=A8=EC=B6=94=20?= =?UTF-8?q?=EC=A4=84(=EC=B0=BE=EA=B8=B0=20=C2=B7=20=EB=AA=A8=EB=91=90=20?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20=EA=B0=92=20=C2=B7=20=EC=A4=84=EB=B0=94?= =?UTF-8?q?=EA=BF=88=20=C2=B7=20=EA=B3=A0=EC=B9=A8=20=EC=B7=A8=EC=86=8C=20?= =?UTF-8?q?=C2=B7=20=EC=98=A4=EB=8A=98=20=C2=B7=20$=20=EB=8F=8C=EB=A6=AC?= =?UTF-8?q?=EA=B8=B0)=20=C2=B7=20B05=20=EC=9E=90=EC=9C=A0=EC=A7=80?= =?UTF-8?q?=EC=A0=95=20[=EC=A0=90=20=EB=B9=BC=EA=B8=B0]=20[=EB=81=9D]=20?= =?UTF-8?q?=C2=B7=20B06=20=EC=83=81=EC=84=B8=20=C2=B7=20M02=20=EB=82=98?= =?UTF-8?q?=EB=AC=B4=20[=EC=97=AC=EB=9F=BF]=20=C2=B7=20=EA=B3=B5=EC=9A=A9?= =?UTF-8?q?=20.ui-touch-only=20(=EA=B8=B0=EA=B8=B0=EB=8C=80=EC=9D=91=204-3?= =?UTF-8?q?)?= 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_01Ji4nuyLVHcxrXWHTdb5nRD --- A00_Common/device_context.css | 5 +++ A00_Common/spreadsheet/spreadsheet.ts | 2 + .../spreadsheet/spreadsheet_toolbar.css | 11 +++++ .../spreadsheet/spreadsheet_touch_keys.ts | 43 +++++++++++++++++++ B05_Profile/B05_Profile_UI_Profile_Tools.ts | 9 +++- .../B06_Section_UI_Cross_Detail_Select.ts | 20 ++++++++- .../M02_MasterTemplete_TreePanel.ts | 18 +++++++- resources/tester/test_spreadsheet_touch.py | 20 ++++++++- 8 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 A00_Common/spreadsheet/spreadsheet_touch_keys.ts diff --git a/A00_Common/device_context.css b/A00_Common/device_context.css index a636ae88b..cb33787e3 100644 --- a/A00_Common/device_context.css +++ b/A00_Common/device_context.css @@ -76,3 +76,8 @@ html:is([data-size="compact"], [data-size="medium"]) > :first-child { z-index: 3; } + +/* 터치 모드에서만 보이는 단추(자판 없는 기기용 · 4-3) — 마우스 화면엔 없음 */ +html:not([data-touch="on"]) .ui-touch-only { + display: none !important; +} diff --git a/A00_Common/spreadsheet/spreadsheet.ts b/A00_Common/spreadsheet/spreadsheet.ts index 40ab62717..27ac5da6a 100644 --- a/A00_Common/spreadsheet/spreadsheet.ts +++ b/A00_Common/spreadsheet/spreadsheet.ts @@ -16,6 +16,7 @@ import { parseA1, rangeToA1, toA1 } from "./spreadsheet_address"; import { attachClipboard, pasteTableAt, type PasteCheck } from "./spreadsheet_clipboard"; import { applyCommand, CommandError } from "./spreadsheet_commands"; import { createEditor, slotOf } from "./spreadsheet_editor"; +import { mountTouchKeys } from "./spreadsheet_touch_keys"; import { toCalcValue } from "./spreadsheet_eval"; import { attachExtras } from "./spreadsheet_extras"; import { createCalcEngine } from "./spreadsheet_graph"; @@ -161,6 +162,7 @@ export function createSpreadsheet( const extras = attachExtras(ctx, ed, toolbar?.root ?? null); others.push(...extras.parts); const detachKeys = attachKeys(ctx, ed, extras.keys); + mountTouchKeys(ctx, ed); // 자판 없는 기기 단추 줄(기기대응 4-3 · 터치 모드에서만 보임) const detachMouse = attachMouse(ctx, ed, extras.mouse); /** `addSheet` 가 나중에 붙인 탭 줄(탭 없이 띄운 시트) */ let lateTabs: PartHandle | null = null; diff --git a/A00_Common/spreadsheet/spreadsheet_toolbar.css b/A00_Common/spreadsheet/spreadsheet_toolbar.css index 3d9b08d9d..fc0ffa84f 100644 --- a/A00_Common/spreadsheet/spreadsheet_toolbar.css +++ b/A00_Common/spreadsheet/spreadsheet_toolbar.css @@ -300,3 +300,14 @@ html[data-touch="on"] .ss-toolbar .ss-toolbar__more { display: inline-flex; } + +/* 자판 없는 기기 단추 줄(기기대응 4-3 · spreadsheet_touch_keys.ts) — 터치 모드에서만 */ +.ss-touch-keys { + display: none; +} + +html[data-touch="on"] .ss-touch-keys { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-4); +} diff --git a/A00_Common/spreadsheet/spreadsheet_touch_keys.ts b/A00_Common/spreadsheet/spreadsheet_touch_keys.ts new file mode 100644 index 000000000..056d0b8a8 --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_touch_keys.ts @@ -0,0 +1,43 @@ +/* ============================================================================= + * spreadsheet_touch_keys.ts + * 자판 없는 기기용 단추 줄(기기대응 4-3) — 단축키로만 되던 일을 같은 키 길(`attachKeys`)로 보냄. + * 입력줄(`ed.bar`) 끝에 붙고 터치 모드에서만 보임(spreadsheet_toolbar.css). 마우스 화면은 그대로. + * ========================================================================== */ + +import { createButton } from "@ui/ui_template_elements"; +import type { Editor } from "./spreadsheet_editor"; +import type { SpreadsheetContext } from "./spreadsheet_view_types"; + +const KEYS: [label: string, title: string, init: KeyboardEventInit, edit: boolean][] = [ + ["🔍", "찾기 · 바꾸기(Ctrl+F)", { key: "f", code: "KeyF", ctrlKey: true }, false], + ["⇉", "고른 칸 모두 같은 값(Ctrl+Enter)", { key: "Enter", ctrlKey: true }, true], + ["↵", "칸 안 줄바꿈(Alt+Enter)", { key: "Enter", altKey: true }, true], + ["✕", "고침 취소(Esc)", { key: "Escape" }, true], + ["📅", "오늘 날짜(Ctrl+;)", { key: ";", code: "Semicolon", ctrlKey: true }, true], + ["$", "참조 $ 돌리기(F4)", { key: "F4" }, true], +]; + +export function mountTouchKeys(ctx: SpreadsheetContext, ed: Editor): void { + const row = document.createElement("div"); + row.className = "ss-touch-keys"; + for (const [label, title, init, edit] of KEYS) { + if (edit && ctx.readOnly) continue; + const b = createButton({ + label, + variant: "ghost", + onClick: () => { + // 고치는 중이면 그 칸(입력줄 · 칸 편집기) — 아니면 칸 편집기로 + const target = document.activeElement === ed.formula ? ed.formula : ed.ta; + target.dispatchEvent( + new KeyboardEvent("keydown", { ...init, bubbles: true, cancelable: true }), + ); + }, + }); + b.title = title; + b.setAttribute("aria-label", title); + // 누를 때 초점을 뺏지 않음 — 고치던 칸이 그대로(손가락은 호환 mousedown 이 옴) + b.addEventListener("mousedown", (e) => e.preventDefault()); + row.append(b); + } + ed.bar.append(row); +} diff --git a/B05_Profile/B05_Profile_UI_Profile_Tools.ts b/B05_Profile/B05_Profile_UI_Profile_Tools.ts index 4403fcdda..35182c856 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Tools.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Tools.ts @@ -205,7 +205,14 @@ export function createProfileTools(callbacks: ProfileToolsCallbacks): ProfileToo const hint = document.createElement("em"); hint.className = "b05-route-profile__tool-hint"; hint.textContent = callbacks.freeHint(); - wrap.append(hint); + // 자판 없는 기기 — Backspace · Enter 대신(기기대응 4-3 · 같은 키 길 · 터치 모드에서만 보임) + const press = (key: string) => () => + document.body.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true })); + const back = toolButton("점 빼기", "마지막 점 빼기 (Backspace)", press("Backspace")); + const end = toolButton("끝", "자유지정 끝 (Enter)", press("Enter")); + back.classList.add("ui-touch-only"); + end.classList.add("ui-touch-only"); + wrap.append(back, end, hint); } else if (mode === "straighten" && pending !== null) { const hint = document.createElement("em"); hint.className = "b05-route-profile__tool-hint"; diff --git a/B06_Section/B06_Section_UI_Cross_Detail_Select.ts b/B06_Section/B06_Section_UI_Cross_Detail_Select.ts index a94566b48..648171d0d 100644 --- a/B06_Section/B06_Section_UI_Cross_Detail_Select.ts +++ b/B06_Section/B06_Section_UI_Cross_Detail_Select.ts @@ -352,6 +352,21 @@ export function attachDetailSelect(input: { input.commit(); }); + // 자판 없는 기기 — Shift · Ctrl 누르고 고르기 대신 「여럿」 켜고 톡(기기대응 4-3 · 터치 모드에서만 보임) + let multi = false; + const multiBtn = document.createElement("button"); + multiBtn.type = "button"; + multiBtn.className = "b06-cross-card__zoom-btn ui-touch-only"; + multiBtn.textContent = "여럿"; + multiBtn.title = "선 여럿 고르기 (Shift 누르고 누르기와 같음)"; + multiBtn.setAttribute("aria-pressed", "false"); + multiBtn.addEventListener("click", (event) => { + event.stopPropagation(); + multi = !multi; + multiBtn.setAttribute("aria-pressed", String(multi)); + multiBtn.classList.toggle("is-on", multi); + }); + const show = (): void => { const chosen = selected.map((i) => pieces[i]); hits.forEach((hit, i) => hit.classList.toggle("is-selected", selected.includes(i))); @@ -373,7 +388,8 @@ export function attachDetailSelect(input: { put.hidden = !mode || !selectionSource(mode, chosen); // [+ − ⤢] 왼쪽(카드 꼴) — 그림을 다시 그리면 막대가 새로 생김 const bar = cad.querySelector(".b06-cross-card__zoom"); - if (bar && label.parentElement !== bar) bar.prepend(label, put); + if (bar && label.parentElement !== bar) bar.prepend(label, put, multiBtn); + multiBtn.hidden = !mode; }; /** 누른 자리(그림 안만) → 선 · 영역 — 판정 선 굵기 대신 거리로(261-3). */ @@ -388,7 +404,7 @@ export function attachDetailSelect(input: { const onClick = (event: MouseEvent): void => { const pick = pickOf(event); - const shift = event.shiftKey || event.ctrlKey || event.metaKey; + const shift = event.shiftKey || event.ctrlKey || event.metaKey || multi; const additive = shift || mode === "area"; noteText = null; if (!pick || !("piece" in pick)) { diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts index 38b78fe5f..a3daadd23 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts @@ -148,7 +148,21 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { add.hidden = !opt.canEdit; // 「N개 고름」 · 「+」 — 층 단추 줄 오른쪽 끝(30-2 · 부르는 쪽이 붙임) const status = el("span", { className: "m02-tree__picked", attrs: { hidden: "" } }); - const tools = el("span", { className: "m02-tree__tools", children: [status, add] }); + // 자판 없는 기기 — Ctrl 누르고 고르기 대신 「여럿」 켜고 톡(기기대응 4-3 · 터치 모드에서만 보임) + let multi = false; + const multiBtn = createButton({ + label: "여럿", + variant: "ghost", + onClick: () => { + multi = !multi; + multiBtn.classList.toggle("is-on", multi); + multiBtn.setAttribute("aria-pressed", String(multi)); + }, + }); + multiBtn.classList.add("ui-touch-only"); + multiBtn.title = "여럿 고르기 (Ctrl 누르고 누르기와 같음)"; + multiBtn.setAttribute("aria-pressed", "false"); + const tools = el("span", { className: "m02-tree__tools", children: [status, multiBtn, add] }); /* --- 그리기 --- */ const canCopy = !!api.paste; @@ -275,7 +289,7 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { picked, anchor, one.id, - { ctrl: event.ctrlKey || event.metaKey, shift: event.shiftKey }, + { ctrl: event.ctrlKey || event.metaKey || multi, shift: event.shiftKey }, visibleIds(), ); if (got.handled) event.stopImmediatePropagation(); diff --git a/resources/tester/test_spreadsheet_touch.py b/resources/tester/test_spreadsheet_touch.py index 1992e9d79..f989f40bf 100644 --- a/resources/tester/test_spreadsheet_touch.py +++ b/resources/tester/test_spreadsheet_touch.py @@ -3,7 +3,7 @@ sub1 화면 서버(5174)에 견본 통합문서를 붙여 CDP 터치를 넣음: ① 톡 = 칸 고름 · 화면 자판 안 뜸(inputMode none) ② 고른 칸에서 끌기 = 범위 ③ 안 고른 칸에서 끌기 = 고름 그대로(화면 밀기) ④ 두 번 톡 = 고침 · 자판 뜸(inputMode text) -⑤ ⋯ 단추(터치 모드) = 칸 메뉴 +⑤ ⋯ 단추(터치 모드) = 칸 메뉴 ⑥ 자판 없는 단추 줄 · 🔍 = 찾기 """ import urllib.request @@ -75,6 +75,23 @@ def test_spreadsheet_touch_select_range_edit(): _touch(cdp, "touchEnd", []) page.wait_for_timeout(450) + # ⑥ 자판 없는 단추 줄(4-3) — 6개 보임 · 🔍 = 찾기 칸 열림 + n = page.evaluate( + "[...document.querySelectorAll('.ss-touch-keys > button')].filter(b => b.getClientRects().length).length" + ) + assert n == 6, n + page.evaluate("document.querySelector('.ss-touch-keys > button').click()") + page.wait_for_function( # 찾기 칸은 늦게 불러옴(크기 예산) + "() => [...document.querySelectorAll('.ss-find__input')].some(e => e.getClientRects().length)", + timeout=10000, + ) + page.keyboard.press("Escape") + page.evaluate("document.activeElement.blur()") + gx, gy, gw, gh = page.evaluate( # 찾기 칸이 자리를 먹어 격자가 옮겨졌을 수 있음 + "(() => { const g = document.querySelector('.aislo-grid').getBoundingClientRect();" + " return [g.left, g.top, g.width, g.height]; })()" + ) + a = (gx + 90, gy + 60) tap(*a) # ① 고름 first = sel() @@ -105,4 +122,5 @@ def test_spreadsheet_touch_select_range_edit(): assert page.evaluate( "[...document.querySelectorAll('.ui-ctx-menu')].some(m => !m.hidden && m.getClientRects().length)" ) + browser.close()