Files
Aislo/A00_Common/spreadsheet/spreadsheet_actions_run.ts
T
eomsangdonandClaude Opus 5.5 fca4463768 feat(spreadsheet): D 잇기 3 — 칸 넣기 · 이동 · 틀 고정 · 데이터 도구 · 묶기 · 단축키 표 「안됨」 잇기
- 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
2026-09-27 21:51:06 +09:00

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));
}
}