feat(spreadsheet): 우클릭 메뉴에 붙여넣기 · 삽입/삭제 ▸ · 크기 · 정렬 · 필터 · 고정 · 그룹 · 더보기 붙임 · 읽기 전용 복사(81-3)

- spreadsheet_menu.ts — 칸 · 행 머리 · 열 머리 메뉴에 spreadsheet_menu_items 펼침 · 구분선
- 잘라내기 · 복사 — 초점을 격자로 되돌린 뒤 execCommand(메뉴가 닫히며 초점이 빠져 빈 복사되던 흠)
- 읽기 전용 · 미리보기 시트 우클릭 = 「복사」 한 줄(spreadsheet.ts 메뉴 늘 붙임)
- 시험 test_stage4_menu_items.ts 34 · 크기 화면 16.3 KB(예산 72)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
This commit is contained in:
2026-10-08 12:34:54 +09:00
co-authored by Claude Opus 5.5
parent 4a61b42424
commit ea84957927
3 changed files with 343 additions and 19 deletions
+2 -2
View File
@@ -110,7 +110,7 @@ export function createSpreadsheet(
): SpreadsheetHandle {
const readOnly = !!opts.readOnly;
const noTabs = opts.tabs === false;
const bare = readOnly || !!opts.preview; // 도구 모음 · 메뉴 없음
const bare = readOnly || !!opts.preview; // 도구 모음 없음 · 우클릭은 「복사」 한 줄
const root = el("div", {
className: `ss${readOnly ? " ss--readonly" : ""}${noTabs ? " ss--notabs" : ""}${opts.preview ? " ss--preview" : ""}`,
});
@@ -142,7 +142,7 @@ export function createSpreadsheet(
ctx.editor = ed;
const toolbar: PartHandle | null = bare ? null : mountToolbar(ctx);
const tabs: PartHandle | null = noTabs ? null : mountTabs(ctx);
const menu: PartHandle | null = bare ? null : attachMenu(ctx);
const menu: PartHandle = attachMenu(ctx); // 읽기 전용 · 미리보기는 「복사」 한 줄(81-3)
const clip = attachClipboard(ctx);
const status = createStatusBar(ctx);
const others = [toolbar, tabs, menu, clip, status].filter((p): p is PartHandle => !!p);
+37 -17
View File
@@ -12,10 +12,21 @@
* ========================================================================== */
import { createMapContextMenu, type MapContextMenuItem } from "@ui/ui_template_context_menu";
import { createButton, el, showToast } from "@ui/ui_template_elements";
import { createButton, el } from "@ui/ui_template_elements";
import { rangeToA1 } from "./spreadsheet_address";
import { confirmMerge, dialogKeys } from "./spreadsheet_actions";
import { deleteComment, getComment, setComment } from "./spreadsheet_comments";
import {
cellInsertItems,
cellMoreItems,
colInsertItems,
colMoreItems,
copyCut,
copyOnlyItems,
pasteItems,
rowInsertItems,
rowMoreItems,
} from "./spreadsheet_menu_items";
import { st } from "./spreadsheet_text";
import { MAX_COLS, MAX_ROWS, type CellRange } from "./spreadsheet_types";
import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types";
@@ -28,7 +39,8 @@ function within(range: CellRange, r: number, c: number): boolean {
/** `ctx.root` 의 contextmenu 를 받음(root null) */
export function attachMenu(ctx: SpreadsheetContext): PartHandle {
if (ctx.readOnly) return { root: null, refresh() {}, destroy() {} };
// 읽기 전용 · 미리보기 = 「복사」 한 줄(81-3)
const bare = ctx.readOnly || ctx.root.classList.contains("ss--preview");
const menu = createMapContextMenu("ss");
ctx.root.append(menu.element);
@@ -39,7 +51,7 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
}
function cutCopy(kind: "cut" | "copy"): void {
document.execCommand(kind);
copyCut(ctx, kind);
}
/** 메모 글 입력 — 작은 오버레이(native prompt 안 씀, 자동화 창이 native 창을 자동 취소하는
@@ -79,7 +91,10 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
return [
[st("MenuCut"), () => cutCopy("cut")],
[st("MenuCopy"), () => cutCopy("copy")],
[st("MenuPasteHint"), () => showToast(st("MenuPasteHint"), "info")],
...pasteItems(ctx),
"-",
...cellInsertItems(ctx, range),
"-",
[
st("MenuClearContent"),
() => ctx.dispatch({ 종류: "내용지움", 시트: ctx.selection.시트, 범위: [range] }),
@@ -107,6 +122,7 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
...(note
? ([[st("MenuCommentDelete"), () => deleteComment(ctx, r, c)]] as MapContextMenuItem[])
: []),
...cellMoreItems(ctx, range),
...(ctx.cellMenuExtra?.(r, c) ?? []),
];
}
@@ -117,11 +133,9 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
return [
[st("MenuCut"), () => cutCopy("cut")],
[st("MenuCopy"), () => cutCopy("copy")],
[st("MenuPasteHint"), () => showToast(st("MenuPasteHint"), "info")],
[
st("MenuInsertRow"),
() => ctx.dispatch({ 종류: "행넣기", 시트: ctx.selection.시트, at, 수 }),
],
...pasteItems(ctx),
"-",
...rowInsertItems(ctx, at, 수),
[
st("MenuDeleteRow"),
() => ctx.dispatch({ 종류: "행지우기", 시트: ctx.selection.시트, at, 수 }),
@@ -148,6 +162,7 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
숨김: false,
}),
],
...rowMoreItems(ctx, rows),
];
}
@@ -157,11 +172,9 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
return [
[st("MenuCut"), () => cutCopy("cut")],
[st("MenuCopy"), () => cutCopy("copy")],
[st("MenuPasteHint"), () => showToast(st("MenuPasteHint"), "info")],
[
st("MenuInsertCol"),
() => ctx.dispatch({ 종류: "열넣기", 시트: ctx.selection.시트, at, 수 }),
],
...pasteItems(ctx),
"-",
...colInsertItems(ctx, at, 수),
[
st("MenuDeleteCol"),
() => ctx.dispatch({ 종류: "열지우기", 시트: ctx.selection.시트, at, 수 }),
@@ -188,6 +201,7 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
숨김: false,
}),
],
...colMoreItems(ctx, cols),
];
}
@@ -217,7 +231,7 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
활성: { r: hit.r, c: 0 },
기준: { r: hit.r, c: 0 },
});
menu.open(x, y, rowMenuItems(rows));
menu.open(x, y, bare ? copyOnlyItems(ctx) : rowMenuItems(rows));
return;
}
@@ -235,7 +249,7 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
활성: { r: 0, c: hit.c },
기준: { r: 0, c: hit.c },
});
menu.open(x, y, colMenuItems(cols));
menu.open(x, y, bare ? copyOnlyItems(ctx) : colMenuItems(cols));
return;
}
@@ -250,7 +264,13 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
});
}
const active = ctx.selection.범위[0] ?? range(hit.r, hit.c, hit.r, hit.c);
menu.open(x, y, cellMenuItems(active, hit.r, hit.c));
menu.open(
x,
y,
bare
? [...copyOnlyItems(ctx), ...(ctx.cellMenuExtra?.(hit.r, hit.c) ?? [])]
: cellMenuItems(active, hit.r, hit.c),
);
}
}