- spreadsheet_actions(키 · 메뉴 → 이름) + actions_run(몸통 · 늦게): Ctrl+Shift++ · Ctrl+- (행 · 열 통째면 바로) · F5 · Ctrl+G · Ctrl+9/0 · Ctrl+Shift+9/0 · Ctrl+B/I/U/5 · Ctrl+Shift+& · Ctrl+Shift+_ · Ctrl+; · Ctrl+PageDown/PageUp · Shift+F11 · Shift+Alt+→/← - [데이터 ▾] 에 이동 · 틀 고정 ▸ · 칸 넣기/지우기 · 텍스트 나누기 · 중복 제거 · 연속 채우기 · 윤곽 ▸ - 식 편집 중 F4 참조 $ 돌리기(cycleAbs) · 접힌 묶음 행 · 열은 방향키가 건너뜀 - 크기: 수식 분석 · 자동 필터 · 찾기 패널을 처음 쓸 때 불러옴 — 화면 27.0 KB · 합 60.9 KB - M02 [스프레드시트 시험] ORCA 수치 확인 · D 규칙 시험 9 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6KXAabDTenEU7hrDQmCKY
171 lines
6.5 KiB
TypeScript
171 lines
6.5 KiB
TypeScript
/* =============================================================================
|
|
* spreadsheet_actions_run.ts (주인 D)
|
|
* `spreadsheet_actions.ts` 단축키 · 메뉴가 부르는 몸통 — 처음 누를 때 늦게 불러옴(크기 예산).
|
|
* ========================================================================== */
|
|
|
|
import { toA1 } from "./spreadsheet_address";
|
|
import { watchClose, type Act } from "./spreadsheet_actions";
|
|
import { cellStyleAt } from "./spreadsheet_cellstyle";
|
|
import { LAST_C, LAST_R } from "./spreadsheet_selection";
|
|
import type { CellRange, CellStyle, Command } from "./spreadsheet_types";
|
|
import type { SpreadsheetContext } from "./spreadsheet_view_types";
|
|
|
|
/** 엑셀 날짜 일련번호 0 = 1899-12-30 */
|
|
const EPOCH = Date.UTC(1899, 11, 30);
|
|
|
|
export function run(ctx: SpreadsheetContext, back: () => void, act: Act): void {
|
|
const insert = () => import("./spreadsheet_insert_dialog");
|
|
const view = () => import("./spreadsheet_view_menu");
|
|
const tools = () => import("./spreadsheet_data_tools");
|
|
/** 늦게 불러 창을 열고 닫히면 격자로 */
|
|
const dialog = <M>(load: () => Promise<M>, open: (m: M) => void) =>
|
|
void load().then((m) => (open(m), watchClose(back)));
|
|
const freezeBy = (pick: (m: Awaited<ReturnType<typeof view>>) => void) =>
|
|
void view().then((m) => (pick(m), back()));
|
|
|
|
const range = (): CellRange => ctx.selection.범위[0];
|
|
const fullRows = (g: CellRange) => g.c0 === 0 && g.c1 === LAST_C;
|
|
const fullCols = (g: CellRange) => g.r0 === 0 && g.r1 === LAST_R;
|
|
const span = (a: number, b: number) => Array.from({ length: b - a + 1 }, (_, i) => a + i);
|
|
const id = () => ctx.selection.시트;
|
|
|
|
/** 칸 넣기 · 지우기 — 행 · 열 통째 고름이면 엑셀처럼 창 없이 바로 */
|
|
function cells(add: boolean): void {
|
|
const g = range();
|
|
if (fullRows(g) || fullCols(g)) {
|
|
const rows = fullRows(g);
|
|
ctx.dispatch({
|
|
종류: rows ? (add ? "행넣기" : "행지우기") : add ? "열넣기" : "열지우기",
|
|
시트: id(),
|
|
at: rows ? g.r0 : g.c0,
|
|
수: rows ? g.r1 - g.r0 + 1 : g.c1 - g.c0 + 1,
|
|
});
|
|
} else
|
|
dialog(insert, (m) => (add ? m.openInsertCellsDialog(ctx) : m.openDeleteCellsDialog(ctx)));
|
|
}
|
|
|
|
function hide(rows: boolean, 숨김: boolean): void {
|
|
const g = range();
|
|
const 번호 = rows ? span(g.r0, g.r1) : span(g.c0, g.c1);
|
|
ctx.dispatch({ 종류: "숨김", 시트: id(), 축: rows ? "행" : "열", 번호, 숨김 });
|
|
}
|
|
|
|
function group(on: boolean): void {
|
|
const g = range();
|
|
const cols = fullCols(g) && !fullRows(g);
|
|
const 번호 = cols ? span(g.c0, g.c1) : span(g.r0, g.r1);
|
|
const 묶음 = on ? { 층: 1 } : null;
|
|
ctx.dispatch({ 종류: "묶음설정", 시트: id(), 축: cols ? "열" : "행", 번호, 묶음 });
|
|
}
|
|
|
|
function flip(key: keyof CellStyle): void {
|
|
const { r, c } = ctx.selection.활성;
|
|
const on = !!cellStyleAt(ctx.book, ctx.sheet(), r, c)[key];
|
|
ctx.dispatch({ 종류: "서식", 시트: id(), 범위: ctx.selection.범위, 바꿀: { [key]: !on } });
|
|
}
|
|
|
|
/** 바깥 테두리(가는 선) — 가장자리 칸마다 지금 테두리에 변을 더함 */
|
|
function outline(): void {
|
|
const g = range();
|
|
const n = (g.r1 - g.r0 + 1) * (g.c1 - g.c0 + 1);
|
|
// ponytail: 가장자리 칸만 셈 · 행 · 열 통째(백만 칸)는 안 함 — 쓴 범위로 자르면 풀림
|
|
if (fullRows(g) || fullCols(g) || n > 1e5) return;
|
|
const cmds: Command[] = [];
|
|
for (let r = g.r0; r <= g.r1; r++)
|
|
for (let c = g.c0; c <= g.c1; c++) {
|
|
const sides = [
|
|
r === g.r0 && "위",
|
|
r === g.r1 && "아래",
|
|
c === g.c0 && "왼",
|
|
c === g.c1 && "오른",
|
|
].filter(Boolean) as ("위" | "아래" | "왼" | "오른")[];
|
|
if (!sides.length) continue;
|
|
const 테두리 = { ...cellStyleAt(ctx.book, ctx.sheet(), r, c).테두리 };
|
|
for (const s of sides) 테두리[s] = { 선: "thin" };
|
|
cmds.push({
|
|
종류: "서식",
|
|
시트: id(),
|
|
범위: [{ r0: r, c0: c, r1: r, c1: c }],
|
|
바꿀: { 테두리 },
|
|
});
|
|
}
|
|
ctx.dispatch({ 종류: "묶음", 명령: cmds });
|
|
}
|
|
|
|
function today(): void {
|
|
const d = new Date();
|
|
const 값 = Math.round((Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) - EPOCH) / 864e5);
|
|
const { r, c } = ctx.selection.활성;
|
|
const key = toA1(r, c);
|
|
const 서식 = ctx.sheet().칸[key]?.서식;
|
|
ctx.dispatch({
|
|
종류: "묶음",
|
|
명령: [
|
|
{ 종류: "칸", 시트: id(), 칸: { [key]: 서식 === undefined ? { 값 } : { 값, 서식 } } },
|
|
{
|
|
종류: "서식",
|
|
시트: id(),
|
|
범위: [{ r0: r, c0: c, r1: r, c1: c }],
|
|
바꿀: { 형식: "yyyy-mm-dd" },
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
/** 다음 · 이전 보이는 시트 */
|
|
function sheetStep(d: number): void {
|
|
const list = ctx.book.시트.filter((s) => !s.숨김);
|
|
const i = list.findIndex((s) => s.id === id());
|
|
const to = list[i + d];
|
|
if (to) ctx.showSheet(to.id);
|
|
}
|
|
|
|
const goTo = () => dialog(view, (m) => m.openGoToDialog(ctx));
|
|
|
|
function clearBorders(): void {
|
|
ctx.dispatch({ 종류: "서식", 시트: id(), 범위: ctx.selection.범위, 바꿀: { 테두리: null } });
|
|
}
|
|
|
|
if (act.startsWith("flip:")) return flip(act.slice(5) as keyof CellStyle);
|
|
switch (act) {
|
|
case "goTo":
|
|
return goTo();
|
|
case "insert":
|
|
case "remove":
|
|
return cells(act === "insert");
|
|
case "hideRows":
|
|
case "hideCols":
|
|
case "showRows":
|
|
case "showCols":
|
|
return hide(act.endsWith("Rows"), act.startsWith("hide"));
|
|
case "group":
|
|
case "ungroup":
|
|
return group(act === "group");
|
|
case "outline":
|
|
return outline();
|
|
case "clearBorders":
|
|
return clearBorders();
|
|
case "today":
|
|
return today();
|
|
case "next":
|
|
case "prev":
|
|
return sheetStep(act === "next" ? 1 : -1);
|
|
case "newSheet":
|
|
return ctx.root.querySelector<HTMLElement>(".ss-tabs__add")?.click();
|
|
case "freezeRow":
|
|
return freezeBy((m) => m.freezeFirstRow(ctx));
|
|
case "freezeCol":
|
|
return freezeBy((m) => m.freezeFirstColumn(ctx));
|
|
case "freezeAt":
|
|
return freezeBy((m) => m.freezeAtActiveCell(ctx));
|
|
case "unfreeze":
|
|
return freezeBy((m) => m.unfreezePanes(ctx));
|
|
case "split":
|
|
return dialog(tools, (m) => m.openTextToColumnsDialog(ctx));
|
|
case "dedupe":
|
|
return dialog(tools, (m) => m.openRemoveDuplicatesDialog(ctx));
|
|
case "series":
|
|
return dialog(tools, (m) => m.openFillSeriesDialog(ctx));
|
|
}
|
|
}
|