feat(device): 단축키로만 되던 일에 터치 단추 — 시트 입력줄 단추 줄(찾기 · 모두 같은 값 · 줄바꿈 · 고침 취소 · 오늘 · $ 돌리기) · B05 자유지정 [점 빼기] [끝] · B06 상세 · M02 나무 [여럿] · 공용 .ui-touch-only (기기대응 4-3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
2026-10-13 13:45:53 +09:00
co-authored by Claude Opus 5.5
parent f5e7c839d0
commit a7cc8a304e
8 changed files with 122 additions and 6 deletions
+5
View File
@@ -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;
}
+2
View File
@@ -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;
@@ -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);
}
@@ -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);
}
+8 -1
View File
@@ -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";
@@ -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)) {
@@ -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();
+19 -1
View File
@@ -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()