브레인 답(2026-09-27) — 저장 자리는 `Sheet.comments?: Record<A1, 글>`(sub3 가 0 계약에 보탬 · 이 창은 손대지 않고 `SheetWithComments` 로 모양만 가정). 명령 전용 「메모」 종류가 아직 없어 `시트통째` 명령에 얹음(전용 명령이 생기면 `applyComments` 한 곳만 고치면 됨). 행열 넣기·지우기 때 메모 주소 옮김은 A 의 refshift 를 부르는 자리로 파일 머리에 적어 둠(지금은 비움). 시험 harness(test_stage2_helpers.ts) 에 「시트통째」 명령 적용도 더함(다른 2단계 시험도 씀). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K7JS47SacjPxZ718QsZKyr
140 lines
6.3 KiB
TypeScript
140 lines
6.3 KiB
TypeScript
/* =============================================================================
|
|
* spreadsheet_comments.ts (2단계 · 주인 sub_laptop_1)
|
|
* 메모(풍선 설명) · 칸 위쪽 빨간 세모 표시. 저장 자리 — 브레인 답(2026-09-27):
|
|
* `Sheet.comments?: Record<string, string>` (열쇠 = 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<string, string>;
|
|
}
|
|
|
|
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<string, string>,
|
|
shift: (old: { r: number; c: number }) => { r: number; c: number } | null,
|
|
): Record<string, string> {
|
|
const next: Record<string, string> = {};
|
|
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<string, string>): void {
|
|
const sheet = ctx.sheet() as SheetWithComments;
|
|
const cleaned: Record<string, string> = {};
|
|
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;
|
|
}
|