feat(spreadsheet): 2단계 여섯째 — 메모(풍선 설명 · 빨간 세모)

브레인 답(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
This commit is contained in:
2026-09-27 20:24:39 +09:00
co-authored by Claude Sonnet 5
parent 5807313d21
commit 0d2d5614aa
3 changed files with 202 additions and 0 deletions
@@ -0,0 +1,59 @@
/* =============================================================================
* test_stage2_comments.ts — spreadsheet_comments 시험. 돌리기: npx tsx <이 파일> (el 때문에 @ui 필요).
* ========================================================================== */
import {
countComments,
deleteComment,
getComment,
hasComment,
listComments,
remapComments,
setComment,
type SheetWithComments,
} from "../../../A00_Common/spreadsheet/spreadsheet_comments.ts";
import { assertEqual, makeBook, makeCtx, makeSheet, v } from "./test_stage2_helpers.ts";
const sheet = makeSheet("s1", { A1: v(1), B2: v(2) }) as SheetWithComments;
sheet.comments = { A1: "돌뒷길이+직고*0.1" };
const book = makeBook([sheet]);
assertEqual(getComment(sheet, 0, 0), "돌뒷길이+직고*0.1", "getComment 있음");
assertEqual(getComment(sheet, 1, 1), null, "getComment 없으면 null");
assertEqual(hasComment(sheet, 0, 0), true, "hasComment 참");
assertEqual(hasComment(sheet, 5, 5), false, "hasComment 거짓");
assertEqual(countComments(book, "s1"), 1, "countComments");
assertEqual(listComments(sheet), [{ r: 0, c: 0, 글: "돌뒷길이+직고*0.1" }], "listComments");
const ctx = makeCtx(book, "s1");
setComment(ctx, 1, 1, "새 메모");
const updated = ctx.applied.at(-1);
assertEqual(updated?.종류, "시트통째", "setComment 는 시트통째 명령으로 나감");
assertEqual(
(updated as { 시트: SheetWithComments }).시트.comments,
{ A1: "돌뒷길이+직고*0.1", B2: "새 메모" },
"메모 칸에 더해짐(다른 메모는 그대로)",
);
setComment(ctx, 0, 0, "");
const cleared = ctx.applied.at(-1);
assertEqual(
(cleared as { 시트: SheetWithComments }).시트.comments,
{ B2: "새 메모" },
"빈 글로 지움(안 적음)",
);
const moved = remapComments({ A1: "위쪽", C3: "아래쪽" }, (at) =>
at.r === 0 ? { r: at.r + 1, c: at.c } : at,
);
assertEqual(moved, { A2: "위쪽", C3: "아래쪽" }, "remapComments 옮김 함수대로 자리를 바꿈");
const dropped = remapComments({ A1: "지워질 것" }, () => null);
assertEqual(dropped, {}, "remapComments — shift 가 null 이면 버림");
deleteComment(ctx, 1, 1);
const afterDelete = (ctx.applied.at(-1) as { 시트: SheetWithComments }).시트.comments;
assertEqual(afterDelete?.B2, undefined, "deleteComment 로 지워짐");
if (process.exitCode) process.exit(process.exitCode);
console.log("spreadsheet_comments 시험 끝");
@@ -90,6 +90,10 @@ export function makeCtx(
sheet.열 = sheet.열 ?? {};
for (const c of cmd.열)
sheet.열[colLetters(c)] = { ...sheet.열[colLetters(c)], 폭: cmd.폭 ?? undefined };
} else if (cmd.종류 === "시트통째") {
const idx = book.시트.findIndex((s) => s.id === cmd.시트.id);
if (idx >= 0) book.시트[idx] = cmd.시트;
if (cmd.서식) book.서식 = cmd.서식;
}
};