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
243 lines
9.3 KiB
TypeScript
243 lines
9.3 KiB
TypeScript
/* =============================================================================
|
|
* spreadsheet_insert_dialog.ts (2단계 · 주인 sub_laptop_1)
|
|
* 칸 삽입(Ctrl+Shift++) · 칸 삭제(Ctrl+-) 대화상자 — 엑셀처럼 네 갈래:
|
|
* 삽입 = 셀을 오른쪽으로 밀기 · 셀을 아래로 밀기 · 행 전체 · 열 전체
|
|
* 삭제 = 셀을 왼쪽으로 밀기 · 셀을 위로 밀기 · 행 전체 · 열 전체
|
|
* 「행 전체」·「열 전체」는 A 의 행넣기 · 행지우기 · 열넣기 · 열지우기 명령을 그대로 씀
|
|
* (참조 옮김까지 A 가 다 함). 「오른쪽 · 아래로 · 왼쪽 · 위로 밀기」는 0 계약에 전용 명령이
|
|
* 없어(부분 범위 이동 명령 없음) 「칸」 명령으로 손수 값 · 식 글 · 서식 번호를 옮김 —
|
|
* ⚠ 식의 상대 참조는 안 고쳐짐 · 행 · 열 단위 기본서식(`행`·`열` 레코드)도 안 옮김
|
|
* (부분 이동은 칸 레코드만). 온전한 참조 옮김이 필요하면 「행 전체」·「열 전체」를 쓰거나
|
|
* A 의 `spreadsheet_refshift.ts` 가 부분 범위까지 다루게 된 뒤 이 파일의 shift* 함수만 고치면 됨.
|
|
*
|
|
* 잇는 법(D · sub7) — Ctrl+Shift+= → `openInsertCellsDialog(ctx)` · Ctrl+- →
|
|
* `openDeleteCellsDialog(ctx)` 를 `spreadsheet_keys.ts` navKey 에 더함. 우클릭 메뉴
|
|
* [삽입]·[삭제] 도 같은 함수를 부르면 됨.
|
|
* ========================================================================== */
|
|
|
|
import { createButton, el } from "@ui/ui_template_elements";
|
|
import { dialogKeys } from "./spreadsheet_actions";
|
|
import { parseA1, toA1 } from "./spreadsheet_address";
|
|
import type { Cell, CellRange, Sheet } from "./spreadsheet_types";
|
|
import type { SpreadsheetContext } from "./spreadsheet_view_types";
|
|
|
|
const TEXT = {
|
|
insertTitle: "삽입",
|
|
deleteTitle: "삭제",
|
|
shiftRight: "셀을 오른쪽으로 밀기",
|
|
shiftDown: "셀을 아래로 밀기",
|
|
shiftLeft: "셀을 왼쪽으로 밀기",
|
|
shiftUp: "셀을 위로 밀기",
|
|
wholeRow: "행 전체",
|
|
wholeCol: "열 전체",
|
|
ok: "확인",
|
|
cancel: "취소",
|
|
};
|
|
|
|
function readRange(ctx: SpreadsheetContext): CellRange | null {
|
|
return ctx.selection.범위[0] ?? null;
|
|
}
|
|
|
|
/** 시트 `칸` 레코드 안에서 실제 값이 있는 가장 큰 행 · 열(없으면 -1). */
|
|
function sheetUsedBounds(sheet: Sheet): { maxR: number; maxC: number } {
|
|
let maxR = -1;
|
|
let maxC = -1;
|
|
for (const key of Object.keys(sheet.칸)) {
|
|
const at = parseA1(key);
|
|
if (!at) continue;
|
|
if (at.r > maxR) maxR = at.r;
|
|
if (at.c > maxC) maxC = at.c;
|
|
}
|
|
return { maxR, maxC };
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
// 부분 범위 밀기 — 「칸」 명령으로 값 · 식 · 서식 번호만 옮김(위 머리 주석의 한계 참고)
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
export function insertShiftRight(ctx: SpreadsheetContext, range: CellRange): void {
|
|
const sheet = ctx.sheet();
|
|
const width = range.c1 - range.c0 + 1;
|
|
const lastCol = Math.max(sheetUsedBounds(sheet).maxC + width, range.c1);
|
|
const cells: Record<string, Cell | null> = {};
|
|
for (let r = range.r0; r <= range.r1; r++) {
|
|
for (let c = lastCol; c >= range.c0; c--) {
|
|
const src = c - width;
|
|
cells[toA1(r, c)] = src >= range.c0 ? (sheet.칸[toA1(r, src)] ?? null) : null;
|
|
}
|
|
}
|
|
ctx.dispatch({ 종류: "칸", 시트: ctx.selection.시트, 칸: cells });
|
|
}
|
|
|
|
export function insertShiftDown(ctx: SpreadsheetContext, range: CellRange): void {
|
|
const sheet = ctx.sheet();
|
|
const height = range.r1 - range.r0 + 1;
|
|
const lastRow = Math.max(sheetUsedBounds(sheet).maxR + height, range.r1);
|
|
const cells: Record<string, Cell | null> = {};
|
|
for (let c = range.c0; c <= range.c1; c++) {
|
|
for (let r = lastRow; r >= range.r0; r--) {
|
|
const src = r - height;
|
|
cells[toA1(r, c)] = src >= range.r0 ? (sheet.칸[toA1(src, c)] ?? null) : null;
|
|
}
|
|
}
|
|
ctx.dispatch({ 종류: "칸", 시트: ctx.selection.시트, 칸: cells });
|
|
}
|
|
|
|
export function deleteShiftLeft(ctx: SpreadsheetContext, range: CellRange): void {
|
|
const sheet = ctx.sheet();
|
|
const width = range.c1 - range.c0 + 1;
|
|
const lastCol = Math.max(sheetUsedBounds(sheet).maxC, range.c1);
|
|
const cells: Record<string, Cell | null> = {};
|
|
for (let r = range.r0; r <= range.r1; r++) {
|
|
for (let c = range.c0; c <= lastCol; c++) {
|
|
const src = c + width;
|
|
cells[toA1(r, c)] = src <= lastCol ? (sheet.칸[toA1(r, src)] ?? null) : null;
|
|
}
|
|
}
|
|
ctx.dispatch({ 종류: "칸", 시트: ctx.selection.시트, 칸: cells });
|
|
}
|
|
|
|
export function deleteShiftUp(ctx: SpreadsheetContext, range: CellRange): void {
|
|
const sheet = ctx.sheet();
|
|
const height = range.r1 - range.r0 + 1;
|
|
const lastRow = Math.max(sheetUsedBounds(sheet).maxR, range.r1);
|
|
const cells: Record<string, Cell | null> = {};
|
|
for (let c = range.c0; c <= range.c1; c++) {
|
|
for (let r = range.r0; r <= lastRow; r++) {
|
|
const src = r + height;
|
|
cells[toA1(r, c)] = src <= lastRow ? (sheet.칸[toA1(src, c)] ?? null) : null;
|
|
}
|
|
}
|
|
ctx.dispatch({ 종류: "칸", 시트: ctx.selection.시트, 칸: cells });
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
// 대화상자
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
function radioGroup(
|
|
name: string,
|
|
options: [string, string][],
|
|
): { root: HTMLElement; value(): string } {
|
|
const inputs: HTMLInputElement[] = [];
|
|
const root = el("div", {
|
|
children: options.map(([value, label], i) => {
|
|
const input = el("input", {
|
|
attrs: { type: "radio", name, value, ...(i === 0 ? { checked: "true" } : {}) },
|
|
}) as HTMLInputElement;
|
|
inputs.push(input);
|
|
const wrap = el("label", { children: [input, el("span", { text: label })] });
|
|
wrap.style.cssText = "display:flex;align-items:center;gap:6px;margin-bottom:4px;";
|
|
return wrap;
|
|
}),
|
|
});
|
|
return { root, value: () => inputs.find((i) => i.checked)?.value ?? options[0][0] };
|
|
}
|
|
|
|
function modal(
|
|
title: string,
|
|
body: HTMLElement,
|
|
onOk: (choice: string) => void,
|
|
choice: () => string,
|
|
): void {
|
|
const overlay = el("div", { className: "ui-confirm" });
|
|
const panel = el("div", { className: "ui-confirm__panel" });
|
|
const close = (): void => overlay.remove();
|
|
overlay.addEventListener("click", (ev) => {
|
|
if (ev.target === overlay) close();
|
|
});
|
|
panel.append(
|
|
el("h3", { text: title }),
|
|
body,
|
|
el("div", {
|
|
className: "ui-confirm__actions",
|
|
children: [
|
|
createButton({ label: TEXT.cancel, variant: "ghost", onClick: close }),
|
|
createButton({
|
|
label: TEXT.ok,
|
|
variant: "filled",
|
|
onClick: () => {
|
|
onOk(choice());
|
|
close();
|
|
},
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
overlay.append(panel);
|
|
document.body.append(overlay);
|
|
dialogKeys(overlay, close, () => (onOk(choice()), close()));
|
|
}
|
|
|
|
export function openInsertCellsDialog(ctx: SpreadsheetContext): void {
|
|
if (ctx.readOnly) return;
|
|
const range = readRange(ctx);
|
|
if (!range) return;
|
|
const group = radioGroup("insertMode", [
|
|
["right", TEXT.shiftRight],
|
|
["down", TEXT.shiftDown],
|
|
["row", TEXT.wholeRow],
|
|
["col", TEXT.wholeCol],
|
|
]);
|
|
modal(
|
|
TEXT.insertTitle,
|
|
group.root,
|
|
(choice) => {
|
|
if (choice === "right") insertShiftRight(ctx, range);
|
|
else if (choice === "down") insertShiftDown(ctx, range);
|
|
else if (choice === "row") {
|
|
ctx.dispatch({
|
|
종류: "행넣기",
|
|
시트: ctx.selection.시트,
|
|
at: range.r0,
|
|
수: range.r1 - range.r0 + 1,
|
|
});
|
|
} else {
|
|
ctx.dispatch({
|
|
종류: "열넣기",
|
|
시트: ctx.selection.시트,
|
|
at: range.c0,
|
|
수: range.c1 - range.c0 + 1,
|
|
});
|
|
}
|
|
},
|
|
group.value,
|
|
);
|
|
}
|
|
|
|
export function openDeleteCellsDialog(ctx: SpreadsheetContext): void {
|
|
if (ctx.readOnly) return;
|
|
const range = readRange(ctx);
|
|
if (!range) return;
|
|
const group = radioGroup("deleteMode", [
|
|
["left", TEXT.shiftLeft],
|
|
["up", TEXT.shiftUp],
|
|
["row", TEXT.wholeRow],
|
|
["col", TEXT.wholeCol],
|
|
]);
|
|
modal(
|
|
TEXT.deleteTitle,
|
|
group.root,
|
|
(choice) => {
|
|
if (choice === "left") deleteShiftLeft(ctx, range);
|
|
else if (choice === "up") deleteShiftUp(ctx, range);
|
|
else if (choice === "row") {
|
|
ctx.dispatch({
|
|
종류: "행지우기",
|
|
시트: ctx.selection.시트,
|
|
at: range.r0,
|
|
수: range.r1 - range.r0 + 1,
|
|
});
|
|
} else {
|
|
ctx.dispatch({
|
|
종류: "열지우기",
|
|
시트: ctx.selection.시트,
|
|
at: range.c0,
|
|
수: range.c1 - range.c0 + 1,
|
|
});
|
|
}
|
|
},
|
|
group.value,
|
|
);
|
|
}
|