/* ============================================================================= * spreadsheet_comments.ts (2단계 · 주인 sub_laptop_1) * 메모(풍선 설명) · 칸 위쪽 빨간 세모 표시. 저장 자리 — 브레인 답(2026-09-27): * `Sheet.comments?: Record` (열쇠 = A1 주소 · 값 = 메모 글 · 빈 글이면 지움). * 이 칸은 sub3 가 0 계약(spreadsheet_types.ts)에 보태는 중 — 여기서는 고치지 않고 모양만 가정 * (`SheetWithComments` 로 좁혀 씀 · sub3 커밋이 오면 구조가 그대로 맞음). * * 문서를 바꾸는 길은 명령뿐(view_types.ts 머리) — 「메모」 전용 명령이 아직 없어 `시트통째` * (있는 시트를 통째로 바꿔 넣는 명령)로 얹음. 메모 전용 명령이 생기면 이 파일의 `applyComments` * 한 곳만 고치면 됨. * * 행 · 열 넣기 · 지우기 때 메모 주소 옮김 — 지금은 안 함(사각지대로 남김). A 가 * `spreadsheet_refshift.ts` 를 완성하면 그 파일의 참조 옮김 함수(예: `shiftAddress` 류)를 * 이 파일 `remapComments` 자리에 불러 A1 주소들을 옮기게 잇기(지우는 행 · 열에 있던 메모는 * 참조 지움과 같은 규칙으로 버림). 지금은 `remapComments` 를 D · A 가 채울 자리로 비워 둠. * * 잇는 법(D · sub7 · C 격자) — `attachComments(ctx)` 를 한 번 붙이고: * 1) C 가 칸을 그릴 때 `hasComment(sheet, r, c)` 가 참이면 칸 위 오른쪽에 빨간 세모(4~6px)를 그림. * 2) 그 칸에 마우스가 올라가면(hover) `showBalloon(r, c)` · 벗어나면 `hideBalloon()`. * 3) 우클릭 메뉴 [메모 삽입 · 메모 편집 · 메모 삭제] 는 `setComment` · `deleteComment` 를 부름. * ========================================================================== */ import type { Sheet, Workbook } from "./spreadsheet_types"; import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types"; import { el } from "@ui/ui_template_elements"; /** 0 계약이 아직 안 실은 칸 — sub3 커밋 뒤엔 진짜 `Sheet` 가 이 모양을 그대로 가짐. */ export interface SheetWithComments extends Sheet { comments?: Record; } const a1 = (r: number, c: number): string => { let col = c + 1; let letters = ""; while (col > 0) { const rem = (col - 1) % 26; letters = String.fromCharCode(65 + rem) + letters; col = Math.floor((col - 1) / 26); } return `${letters}${r + 1}`; }; /** 자리 이동(행열 넣기 · 지우기) 뒤 메모 주소를 옮김 — A `spreadsheet_refshift` 완성 뒤 채울 자리. * `shift` 는 옛 주소 → 새 주소(지워진 자리면 null). 지금은 호출부가 없어 안 씀(사각지대 기록용). */ export function remapComments( comments: Record, shift: (old: { r: number; c: number }) => { r: number; c: number } | null, ): Record { const next: Record = {}; for (const [key, text] of Object.entries(comments)) { const at = parseA1(key); const moved = shift(at); if (moved) next[a1(moved.r, moved.c)] = text; } return next; } function parseA1(key: string): { r: number; c: number } { const m = /^([A-Z]+)(\d+)$/.exec(key); if (!m) return { r: 0, c: 0 }; let col = 0; for (const ch of m[1]) col = col * 26 + (ch.charCodeAt(0) - 64); return { r: Number(m[2]) - 1, c: col - 1 }; } export function getComment(sheet: Sheet, r: number, c: number): string | null { const comments = (sheet as SheetWithComments).comments; return comments?.[a1(r, c)] ?? null; } export function hasComment(sheet: Sheet, r: number, c: number): boolean { return getComment(sheet, r, c) !== null; } export function listComments(sheet: Sheet): { r: number; c: number; 글: string }[] { const comments = (sheet as SheetWithComments).comments ?? {}; return Object.entries(comments).map(([key, 글]) => ({ ...parseA1(key), 글 })); } /** `시트통째` 명령으로 메모 칸만 바꿔 넣음 — 그 밖 시트 내용은 그대로. */ function applyComments(ctx: SpreadsheetContext, next: Record): void { const sheet = ctx.sheet() as SheetWithComments; const cleaned: Record = {}; for (const [key, text] of Object.entries(next)) if (text) cleaned[key] = text; const nextSheet: SheetWithComments = { ...sheet, comments: cleaned }; ctx.dispatch({ 종류: "시트통째", 시트: nextSheet }); } /** 글이 비면 메모를 지움(값과 같은 규칙 — `spreadsheet_types.ts` 「빈 칸 안 적음」). */ export function setComment(ctx: SpreadsheetContext, r: number, c: number, text: string): void { const sheet = ctx.sheet() as SheetWithComments; const next = { ...(sheet.comments ?? {}) }; if (text) next[a1(r, c)] = text; else delete next[a1(r, c)]; applyComments(ctx, next); } export function deleteComment(ctx: SpreadsheetContext, r: number, c: number): void { setComment(ctx, r, c, ""); } export interface CommentsHandle extends PartHandle { showBalloon(r: number, c: number): void; hideBalloon(): void; } /** 풍선 뿌리 하나를 만들어 `ctx.root` 에 붙임 — 세모 그리기 자체는 C(격자)가 `hasComment` 로 판단해서 함. */ export function attachComments(ctx: SpreadsheetContext): CommentsHandle { const balloon = el("div", { className: "spreadsheet-comment-balloon", attrs: { hidden: "" } }); ctx.root.append(balloon); return { root: balloon, showBalloon(r: number, c: number): void { const text = getComment(ctx.sheet(), r, c); if (!text) return void this.hideBalloon(); balloon.textContent = text; balloon.removeAttribute("hidden"); const box = ctx.grid.cellBox(r, c); balloon.style.left = `${box.x + box.w}px`; balloon.style.top = `${box.y}px`; }, hideBalloon(): void { balloon.setAttribute("hidden", ""); }, refresh(): void { /* 문서가 바뀌면 지금 보이는 풍선 글도 새로 — 열린 풍선이 없으면 할 일 없음 */ }, destroy(): void { balloon.remove(); }, }; } /** 시트 전체 메모 개수(시험 · 실무 대조용) */ export function countComments(book: Workbook, sheetId: string): number { const sheet = book.시트.find((s) => s.id === sheetId); return sheet ? Object.keys((sheet as SheetWithComments).comments ?? {}).length : 0; }