M02 면적산출 SA01010100 에서 도구 막대 · 데이터 메뉴 · 우클릭 · 셀 서식 여섯 탭 · 찾기 · 메모 · 이름 관리 · 조건부 서식 · 유효성 · 이동 · 틀 고정 · 칸 넣기/지우기 · 데이터 도구 · 윤곽 · 수식 분석 · 되돌리기/다시 · 저장 안 하고 떠나기 · 다시 열기를 빈 값 · 같은 이름 · 긴 이름 · 취소 · Esc · Enter · 밝은/어두운 테마 예외까지 돌려 찾은 흠. - 격자(C): 틀고정이 있으면 본문 판이 그릴 행열을 틀고정 폭만큼 덜 셈 — 아래 · 오른쪽 끝이 빈 칸으로 남던 것(29 행 고정 때 1 행만 그림) · visibleRange 도 같음 · reveal 은 바로 그림 - 도구 막대: 세로 정렬 기본 표시 「위」 → 「아래」(칸 실제와 맞춤) · 크기 409 넘으면 409 · 테두리 · 단위 판이 단추 위로 떠 머리까지 덮던 것 → 단추 아래 · ✕ · 수식 안내 · 확대 글 대비(흐림 투명도 없앰 · --color-text-secondary · 밝은 5.88 · 어두운 7.91) - 병합(도구 막대 · 우클릭): 왼위 밖 값이 말없이 지워지던 것 → 엑셀처럼 먼저 물음(confirmMerge) - 셀 서식 · 이동 · 칸 넣기/지우기 · 텍스트 나누기 · 중복 제거 · 연속 채우기 · 메모 창: 열 때 초점이 창 밖(BODY)이라 Esc · Enter 가 안 먹던 것 → dialogKeys(창 안 초점 · Esc 닫기 · Enter 실행) · 셀 서식 창 탭마다 높이가 널뛰던 것 → 본문 최소 높이 - 우클릭 메뉴(ui_template 공용): Esc 로 닫힘 · 화면 밖이면 안쪽으로 당김 · 데이터 ▾ 메뉴와 알약 우클릭 메뉴가 화면 좌표를 부품 기준 자리로 써 오른쪽 아래로 떨어지던 것 고침 · 행 · 열 머리 메뉴에 잘라내기 · 붙여넣기(안내) 더함 - 자동 필터 ▼ 가 칸 밑에 깔려 눌리지 않던 것(z-index) · 값 목록이 격자 아래 끝에서 잘리면 위로 뒤집음 - Σ: 여러 칸을 고르면 첫 칸에 =SUM() 편집을 열어 Enter 로 값을 덮던 것 → 엑셀처럼 열마다 바로 아래(또는 고름 끝 빈 칸)에 합계를 바로 넣음(autoSumCells) - 조건부 서식: 빈 값이 「보다큼 0」 규칙이 되던 것 · #RRGGBB 아닌 색 · 색 없는 규칙 → 안내 후 막음 - 이름 관리: 255 자 넘는 이름 막음 · 긴 이름에 창이 화면 끝까지 늘던 것 · 시트 id(s1) 대신 시트 이름 · 「이름 바꾸기」 가 브라우저 prompt(자동 취소됨)라 안 되던 것 → 줄 안에서 고침 - 표시 형식 「@」 를 수에 걸면 빈 칸이 되던 것 → 엑셀처럼 일반으로 보임 - 연속 데이터 채우기 · 텍스트 나누기가 칸 서식을 지우던 것 → 서식 유지 - 틀 고정 「지금 칸 기준」 을 먼 칸(Z1000)에서 하면 고정 판이 격자를 다 덮어 못 움직이던 것 → 화면 안쪽 칸에서만(안내) ORCA(IP http://100.72.32.27:5181 · 메인 폴더 탭 · 판정은 수치): 목록 80 항목 밝은 80/80 · 어두운 80/80 · 사각형 겹침 · 잘림 · 화면 밖 · 대비 3 미만 0 건. 시험 — test_stage3_autosum (여러 칸 넷 더함) · test_stage2_data_tools · test_stage2_format_code · spreadsheet_b_test_entry 197/197(@ 하나 더함) · test_spreadsheet_size(css gzip 3121 · 합 67403) 통과. 시험으로 바꾼 칸은 저장 안 하고 「다시 열기」 로 버림. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W9YEkMnYQnNnRZRPXoxFsw
179 lines
6.3 KiB
TypeScript
179 lines
6.3 KiB
TypeScript
/* =============================================================================
|
|
* spreadsheet_actions.ts (주인 D)
|
|
* 단축키 표(sub1 `spreadsheet_shortcuts.ts`) 「안됨」 잇기 + 3차 부품 메뉴 자리([데이터 ▾] 에 붙임).
|
|
* Ctrl+Shift++ · Ctrl+- 칸 넣기 · 지우기(행 · 열 통째면 바로 · 아니면 sub1 창) · 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+→/← 행 · 열 묶기 · 풀기(C 윤곽 +/- 가 접음).
|
|
* 여기는 키 · 메뉴 → 이름만 · 몸통은 `spreadsheet_actions_run.ts`(늦게 — 크기 예산).
|
|
* ========================================================================== */
|
|
|
|
import type { MapContextMenuItem } from "@ui/ui_template_context_menu";
|
|
import { showConfirmDialog } from "@ui/ui_template_elements";
|
|
import { parseA1 } from "./spreadsheet_address";
|
|
import { st } from "./spreadsheet_text";
|
|
import type { CellRange } from "./spreadsheet_types";
|
|
import type { SpreadsheetContext } from "./spreadsheet_view_types";
|
|
|
|
const TEXT = {
|
|
goTo: "이동…(F5)",
|
|
freeze: "틀 고정",
|
|
freezeRow: "첫 행 고정",
|
|
freezeCol: "첫 열 고정",
|
|
freezeAt: "지금 칸 기준 고정",
|
|
unfreeze: "틀 고정 풀기",
|
|
insert: "칸 넣기…(Ctrl+Shift++)",
|
|
remove: "칸 지우기…(Ctrl+-)",
|
|
split: "텍스트 나누기…",
|
|
dedupe: "중복된 항목 제거…",
|
|
series: "연속 데이터 채우기…",
|
|
group: "묶기(Shift+Alt+→)",
|
|
ungroup: "묶음 풀기(Shift+Alt+←)",
|
|
outline: "윤곽",
|
|
};
|
|
|
|
export type Act =
|
|
| "goTo"
|
|
| "insert"
|
|
| "remove"
|
|
| "hideRows"
|
|
| "hideCols"
|
|
| "showRows"
|
|
| "showCols"
|
|
| "group"
|
|
| "ungroup"
|
|
| "outline"
|
|
| "clearBorders"
|
|
| "today"
|
|
| "next"
|
|
| "prev"
|
|
| "newSheet"
|
|
| "freezeRow"
|
|
| "freezeCol"
|
|
| "freezeAt"
|
|
| "unfreeze"
|
|
| "split"
|
|
| "dedupe"
|
|
| "series"
|
|
| `flip:${"굵게" | "기울임" | "밑줄" | "취소선"}`;
|
|
|
|
/** Ctrl+글자(e.code) → 이름 */
|
|
const CTRL: Record<string, Act> = {
|
|
KeyB: "flip:굵게",
|
|
KeyI: "flip:기울임",
|
|
KeyU: "flip:밑줄",
|
|
Digit5: "flip:취소선",
|
|
KeyG: "goTo",
|
|
Minus: "remove",
|
|
NumpadSubtract: "remove",
|
|
NumpadAdd: "insert",
|
|
Digit9: "hideRows",
|
|
Digit0: "hideCols",
|
|
Semicolon: "today", // ponytail: 시각(Ctrl+Shift+;)은 numfmt 시간 형식이 오면
|
|
PageDown: "next",
|
|
PageUp: "prev",
|
|
};
|
|
/** Ctrl+Shift+글자 → 이름 */
|
|
const CTRL_SHIFT: Record<string, Act> = {
|
|
Equal: "insert",
|
|
NumpadAdd: "insert",
|
|
Minus: "clearBorders",
|
|
Digit9: "showRows",
|
|
Digit0: "showCols",
|
|
Digit7: "outline",
|
|
PageDown: "next",
|
|
PageUp: "prev",
|
|
};
|
|
|
|
const SHEET_ACTS = new Set<Act>(["next", "prev", "newSheet"]);
|
|
|
|
/** 병합 전 확인 — 왼위 밖 칸에 값이 있으면 지워짐을 물음(엑셀과 같음) · 없으면 바로 true */
|
|
export async function confirmMerge(ctx: SpreadsheetContext, range: CellRange): Promise<boolean> {
|
|
const lost = Object.entries(ctx.sheet().칸).some(([a1, cell]) => {
|
|
const p = parseA1(a1);
|
|
if (!p || (cell.값 === undefined && cell.식 === undefined)) return false;
|
|
const inside = p.r >= range.r0 && p.r <= range.r1 && p.c >= range.c0 && p.c <= range.c1;
|
|
return inside && (p.r !== range.r0 || p.c !== range.c0);
|
|
});
|
|
return !lost || showConfirmDialog(st("MergeLoseConfirm"), st("Merge"));
|
|
}
|
|
|
|
/** 창 키 — Esc 닫기 · Enter 실행(여러 줄 글 · 단추 · 한글 조합 중은 그대로) · 창 안에 초점을 넣음 */
|
|
export function dialogKeys(overlay: HTMLElement, close: () => void, run?: () => void): void {
|
|
overlay.addEventListener("keydown", (e) => {
|
|
const t = e.target as HTMLElement;
|
|
if (e.key === "Escape") close();
|
|
else if (e.key !== "Enter" || !run || e.isComposing) return;
|
|
else if (t.tagName === "TEXTAREA" || t.tagName === "BUTTON") return;
|
|
else run();
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
});
|
|
queueMicrotask(() => {
|
|
if (overlay.contains(document.activeElement)) return;
|
|
const first = overlay.querySelector<HTMLElement>("input:not([type=hidden]), select, textarea");
|
|
if (first) return first.focus();
|
|
overlay.tabIndex = -1;
|
|
overlay.focus();
|
|
});
|
|
}
|
|
|
|
/** body 에 뜬 창(.ui-confirm)이 다 닫히면 초점을 격자로 — 창을 연 바로 뒤에 부름 */
|
|
export function watchClose(back: () => void): void {
|
|
const w = new MutationObserver(() => {
|
|
if (document.querySelector("body > .ui-confirm, body > .ui-confirm__panel")) return;
|
|
w.disconnect();
|
|
back();
|
|
});
|
|
w.observe(document.body, { childList: true });
|
|
}
|
|
|
|
function pick(e: KeyboardEvent): Act | undefined {
|
|
const ctrl = e.ctrlKey || e.metaKey;
|
|
if (e.altKey)
|
|
return e.shiftKey && !ctrl
|
|
? ({ ArrowRight: "group", ArrowLeft: "ungroup" } as Record<string, Act>)[e.key]
|
|
: undefined;
|
|
if (ctrl) return (e.shiftKey ? CTRL_SHIFT : CTRL)[e.code];
|
|
if (e.key === "F5" && !e.shiftKey) return "goTo";
|
|
if (e.key === "F11" && e.shiftKey) return "newSheet";
|
|
}
|
|
|
|
export interface Actions {
|
|
key(e: KeyboardEvent): boolean;
|
|
items: MapContextMenuItem[];
|
|
}
|
|
|
|
export function mountActions(ctx: SpreadsheetContext, back: () => void): Actions {
|
|
const go = (act: Act): void =>
|
|
void import("./spreadsheet_actions_run").then((m) => m.run(ctx, back, act));
|
|
const item = (label: string, act: Act): [string, () => void] => [label, () => go(act)];
|
|
return {
|
|
key(e) {
|
|
const act = pick(e);
|
|
// 시트 탭이 없으면(산출근거) 시트 오가기 · 새 시트를 받지 않음 — 브라우저 몫으로 둠
|
|
if (!act || (SHEET_ACTS.has(act) && ctx.root.classList.contains("ss--notabs"))) return false;
|
|
go(act);
|
|
return true;
|
|
},
|
|
items: [
|
|
item(TEXT.goTo, "goTo"),
|
|
{
|
|
label: TEXT.freeze,
|
|
children: [
|
|
item(TEXT.freezeRow, "freezeRow"),
|
|
item(TEXT.freezeCol, "freezeCol"),
|
|
item(TEXT.freezeAt, "freezeAt"),
|
|
item(TEXT.unfreeze, "unfreeze"),
|
|
],
|
|
},
|
|
item(TEXT.insert, "insert"),
|
|
item(TEXT.remove, "remove"),
|
|
item(TEXT.split, "split"),
|
|
item(TEXT.dedupe, "dedupe"),
|
|
item(TEXT.series, "series"),
|
|
{ label: TEXT.outline, children: [item(TEXT.group, "group"), item(TEXT.ungroup, "ungroup")] },
|
|
],
|
|
};
|
|
}
|