Merge remote-tracking branch 'origin/sub_laptop_4' into sub_laptop_3

# Conflicts:
#	A00_Common/spreadsheet/spreadsheet_types.ts
This commit is contained in:
2026-09-27 21:01:37 +09:00
8 changed files with 1142 additions and 2 deletions
@@ -0,0 +1,157 @@
/* =============================================================================
* test_stage3_condfmt.ts — spreadsheet_condfmt 시험. 돌리기: npx tsx <이 파일>.
* ========================================================================== */
import {
addCondFmtRule,
moveCondFmtRule,
remapCondFmtRanges,
removeCondFmtRule,
resolveCondFmtStyle,
} from "../../../A00_Common/spreadsheet/spreadsheet_condfmt.ts";
import { assertEqual, makeBook, makeCtx, makeSheet, v } from "./test_stage2_helpers.ts";
import type { Sheet } from "../../../A00_Common/spreadsheet/spreadsheet_types.ts";
const range = { r0: 0, c0: 0, r1: 0, c1: 0 };
const sheet = makeSheet(
"s1",
{ A1: v(120) },
{
조건부서식: [
{
id: "cf1",
범위: [range],
조건: { 종류: "비교", 연산: "보다큼", 값: 100 },
스타일: { 채움: "#ffcccc" },
},
],
},
) as Sheet;
const book = makeBook([sheet]);
const env = { value: () => 120 };
assertEqual(
resolveCondFmtStyle(sheet, 0, 0, env),
{ 채움: "#ffcccc" },
"값 100 넘으면 스타일 먹임",
);
assertEqual(resolveCondFmtStyle(sheet, 5, 5, env), null, "범위 밖은 안 먹임");
const envLow = { value: () => 50 };
assertEqual(resolveCondFmtStyle(sheet, 0, 0, envLow), null, "조건 안 맞으면 null");
// 「사이」 · 「글포함」 · 「수식」 조건
const between = {
id: "b",
범위: [range],
조건: { 종류: "사이" as const, 아래: 10, 위: 20 },
스타일: {},
};
assertEqual(
resolveCondFmtStyle({ ...sheet, 조건부서식: [between] }, 0, 0, { value: () => 15 }),
{},
"사이 조건 참",
);
const contains = {
id: "c",
범위: [range],
조건: { 종류: "글포함" as const, 글: "돌" },
스타일: {},
};
assertEqual(
resolveCondFmtStyle({ ...sheet, 조건부서식: [contains] }, 0, 0, { value: () => "돌뒷길이" }),
{},
"글포함 조건 참",
);
const formula = { id: "f", 범위: [range], 조건: { 종류: "수식" as const, 식: "A1>0" }, 스타일: {} };
assertEqual(
resolveCondFmtStyle({ ...sheet, 조건부서식: [formula] }, 0, 0, {
value: () => 1,
evalFormula: (식) => 식 === "A1>0",
}),
{},
"수식 조건 — evalFormula 콜백으로 참",
);
assertEqual(
resolveCondFmtStyle({ ...sheet, 조건부서식: [formula] }, 0, 0, { value: () => 1 }),
null,
"evalFormula 없으면 수식 조건은 늘 거짓",
);
// 우선순위 — 배열 앞쪽이 이김
const two = makeSheet(
"s2",
{ A1: v(1) },
{
조건부서식: [
{
id: "first",
범위: [range],
조건: { 종류: "비교" as const, 연산: "같음" as const, 값: 1 },
스타일: { 채움: "red" },
},
{
id: "second",
범위: [range],
조건: { 종류: "비교" as const, 연산: "같음" as const, 값: 1 },
스타일: { 채움: "blue" },
},
],
},
) as Sheet;
assertEqual(
resolveCondFmtStyle(two, 0, 0, { value: () => 1 }),
{ 채움: "red" },
"먼저 나온 규칙이 이김",
);
// CRUD — 시트통째로 나감
const ctx = makeCtx(book, "s1");
addCondFmtRule(ctx, { 범위: [range], 조건: { 종류: "글포함", 글: "x" }, 스타일: { 굵게: true } });
assertEqual(ctx.applied.at(-1)?.종류, "시트통째", "addCondFmtRule 은 시트통째로 나감");
assertEqual(book.시트[0].조건부서식?.length, 2, "규칙이 더해짐");
const addedId = book.시트[0].조건부서식![1].id;
moveCondFmtRule(ctx, addedId, -1);
assertEqual(book.시트[0].조건부서식![0].id, addedId, "우선순위 위로 옮김");
removeCondFmtRule(ctx, "cf1");
assertEqual(
book.시트[0].조건부서식?.map((r) => r.id),
[addedId],
"규칙 지움",
);
// 행 · 열 넣기 뒤 범위 옮김
const shifted = remapCondFmtRanges(
[
{
id: "a",
범위: [{ r0: 0, c0: 0, r1: 0, c1: 0 }],
조건: { 종류: "글포함", 글: "x" },
스타일: {},
},
],
(r) => ({ ...r, r0: r.r0 + 1, r1: r.r1 + 1 }),
);
assertEqual(
shifted[0].범위[0],
{ r0: 1, c0: 0, r1: 1, c1: 0 },
"remapCondFmtRanges — 행 하나 밀림",
);
const dropped = remapCondFmtRanges(
[
{
id: "a",
범위: [{ r0: 0, c0: 0, r1: 0, c1: 0 }],
조건: { 종류: "글포함", 글: "x" },
스타일: {},
},
],
() => null,
);
assertEqual(dropped, [], "remapCondFmtRanges — 범위를 다 잃으면 규칙째 버림");
if (process.exitCode) process.exit(process.exitCode);
console.log("spreadsheet_condfmt 시험 끝");
@@ -0,0 +1,71 @@
/* =============================================================================
* test_stage3_names.ts — spreadsheet_names 시험. 돌리기: npx tsx <이 파일>.
* ========================================================================== */
import {
defineName,
deleteName,
invalidNameReason,
listNames,
renameName,
resolveName,
} from "../../../A00_Common/spreadsheet/spreadsheet_names.ts";
import { assertEqual, makeBook, makeCtx, makeSheet, f, v } from "./test_stage2_helpers.ts";
assertEqual(invalidNameReason("단가", []), null, "정상 이름은 통과");
assertEqual(invalidNameReason("", []), "이름이 비었음", "빈 이름 거절");
assertEqual(invalidNameReason("A1", []), "칸 주소처럼 보이는 이름은 못 씀", "칸 주소 꼴 거절");
assertEqual(
invalidNameReason("1단가", []),
"글자 · 숫자 · 밑줄만(첫 글자는 글자 · 밑줄)",
"숫자로 시작 거절",
);
assertEqual(invalidNameReason("TRUE", []), "TRUE · FALSE 는 못 씀", "예약어 거절");
assertEqual(
invalidNameReason("단가", ["단가"]),
"이미 있는 이름",
"겹치는 이름 거절(대소문자 안 가림)",
);
const range = { r0: 0, c0: 0, r1: 0, c1: 0 };
const s1 = makeSheet("s1", { A1: v(100) });
const s2 = makeSheet("s2", { B2: f("A1*2") });
const book = makeBook([s1, s2]);
const ctx = makeCtx(book, "s1");
assertEqual(defineName(ctx, "단가", range), null, "defineName 성공 — 까닭 없음");
assertEqual(ctx.applied.at(-1)?.종류, "시트통째", "defineName 은 시트통째로 나감");
assertEqual(listNames(book), [{ 이름: "단가", 시트: "s1", 범위: range }], "listNames 로 보임");
assertEqual(defineName(ctx, "단가", range), "이미 있는 이름", "같은 이름 두 번은 거절");
assertEqual(
resolveName(book, "s1", "단가"),
{ 시트: "s1", 범위: range },
"resolveName 제 시트에서 찾음",
);
assertEqual(
resolveName(book, "s2", "단가"),
{ 시트: "s1", 범위: range },
"resolveName 다른 시트에서도 느슨히 찾음",
);
assertEqual(resolveName(book, "s1", "없는이름"), null, "없는 이름은 null");
// 이름 바꾸기 — 정의 + 다른 시트 식 글 속 이름도 같이
const s2f = makeSheet("s2b", { B2: f("단가*2") });
const book2 = makeBook([s1, s2f]);
const ctx2 = makeCtx(book2, "s1");
defineName(ctx2, "단가", range);
const reason = renameName(ctx2, "단가", "단가2");
assertEqual(reason, null, "renameName 성공");
assertEqual(book2.시트[0].이름정의?.[0].이름, "단가2", "정의 이름 바뀜");
assertEqual(book2.시트[1].칸.B2.식, "단가2*2", "다른 시트 식 글 속 이름도 바뀜");
assertEqual(renameName(ctx2, "단가2", "TRUE"), "TRUE · FALSE 는 못 씀", "바꿀 이름도 규칙을 지킴");
deleteName(ctx2, "단가2");
assertEqual(book2.시트[0].이름정의, [], "deleteName 으로 지움");
assertEqual(book2.시트[1].칸.B2.식, "단가2*2", "지워도 식 글은 그대로 남음(#NAME? 은 풀이 때)");
if (process.exitCode) process.exit(process.exitCode);
console.log("spreadsheet_names 시험 끝");
@@ -0,0 +1,77 @@
/* =============================================================================
* test_stage3_validation.ts — spreadsheet_validation 시험. 돌리기: npx tsx <이 파일>.
* ========================================================================== */
import {
addValidationRule,
checkValue,
findValidation,
remapValidationRanges,
removeValidationRule,
} from "../../../A00_Common/spreadsheet/spreadsheet_validation.ts";
import { assertEqual, makeBook, makeCtx, makeSheet, v } from "./test_stage2_helpers.ts";
import type { Sheet } from "../../../A00_Common/spreadsheet/spreadsheet_types.ts";
const range = { r0: 0, c0: 0, r1: 0, c1: 0 };
const sheet = makeSheet(
"s1",
{ A1: v("사과") },
{
유효성: [
{ id: "v1", 범위: [range], 규칙: { 종류: "목록", 값들: ["사과", "배"] }, 어기면: "막음" },
],
},
) as Sheet;
const book = makeBook([sheet]);
assertEqual(findValidation(sheet, 0, 0)?.id, "v1", "findValidation 있음");
assertEqual(findValidation(sheet, 9, 9), undefined, "범위 밖은 없음");
const rule = sheet.유효성![0];
assertEqual(checkValue(rule, "배"), { ok: true, 메시지: "" }, "목록 안 값은 통과");
assertEqual(checkValue(rule, "감").ok, false, "목록 밖 값은 막힘");
assertEqual(checkValue(rule, "감").어기면, "막음", "어기면 = 막음 그대로 옴");
const rangeRule = {
id: "v2",
범위: [range],
규칙: { 종류: "수범위" as const, 아래: 0, 위: 100 },
어기면: "경고" as const,
메시지: "0~100 사이만",
};
assertEqual(checkValue(rangeRule, 50), { ok: true, 메시지: "" }, "수범위 안 값은 통과");
assertEqual(
checkValue(rangeRule, 150),
{ ok: false, 어기면: "경고", 메시지: "0~100 사이만" },
"수범위 밖 · 안내 글 그대로",
);
assertEqual(checkValue(rangeRule, "글").ok, false, "글은 수범위 규칙에서 늘 거짓");
// CRUD — 시트통째로 나감
const ctx = makeCtx(book, "s1");
addValidationRule(ctx, { 범위: [range], 규칙: { 종류: "수범위", 위: 10 }, 어기면: "경고" });
assertEqual(ctx.applied.at(-1)?.종류, "시트통째", "addValidationRule 은 시트통째로 나감");
assertEqual(book.시트[0].유효성?.length, 2, "규칙이 더해짐");
removeValidationRule(ctx, "v1");
assertEqual(book.시트[0].유효성?.map((r) => r.id).includes("v1"), false, "지운 규칙은 없음");
// 행 · 열 넣기 뒤 범위 옮김
const shifted = remapValidationRanges(
[{ id: "a", 범위: [{ r0: 0, c0: 0, r1: 0, c1: 0 }], 규칙: { 종류: "수범위" }, 어기면: "막음" }],
(r) => ({ ...r, c0: r.c0 + 1, c1: r.c1 + 1 }),
);
assertEqual(
shifted[0].범위[0],
{ r0: 0, c0: 1, r1: 0, c1: 1 },
"remapValidationRanges — 열 하나 밀림",
);
const dropped = remapValidationRanges(
[{ id: "a", 범위: [{ r0: 0, c0: 0, r1: 0, c1: 0 }], 규칙: { 종류: "수범위" }, 어기면: "막음" }],
() => null,
);
assertEqual(dropped, [], "remapValidationRanges — 범위를 다 잃으면 규칙째 버림");
if (process.exitCode) process.exit(process.exitCode);
console.log("spreadsheet_validation 시험 끝");