diff --git a/A00_Common/spreadsheet/spreadsheet_grid.ts b/A00_Common/spreadsheet/spreadsheet_grid.ts index 9e0b97db7..416f5dd62 100644 --- a/A00_Common/spreadsheet/spreadsheet_grid.ts +++ b/A00_Common/spreadsheet/spreadsheet_grid.ts @@ -437,6 +437,16 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle { }; } + /** 굴리는 영역 밖 테두리 자르기(`overlay` 좌표) — 고정 머리 칸 · 모서리 · 스크롤바 위로 안 나가게(요구 566). */ + function clipOf(b: CellBox, inFrozenCol: boolean, inFrozenRow: boolean): string { + const pad = (n: number): number => Math.max(0, Math.ceil(n)); + const top = inFrozenRow ? 0 : pad(frozenH - b.y); + const left = inFrozenCol ? 0 : pad(frozenW - b.x); + const bottom = pad(b.y + b.h - (frozenH + paneBR.clientHeight)); + const right = pad(b.x + b.w - (frozenW + paneBR.clientWidth)); + return `inset(${top}px ${right}px ${bottom}px ${left}px)`; + } + function renderSelection(): void { let selLayer = overlay.querySelector(".aislo-grid__sel-layer"); if (!selLayer) { @@ -470,6 +480,7 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle { node.style.height = `${screen.h}px`; node.style.borderColor = "var(--aislo-grid-selected)"; node.style.borderWidth = i === 0 ? "2px" : "1px"; + node.style.clipPath = clipOf(screen, inFrozenCol, inFrozenRow); selLayer!.append(node); } // 활성 칸 테두리 — 칸 다시 그리기(`renderRowOfCells`) 없이 고름만 바뀌어도 옛 칸에 남지 않게. @@ -487,6 +498,17 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle { handle.className = "aislo-grid__fill-handle"; handle.style.left = `${corner.x + corner.w - 4}px`; handle.style.top = `${corner.y + corner.h - 4}px`; + const hx = corner.x + corner.w; + const hy = corner.y + corner.h; + const hFC = handleAt.c < frozenCols; + const hFR = handleAt.r < frozenRows; + if ( + (!hFC && hx < frozenW) || + (!hFR && hy < frozenH) || + hx > frozenW + paneBR.clientWidth || + hy > frozenH + paneBR.clientHeight + ) + handle.style.display = "none"; selLayer!.append(handle); } @@ -525,6 +547,16 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle { node.style.width = `${merged.w}px`; node.style.height = `${merged.h}px`; node.style.borderColor = REF_COLORS[ref.색번호 % REF_COLORS.length]; + node.style.clipPath = clipOf( + { + x: parseFloat(node.style.left), + y: parseFloat(node.style.top), + w: merged.w, + h: merged.h, + }, + inFrozenCol, + inFrozenRow, + ); refLayer!.append(node); } } diff --git a/resources/tester/test_730_grid_sel_clip.py b/resources/tester/test_730_grid_sel_clip.py new file mode 100644 index 000000000..3f774ec45 --- /dev/null +++ b/resources/tester/test_730_grid_sel_clip.py @@ -0,0 +1,30 @@ +"""730-1 지킴 — 엑셀 격자 고른 범위 · 참조 테두리 · 채우기 손잡이는 굴리는 영역 밖으로 안 나감(요구 566). +화면 수치 판정은 tmp 의 playwright 일회 검증 · 여기서는 자르기 길이 빠지지 않았는지만 지킴.""" + +import re +from pathlib import Path + +SRC = (Path(__file__).parents[2] / "A00_Common/spreadsheet/spreadsheet_grid.ts").read_text( + encoding="utf-8" +) + + +def body(name: str) -> str: + m = re.search(rf"function {name}\(.*?\n }}\n", SRC, re.S) + assert m, name + return m.group(0) + + +def test_clip_helper_uses_frozen_and_pane_size(): + h = body("clipOf") + assert "frozenH" in h and "frozenW" in h + assert "paneBR.clientHeight" in h and "paneBR.clientWidth" in h + + +def test_selection_and_ref_borders_are_clipped(): + assert "clipPath = clipOf(" in body("renderSelection") + assert "clipPath = clipOf(" in body("setRefHighlights") + + +def test_fill_handle_hidden_outside_view(): + assert 'handle.style.display = "none"' in body("renderSelection")