Merge remote-tracking branches 'origin/sub_laptop_2' and 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { colName, parseA1, parseRange, toA1 } from "./spreadsheet_address";
|
||||
import { clipWholeToUsed } from "./spreadsheet_grid_cells";
|
||||
import { formatValue } from "./spreadsheet_numfmt";
|
||||
import { moveFormula, r1c1ToA1 } from "./spreadsheet_refshift";
|
||||
import { st } from "./spreadsheet_text";
|
||||
@@ -425,15 +426,15 @@ function notifyNoFormulaOnce(): void {
|
||||
|
||||
/* ── 화면 붙임 — copy · cut · paste 사건 ────────────────────────────────── */
|
||||
|
||||
/** 복사 · 잘라내기 범위 — 행 · 열 · 전체 선택은 쓴 최외곽까지만(99-4) */
|
||||
function activeRange(ctx: SpreadsheetContext): CellRange {
|
||||
return (
|
||||
ctx.selection.범위[0] ?? {
|
||||
r0: ctx.selection.활성.r,
|
||||
c0: ctx.selection.활성.c,
|
||||
r1: ctx.selection.활성.r,
|
||||
c1: ctx.selection.활성.c,
|
||||
}
|
||||
);
|
||||
const range = ctx.selection.범위[0] ?? {
|
||||
r0: ctx.selection.활성.r,
|
||||
c0: ctx.selection.활성.c,
|
||||
r1: ctx.selection.활성.r,
|
||||
c1: ctx.selection.활성.c,
|
||||
};
|
||||
return clipWholeToUsed(ctx.sheet(), range);
|
||||
}
|
||||
|
||||
function rangesOverlap(a: CellRange, b: CellRange): boolean {
|
||||
@@ -539,8 +540,7 @@ export function attachClipboard(ctx: SpreadsheetContext): PartHandle {
|
||||
/** 마지막 잘라내기 — 같은 문서에 붙이면 칸을 옮김(그 칸을 가리키던 식도 따라감 · 엑셀) */
|
||||
let cut: PendingCut | null = null;
|
||||
|
||||
function write(ev: ClipboardEvent, prefix = ""): void {
|
||||
const range = activeRange(ctx);
|
||||
function write(ev: ClipboardEvent, range: CellRange, prefix = ""): void {
|
||||
const { html, text } = blockToClipboard(ctx.book, ctx.selection.시트, range, (r, c) =>
|
||||
ctx.engine.value(ctx.selection.시트, r, c),
|
||||
);
|
||||
@@ -552,7 +552,7 @@ export function attachClipboard(ctx: SpreadsheetContext): PartHandle {
|
||||
function onCopy(ev: ClipboardEvent): void {
|
||||
if (ctx.editor.editing()) return;
|
||||
cut = null;
|
||||
write(ev);
|
||||
write(ev, activeRange(ctx));
|
||||
}
|
||||
|
||||
function onCut(ev: ClipboardEvent): void {
|
||||
@@ -563,7 +563,7 @@ export function attachClipboard(ctx: SpreadsheetContext): PartHandle {
|
||||
for (const [a1, cell] of Object.entries(sheet.칸))
|
||||
if (inside(range, a1)) cells[a1] = structuredClone(cell);
|
||||
const token = Math.random().toString(36).slice(2);
|
||||
write(ev, `<!--aislo-cut:${token}-->`);
|
||||
write(ev, range, `<!--aislo-cut:${token}-->`);
|
||||
cut = { token, sheet: sheet.id, range, cells };
|
||||
ctx.dispatch({ 종류: "내용지움", 시트: sheet.id, 범위: [range] });
|
||||
}
|
||||
|
||||
@@ -81,10 +81,10 @@ function toScalar(value: number | string | boolean): Scalar {
|
||||
return typeof value === "number" ? toFrac(value) : value;
|
||||
}
|
||||
|
||||
/** 시트에 실제 쓰인 자리 + 여유(엑셀 최대 자리까지 다 쓰지 않음 — 스크롤판 스페이서를 적당히 둠). */
|
||||
export function computeExtent(sheet: Sheet): { rows: number; cols: number } {
|
||||
let maxR = 0;
|
||||
let maxC = 0;
|
||||
/** 시트에 실제 쓰인 최외곽 — 줄 수 · 칸 수(0 = 빈 시트). 칸 · 행 높이 · 열 폭 · 병합을 모두 침. */
|
||||
export function usedExtent(sheet: Sheet): { rows: number; cols: number } {
|
||||
let maxR = -1;
|
||||
let maxC = -1;
|
||||
for (const key of Object.keys(sheet.칸)) {
|
||||
const addr = parseA1(key);
|
||||
if (!addr) continue;
|
||||
@@ -99,9 +99,30 @@ export function computeExtent(sheet: Sheet): { rows: number; cols: number } {
|
||||
maxR = Math.max(maxR, range.r1);
|
||||
maxC = Math.max(maxC, range.c1);
|
||||
}
|
||||
return { rows: maxR + 1, cols: maxC + 1 };
|
||||
}
|
||||
|
||||
/** 시트에 실제 쓰인 자리 + 여유(엑셀 최대 자리까지 다 쓰지 않음 — 스크롤판 스페이서를 적당히 둠). */
|
||||
export function computeExtent(sheet: Sheet): { rows: number; cols: number } {
|
||||
const used = usedExtent(sheet);
|
||||
return {
|
||||
rows: Math.min(MAX_ROWS, Math.max(maxR + 100, 200)),
|
||||
cols: Math.min(MAX_COLS, Math.max(maxC + 20, 60)),
|
||||
rows: Math.min(MAX_ROWS, Math.max(used.rows + 99, 200)),
|
||||
cols: Math.min(MAX_COLS, Math.max(used.cols + 19, 60)),
|
||||
};
|
||||
}
|
||||
|
||||
/** 행 머리 · 열 머리 · 전체로 고른 범위를 쓴 최외곽까지만 줄임(복사 · 잘라내기 — 99-4). 그 밖 범위는 그대로.
|
||||
* 쓴 곳이 범위 앞이면 한 줄 · 한 칸만 남김(빈 시트 = 빈 칸 하나). */
|
||||
export function clipWholeToUsed(sheet: Sheet, range: CellRange): CellRange {
|
||||
const wholeRows = range.c0 === 0 && range.c1 === MAX_COLS - 1;
|
||||
const wholeCols = range.r0 === 0 && range.r1 === MAX_ROWS - 1;
|
||||
if (!wholeRows && !wholeCols) return range;
|
||||
const used = usedExtent(sheet);
|
||||
return {
|
||||
r0: range.r0,
|
||||
c0: range.c0,
|
||||
r1: wholeCols ? Math.max(range.r0, Math.min(range.r1, used.rows - 1)) : range.r1,
|
||||
c1: wholeRows ? Math.max(range.c0, Math.min(range.c1, used.cols - 1)) : range.c1,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/* 행 · 열 · 전체 고른 복사 = 쓴 최외곽까지(99-4) 시험 진입 — vite --ssr 로 번들해 Node 로 돌림.
|
||||
* 결과 = JSON {pass, fail, failures} 한 줄(`test_spreadsheet_clip_used.py` 가 읽음) */
|
||||
import { blockToClipboard } from "../../../A00_Common/spreadsheet/spreadsheet_clipboard";
|
||||
import {
|
||||
clipWholeToUsed,
|
||||
usedExtent,
|
||||
} from "../../../A00_Common/spreadsheet/spreadsheet_grid_cells";
|
||||
import { MAX_COLS, MAX_ROWS } from "../../../A00_Common/spreadsheet/spreadsheet_types";
|
||||
import type { CellRange, Workbook } from "../../../A00_Common/spreadsheet/spreadsheet_types";
|
||||
|
||||
let pass = 0;
|
||||
const failures: string[] = [];
|
||||
const eq = (name: string, got: unknown, want: unknown): void => {
|
||||
if (JSON.stringify(got) === JSON.stringify(want)) pass += 1;
|
||||
else failures.push(`${name}: ${JSON.stringify(got)} ≠ ${JSON.stringify(want)}`);
|
||||
};
|
||||
|
||||
const book = (칸: Record<string, { 값: number }>): Workbook =>
|
||||
({
|
||||
종류: "통합문서",
|
||||
판: 1,
|
||||
열: "t",
|
||||
서식: [{}],
|
||||
시트: [{ id: "s1", 이름: "면적산출", 칸 }],
|
||||
활성: "s1",
|
||||
}) as unknown as Workbook;
|
||||
|
||||
const used = book({ A1: { 값: 1 }, C7: { 값: 2 }, B12: { 값: 3 } }); // 12 줄 · 3 칸
|
||||
const empty = book({});
|
||||
const sheet = used.시트[0];
|
||||
const R = (r0: number, c0: number, r1: number, c1: number): CellRange => ({ r0, c0, r1, c1 });
|
||||
const lines = (b: Workbook, rg: CellRange): { rows: number; cols: number } => {
|
||||
const t = blockToClipboard(b, "s1", rg, () => null).text.split("\r\n");
|
||||
return { rows: t.length, cols: t[0].split("\t").length };
|
||||
};
|
||||
|
||||
eq("usedExtent", usedExtent(sheet), { rows: 12, cols: 3 });
|
||||
eq("usedExtent 빈 시트", usedExtent(empty.시트[0]), { rows: 0, cols: 0 });
|
||||
eq("열 머리 A:C", clipWholeToUsed(sheet, R(0, 0, MAX_ROWS - 1, 2)), R(0, 0, 11, 2));
|
||||
eq("열 머리 줄 수", lines(used, clipWholeToUsed(sheet, R(0, 0, MAX_ROWS - 1, 0))), {
|
||||
rows: 12,
|
||||
cols: 1,
|
||||
});
|
||||
eq("행 머리 3행", clipWholeToUsed(sheet, R(2, 0, 2, MAX_COLS - 1)), R(2, 0, 2, 2));
|
||||
eq("행 머리 칸 수", lines(used, clipWholeToUsed(sheet, R(2, 0, 2, MAX_COLS - 1))), {
|
||||
rows: 1,
|
||||
cols: 3,
|
||||
});
|
||||
eq("전체", clipWholeToUsed(sheet, R(0, 0, MAX_ROWS - 1, MAX_COLS - 1)), R(0, 0, 11, 2));
|
||||
eq("전체 줄 · 칸", lines(used, clipWholeToUsed(sheet, R(0, 0, MAX_ROWS - 1, MAX_COLS - 1))), {
|
||||
rows: 12,
|
||||
cols: 3,
|
||||
});
|
||||
eq("쓴 곳 밖 열 머리 Z", clipWholeToUsed(sheet, R(0, 25, MAX_ROWS - 1, 25)), R(0, 25, 11, 25));
|
||||
eq("쓴 곳 밖 행 머리 500", clipWholeToUsed(sheet, R(499, 0, 499, MAX_COLS - 1)), R(499, 0, 499, 2));
|
||||
eq(
|
||||
"빈 시트 전체",
|
||||
clipWholeToUsed(empty.시트[0], R(0, 0, MAX_ROWS - 1, MAX_COLS - 1)),
|
||||
R(0, 0, 0, 0),
|
||||
);
|
||||
eq("빈 시트 복사 글", blockToClipboard(empty, "s1", R(0, 0, 0, 0), () => null).text.trim(), "");
|
||||
eq("보통 범위는 그대로", clipWholeToUsed(sheet, R(0, 0, 999, 5)), R(0, 0, 999, 5));
|
||||
eq("단일 칸", clipWholeToUsed(sheet, R(4, 4, 4, 4)), R(4, 4, 4, 4));
|
||||
|
||||
console.log(JSON.stringify({ pass, fail: failures.length, failures }));
|
||||
@@ -0,0 +1,57 @@
|
||||
"""스프레드시트 행 · 열 · 전체 고른 복사 = 쓴 최외곽까지(PLAN 99-4).
|
||||
|
||||
`clipWholeToUsed`(순수) · `blockToClipboard` 를 vite `--ssr` 로 번들해 Node 로 돌림 —
|
||||
열 머리 → 쓴 줄 수만 · 행 머리 → 쓴 칸 수만 · 전체 → 최외곽 사각형 · 빈 시트 → 빈 복사 · 보통 범위는 그대로.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
ENTRY = ROOT / "resources" / "tester" / "spreadsheet" / "spreadsheet_clip_used_entry.ts"
|
||||
VITE = ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
def test_고른_복사_쓴_범위까지():
|
||||
with tempfile.TemporaryDirectory(prefix="spreadsheet_clip_used_") as outdir:
|
||||
entry_rel = "../" + str(ENTRY.relative_to(ROOT)).replace("\\", "/")
|
||||
build = subprocess.run(
|
||||
[
|
||||
"node",
|
||||
str(VITE),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
entry_rel,
|
||||
"--outDir",
|
||||
outdir,
|
||||
],
|
||||
cwd=str(ROOT),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=120,
|
||||
)
|
||||
assert build.returncode == 0, f"vite ssr 번들 실패:\n{build.stdout}\n{build.stderr}"
|
||||
run = subprocess.run(
|
||||
["node", str(Path(outdir) / "spreadsheet_clip_used_entry.js")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=60,
|
||||
)
|
||||
assert run.stdout.strip(), run.stderr
|
||||
result = json.loads(run.stdout)
|
||||
assert result["fail"] == 0, result["failures"]
|
||||
assert result["pass"] == 14
|
||||
Reference in New Issue
Block a user