feat(spreadsheet): 우클릭 메뉴에 구글 시트 항목 덧붙임(81-3)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPcuokNm1ZR2kqgGGDfaJA
This commit is contained in:
2026-10-08 12:54:24 +09:00
co-authored by Claude Sonnet 5
parent 49a8867892
commit 3f5dfd47b1
6 changed files with 407 additions and 10 deletions
+145 -7
View File
@@ -28,10 +28,35 @@ 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() {} };
const menu = createMapContextMenu("ss");
ctx.root.append(menu.element);
// 읽기 전용 · 미리보기 시트 — 메뉴 없이 무반응 대신 [복사] 한 줄만(81-3)
if (ctx.readOnly) {
const onReadOnlyMenu = (ev: MouseEvent): void => {
if (!ctx.grid.hitTest(ev.clientX, ev.clientY)) return;
ev.preventDefault();
const box = ctx.root.getBoundingClientRect();
menu.open(ev.clientX - box.left, ev.clientY - box.top, [
[st("MenuCopyOnly"), () => document.execCommand("copy")],
]);
};
ctx.root.addEventListener("contextmenu", onReadOnlyMenu);
return {
root: menu.element,
refresh() {},
destroy() {
ctx.root.removeEventListener("contextmenu", onReadOnlyMenu);
menu.close();
menu.element.remove();
},
};
}
const loadMenuItems = () => import("./spreadsheet_menu_items");
const fallbackPasteHint = (ok: boolean): void => {
if (!ok) showToast(st("MenuPasteHint"), "info");
};
ctx.cellMenuExtra ??= () => []; // 칸 메뉴 있음 — 조건 만들기가 이 끝에 붙음
function isMerged(range: CellRange): boolean {
@@ -79,7 +104,40 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
return [
[st("MenuCut"), () => cutCopy("cut")],
[st("MenuCopy"), () => cutCopy("copy")],
[st("MenuPasteHint"), () => showToast(st("MenuPasteHint"), "info")],
[
st("MenuPaste"),
() => void loadMenuItems().then((m) => m.pasteFromMenu(ctx).then(fallbackPasteHint)),
],
{
label: st("MenuPasteSpecial"),
children: [
[
st("MenuPasteValues"),
() =>
void loadMenuItems().then((m) =>
m.pasteSpecialFromMenu(ctx, "값").then(fallbackPasteHint),
),
],
[
st("MenuPasteFormat"),
() =>
void loadMenuItems().then((m) =>
m.pasteSpecialFromMenu(ctx, "서식").then(fallbackPasteHint),
),
],
],
},
[st("MenuInsertCells"), () => void loadMenuItems().then((m) => m.insertCells(ctx))],
[st("MenuDeleteCells"), () => void loadMenuItems().then((m) => m.deleteCells(ctx))],
[
st("MenuDeleteRow"),
() => ctx.dispatch({ 종류: "행지우기", 시트: ctx.selection.시트, at: r, 수: 1 }),
],
[
st("MenuDeleteCol"),
() => ctx.dispatch({ 종류: "열지우기", 시트: ctx.selection.시트, at: c, 수: 1 }),
],
{ divider: true },
[
st("MenuClearContent"),
() => ctx.dispatch({ 종류: "내용지움", 시트: ctx.selection.시트, 범위: [range] }),
@@ -107,6 +165,15 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
...(note
? ([[st("MenuCommentDelete"), () => deleteComment(ctx, r, c)]] as MapContextMenuItem[])
: []),
{ divider: true },
[st("MenuAutofit"), () => void loadMenuItems().then((m) => m.autofitCols(ctx, [c]))],
{ divider: true },
[st("MenuCondFmt"), () => void loadMenuItems().then((m) => m.openCondFmt(ctx))],
[st("MenuValidation"), () => void loadMenuItems().then((m) => m.openValidation(ctx))],
[st("MenuNames"), () => void loadMenuItems().then((m) => m.openNames(ctx))],
{ divider: true },
[st("MenuFreeze"), () => void loadMenuItems().then((m) => m.freezeAtActiveCell(ctx))],
[st("MenuUnfreeze"), () => void loadMenuItems().then((m) => m.unfreezePanes(ctx))],
...(ctx.cellMenuExtra?.(r, c) ?? []),
];
}
@@ -117,11 +184,24 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
return [
[st("MenuCut"), () => cutCopy("cut")],
[st("MenuCopy"), () => cutCopy("copy")],
[st("MenuPasteHint"), () => showToast(st("MenuPasteHint"), "info")],
[
st("MenuInsertRow"),
st("MenuPaste"),
() => void loadMenuItems().then((m) => m.pasteFromMenu(ctx).then(fallbackPasteHint)),
],
[
st("MenuInsertRowAbove"),
() => ctx.dispatch({ 종류: "행넣기", 시트: ctx.selection.시트, at, 수 }),
],
[
st("MenuInsertRowBelow"),
() =>
ctx.dispatch({
종류: "행넣기",
시트: ctx.selection.시트,
at: rows[rows.length - 1] + 1,
수,
}),
],
[
st("MenuDeleteRow"),
() => ctx.dispatch({ 종류: "행지우기", 시트: ctx.selection.시트, at, 수 }),
@@ -148,6 +228,25 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
숨김: false,
}),
],
{ divider: true },
[st("MenuRowSize"), () => void loadMenuItems().then((m) => m.openRowSizeDialog(ctx, rows))],
{ divider: true },
[st("MenuFilterToggle"), () => void loadMenuItems().then((m) => m.filterToggle(ctx))],
[st("MenuFreeze"), () => void loadMenuItems().then((m) => m.freezeFirstRow(ctx))],
[st("MenuUnfreeze"), () => void loadMenuItems().then((m) => m.unfreezePanes(ctx))],
{
label: st("MenuGroup"),
children: [
[
st("MenuGroupOn"),
() => void loadMenuItems().then((m) => m.groupAxis(ctx, "행", rows, true)),
],
[
st("MenuGroupOff"),
() => void loadMenuItems().then((m) => m.groupAxis(ctx, "행", rows, false)),
],
],
},
];
}
@@ -157,11 +256,24 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
return [
[st("MenuCut"), () => cutCopy("cut")],
[st("MenuCopy"), () => cutCopy("copy")],
[st("MenuPasteHint"), () => showToast(st("MenuPasteHint"), "info")],
[
st("MenuInsertCol"),
st("MenuPaste"),
() => void loadMenuItems().then((m) => m.pasteFromMenu(ctx).then(fallbackPasteHint)),
],
[
st("MenuInsertColLeft"),
() => ctx.dispatch({ 종류: "열넣기", 시트: ctx.selection.시트, at, 수 }),
],
[
st("MenuInsertColRight"),
() =>
ctx.dispatch({
종류: "열넣기",
시트: ctx.selection.시트,
at: cols[cols.length - 1] + 1,
수,
}),
],
[
st("MenuDeleteCol"),
() => ctx.dispatch({ 종류: "열지우기", 시트: ctx.selection.시트, at, 수 }),
@@ -188,6 +300,32 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
숨김: false,
}),
],
{ divider: true },
[st("MenuColSize"), () => void loadMenuItems().then((m) => m.openColSizeDialog(ctx, cols))],
[st("MenuAutofit"), () => void loadMenuItems().then((m) => m.autofitCols(ctx, cols))],
{ divider: true },
[st("MenuSortAsc"), () => void loadMenuItems().then((m) => m.sortAsc(ctx))],
[st("MenuSortDesc"), () => void loadMenuItems().then((m) => m.sortDesc(ctx))],
[st("MenuFilterToggle"), () => void loadMenuItems().then((m) => m.filterToggle(ctx))],
[st("MenuFreeze"), () => void loadMenuItems().then((m) => m.freezeFirstColumn(ctx))],
[st("MenuUnfreeze"), () => void loadMenuItems().then((m) => m.unfreezePanes(ctx))],
{
label: st("MenuGroup"),
children: [
[
st("MenuGroupOn"),
() => void loadMenuItems().then((m) => m.groupAxis(ctx, "열", cols, true)),
],
[
st("MenuGroupOff"),
() => void loadMenuItems().then((m) => m.groupAxis(ctx, "열", cols, false)),
],
],
},
{ divider: true },
[st("MenuCondFmt"), () => void loadMenuItems().then((m) => m.openCondFmt(ctx))],
[st("MenuValidation"), () => void loadMenuItems().then((m) => m.openValidation(ctx))],
[st("MenuNames"), () => void loadMenuItems().then((m) => m.openNames(ctx))],
];
}