Merge remote-tracking branch 'origin/main_desktop_1' into sub_laptop_8

This commit is contained in:
2026-10-08 12:31:56 +09:00
6 changed files with 100 additions and 21 deletions
+7 -7
View File
@@ -171,7 +171,7 @@ export function createSpreadsheet(
}
grid.render();
select(ctx.selection);
select(ctx.selection, false, true);
loadLinks();
if (opts.conditions && !readOnly)
void import("./spreadsheet_condition").then((m) => {
@@ -221,7 +221,7 @@ export function createSpreadsheet(
if (command.종류 === "칸") grid.invalidate([...again, ...touched(command.시트, command.칸)]);
else grid.render();
}
select(ctx.selection);
select(ctx.selection, false, true); // 명령이 바꾼 뒤 — 알약 점 등 문서 종속 부품도 다시(81-2)
opts.onChange?.(structuredClone(ctx.book));
}
@@ -235,7 +235,7 @@ export function createSpreadsheet(
return out;
}
function select(selection: Selection, reveal = false): void {
function select(selection: Selection, reveal = false, dataChanged = false): void {
ctx.selection = selection;
memory.set(selection.시트, selection);
grid.renderSelection();
@@ -244,7 +244,7 @@ export function createSpreadsheet(
grid.reveal(far.r, far.c);
}
ed.sync();
for (const p of others) p.refresh();
for (const p of others) p.refresh(dataChanged);
const g = selection.범위[0];
opts.onSelect?.({
시트: selection.시트,
@@ -261,7 +261,7 @@ export function createSpreadsheet(
ctx.book.활성 = sheetId;
ctx.selection = memory.get(sheetId) ?? selectCell(sheet, 0, 0);
grid.render();
select(ctx.selection, true);
select(ctx.selection, true, true);
ed.focus();
}
@@ -275,13 +275,13 @@ export function createSpreadsheet(
memory.clear();
ctx.selection = selectCell(firstSheet(next), 0, 0);
grid.render();
select(ctx.selection);
select(ctx.selection, false, true);
loadLinks();
},
recalc() {
ctx.engine.rebuild(ctx.book);
grid.render();
for (const p of others) p.refresh();
for (const p of others) p.refresh(true);
return ctx.engine.snapshot();
},
getLinkedValues() {
+37 -9
View File
@@ -358,7 +358,9 @@ export function mountLinks(
bar.querySelector(".is-here")?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
}
/** 이은 칸 — 알약 색 옅은 배경 + 왼아래 작은 세모 · 칸이 든 판 층에 얹어 스크롤을 따라감 */
/** 이은 칸 — 알약 색 옅은 배경 + 왼아래 작은 세모 · 칸이 든 판 층에 얹어 스크롤을 따라감
* 읽기(slot — getBoundingClientRect) 를 칸마다 먼저 다 끝내고 쓰기(append)는 나중에 — 안 그러면
* 읽기·쓰기가 번갈아 와서 매 칸마다 강제 레이아웃이 남(81-2②). */
function placeDots(): void {
for (const d of dots) d.remove();
dots = [];
@@ -368,11 +370,37 @@ export function mountLinks(
// 칸마다 이은 알약(이은 차례) — 색 = 먼저 이은 알약 · 둘 넘으면 수
const byCell = new Map<string, CellLink[]>();
for (const l of ctx.sheet().연결 ?? []) byCell.set(l.칸, [...(byCell.get(l.칸) ?? []), l]);
// 1) 읽기 — 칸마다 자리만 먼저 모음(DOM 변경 없음)
const cellSlots: Array<{
list: CellLink[];
layer: HTMLElement;
box: ReturnType<typeof slot>["box"];
}> = [];
for (const [a1, list] of byCell) {
const p = parseA1(a1);
if (!p) continue;
const { layer, box } = slot(ctx, p.r, p.c);
if (!box.w || !box.h) continue; // 숨긴 행열
cellSlots.push({ list, layer, box });
}
const rangeSlots: Array<{
mark: { 시트: string; 범위: string; 이름: string; 설명?: string };
layer: HTMLElement;
box: ReturnType<typeof slot>["box"];
end: ReturnType<typeof slot>["box"];
}> = [];
for (const mark of opts?.표시범위 ?? []) {
const g = mark.시트 === ctx.sheet().id ? parseRange(mark.범위) : null;
if (!g) continue;
const { layer, box } = slot(ctx, g.r0, g.c0);
const end = slot(ctx, g.r1, g.c1).box;
if (!box.w || !box.h) continue;
rangeSlots.push({ mark, layer, box, end });
}
// 2) 쓰기 — 모은 자리에 한 번에 그림
for (const { list, layer, box } of cellSlots) {
const first = list[0];
const tint = el("div", { className: "ss-link-cell", attrs: { "data-key": first.키 } });
tint.style.cssText = `left:${box.x}px;top:${box.y}px;width:${box.w}px;height:${box.h}px`;
@@ -392,12 +420,7 @@ export function mountLinks(
dots.push(...marks);
}
// 보낸 범위(도면 표 등) — 채움 없는 점선 테두리 + 왼위 이름표(알약 색 채움과 안 겹침)
for (const mark of opts?.표시범위 ?? []) {
const g = mark.시트 === ctx.sheet().id ? parseRange(mark.범위) : null;
if (!g) continue;
const { layer, box } = slot(ctx, g.r0, g.c0);
const end = slot(ctx, g.r1, g.c1).box;
if (!box.w || !box.h) continue;
for (const { mark, layer, box, end } of rangeSlots) {
const frame = el("div", {
className: "ss-link-range",
attrs: { "data-range": mark.범위 },
@@ -542,12 +565,17 @@ export function mountLinks(
ctx.grid.root.addEventListener("drop", onDropEvent);
ctx.grid.root.addEventListener("mousemove", onHover);
let dotsReady = false;
return {
root: opts ? bar : null,
apply: applyLinked,
refresh() {
// dataChanged 거짓(고름만 바뀜) 이면 점은 건너뜀 — 점은 선택에 안 매임(81-2②)
refresh(dataChanged = false) {
renderBar();
placeDots();
if (dataChanged || !dotsReady) {
placeDots();
dotsReady = true;
}
},
destroy() {
ctx.grid.root.removeEventListener("aislo-grid-zoom", onZoom);
+2 -2
View File
@@ -112,7 +112,7 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
}
function rowMenuItems(rows: number[]): MapContextMenuItem[] {
const at = Math.min(...rows);
const at = rows[0]; // rows 는 늘 오름차순 연속(81-2③) — 전체 고름이면 104만 개라 Math.min(...rows) 는 스택 넘침
const 수 = rows.length;
return [
[st("MenuCut"), () => cutCopy("cut")],
@@ -152,7 +152,7 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
}
function colMenuItems(cols: number[]): MapContextMenuItem[] {
const at = Math.min(...cols);
const at = cols[0]; // cols 도 늘 오름차순 연속 — 같은 까닭(81-2③)
const 수 = cols.length;
return [
[st("MenuCut"), () => cutCopy("cut")],
@@ -10,6 +10,7 @@
import { el } from "@ui/ui_template_elements";
import { createMapContextMenu, type MapContextMenuItem } from "@ui/ui_template_context_menu";
import { isFrac, fracToDouble } from "./spreadsheet_functions_math";
import { computeExtent } from "./spreadsheet_grid_cells";
import type { CellRange, Scalar } from "./spreadsheet_types";
import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types";
@@ -31,13 +32,16 @@ export interface SelectionStats {
/** 고른 범위(여럿) 안 칸 값을 모아 통계를 냄 — 엔진 값 기준(식 칸도 풀린 값). */
export function computeSelectionStats(ctx: SpreadsheetContext): SelectionStats {
const sheet = ctx.selection.시트;
const extent = computeExtent(ctx.sheet()); // 쓴 범위까지만 셈(81-2 — 열 머리 전체 고름이면 104만 칸을 다 돎)
const nums: number[] = [];
let 개수 = 0;
const seen = new Set<number>();
const visit = (range: CellRange): void => {
for (let r = range.r0; r <= range.r1; r++) {
for (let c = range.c0; c <= range.c1; c++) {
const r1 = Math.min(range.r1, extent.rows - 1);
const c1 = Math.min(range.c1, extent.cols - 1);
for (let r = range.r0; r <= r1; r++) {
for (let c = range.c0; c <= c1; c++) {
const key = r * 1_000_000 + c;
if (seen.has(key)) continue;
seen.add(key);
@@ -119,6 +119,7 @@ export interface SpreadsheetContext {
/** E 부품 — D 가 `mountToolbar(ctx)` 등으로 붙이고 고름 · 문서가 바뀌면 `refresh()`. */
export interface PartHandle {
root: HTMLElement | null;
refresh(): void;
/** `dataChanged` — 문서가 실제로 바뀐 다시 그림(참) vs 고름만 바뀐 다시 그림(거짓 · 81-2 알약 점 건너뜀용) */
refresh(dataChanged?: boolean): void;
destroy(): void;
}
@@ -0,0 +1,46 @@
/* =============================================================================
* test_stage4_menu_perf.ts — 우클릭 속도(81-2③) 재현 시험. 돌리기: npx tsx <이 파일>.
* `spreadsheet_menu.ts` rowMenuItems/colMenuItems 의 `at` 계산 — 옛 Math.min(...rows) 는
* 전체 고름(열 머리 104만 개) 배열을 스프레드해 콜스택을 넘침(RangeError) · 고친 `rows[0]`
* (rows 는 늘 오름차순 연속이라 첫 자리가 최소)는 크기와 무관하게 안전·상수 시간.
* `computeSelectionStats` 쪽(①)은 createMapContextMenu 가 끌어오는 css 임포트 때문에
* 이 node 시험 환경에서 직접 불러오지 못함 — ORCA 화면 검증으로 수치 확인(81-2 보고 참고).
* ========================================================================== */
import { MAX_COLS, MAX_ROWS } from "../../../A00_Common/spreadsheet/spreadsheet_types.ts";
import { assertEqual } from "./test_stage2_helpers.ts";
const bigRows = Array.from({ length: MAX_ROWS }, (_, i) => i); // 열 머리 전체 고름 재현
let oldThrew = false;
try {
Math.min(...bigRows); // 옛 코드 — 이 크기에선 RangeError(콜스택 넘침)
} catch {
oldThrew = true;
}
if (!oldThrew) {
console.error(
"FAIL 옛 Math.min(...rows) 재현 실패 — 이 Node 가 다른 콜스택 한도를 씀(시험 전제 깨짐)",
);
process.exitCode = 1;
} else {
console.log("ok 옛 Math.min(...rows) 는 104만 개에서 콜스택을 넘침(고치기 전 재현)");
}
const t0 = performance.now();
const at = bigRows[0]; // 고친 코드(spreadsheet_menu.ts rowMenuItems)
const ms = performance.now() - t0;
assertEqual(at, 0, "rows[0] = 최소값(오름차순 연속)");
if (ms >= 5) {
console.error(`FAIL rows[0] 은 5ms 안(상수 시간) — 실측 ${ms.toFixed(2)}ms`);
process.exitCode = 1;
} else {
console.log(`ok rows[0] ${ms.toFixed(3)}ms(크기 무관 상수 시간)`);
}
// 열 머리(MAX_COLS) 도 같은 모양
const bigCols = Array.from({ length: MAX_COLS }, (_, i) => i);
assertEqual(bigCols[0], 0, "cols[0] = 최소값");
if (process.exitCode) process.exit(process.exitCode);
console.log("spreadsheet_menu 우클릭 속도(81-2③) 시험 끝");