feat(spreadsheet): 3단계 — 조건부 서식(계약 더함)
Sheet.조건부서식·유효성·이름정의 계약 더함(0 계약 · git-sync 뒤 더함) · spreadsheet_condfmt.ts — 값 비교·사이·글 포함·수식 조건 · 채움·글자색 · 규칙 관리 창 · 우선순위 옮김 · 행열 넣기·지우기 범위 따라가기(remapCondFmtRanges). 엔진(A)·격자(C) 파일은 안 건드림 — 잇는 법을 머리 주석에 적어 둠(잇기는 sub7). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EUypcnp5d1gU2aeKh9F2H7
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
/* =============================================================================
|
||||
* spreadsheet_condfmt.ts (3단계 · 주인 sub_laptop_4)
|
||||
* 조건부 서식 — 칸 값이 규칙에 맞으면 채움색 · 글자색을 덮어씀. 저장 자리 `Sheet.조건부서식`
|
||||
* (0 계약 `spreadsheet_types.ts` 에 방금 더함 · 계약 더함).
|
||||
*
|
||||
* 규칙 차례 = 우선순위(배열 앞쪽이 먼저 이김 · 엑셀 「규칙 관리」 위쪽과 같음). 스타일은 `서식` 표
|
||||
* 번호를 안 씀 — 규칙마다 채움 · 글자색을 직접 가져 표 번호 조율(A 몫)이 필요 없음. 「수식이 참」
|
||||
* 조건은 임의 수식 글을 파싱·계산해야 해서(엔진 A) 이 파일은 `evalFormula` 콜백만 받고 엔진을 모름.
|
||||
*
|
||||
* 잇는 법(A · C · D · sub7 — 지금은 하나도 안 이어짐):
|
||||
* 1) 격자(C)가 칸을 그릴 때 `resolveCondFmtStyle(sheet, r, c, env)` 로 덮어쓸 스타일을 얻어
|
||||
* 지금 셀 스타일 위에 얹음(env.value 는 `ctx.engine.value` 를 수 · 글 · 참거짓 · null 로 바꿔 줌).
|
||||
* 2) `env.evalFormula(식, r, c)` 구현 — `spreadsheet_parser.parse(식)` 뒤 `spreadsheet_eval` 로
|
||||
* 풀어 참거짓으로(A 가 만들 자리 · 지금 없음).
|
||||
* 3) 행 · 열 넣기 · 지우기 뒤 `remapCondFmtRanges` 를 그 시트 `조건부서식` 에 적용해 범위를 옮김
|
||||
* (`spreadsheet_commands.insertDelete` 가 지금 이 규칙을 모름 — refshift 참조 옮김과 같은 자리에
|
||||
* 걸어야 함 · A 가 채울 자리).
|
||||
* 4) 도구 모음 · 우클릭 메뉴에 [조건부 서식] 항목을 두고 `createCondFmtPanel(ctx)` 를 띄움.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createInputField, createSelectField, el } from "@ui/ui_template_elements";
|
||||
import { inRange, rangeToA1 } from "./spreadsheet_address";
|
||||
import type {
|
||||
CellRange,
|
||||
CondFmtCompareOp,
|
||||
CondFmtCondition,
|
||||
CondFmtRule,
|
||||
CondFmtStyle,
|
||||
Sheet,
|
||||
} from "./spreadsheet_types";
|
||||
import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types";
|
||||
|
||||
// css 는 아직 안 이어짐(잇는 법 머리 참고) — 실제로 붙일 때 `import "./spreadsheet_stage3.css";` 한 줄 더함.
|
||||
|
||||
export type { CondFmtCompareOp, CondFmtCondition, CondFmtRule, CondFmtStyle };
|
||||
|
||||
/** 칸 값을 이미 수 · 글 · 참거짓 · 빈칸으로 바꿔 넘김(Frac → 수는 호출부 몫 — 이 파일은 엔진을 모름). */
|
||||
export type CondFmtValue = number | string | boolean | null;
|
||||
|
||||
function testCondition(
|
||||
condition: CondFmtCondition,
|
||||
value: CondFmtValue,
|
||||
evalFormula: (식: string) => boolean,
|
||||
): boolean {
|
||||
switch (condition.종류) {
|
||||
case "비교": {
|
||||
if (typeof value !== "number") return false;
|
||||
switch (condition.연산) {
|
||||
case "보다큼":
|
||||
return value > condition.값;
|
||||
case "보다작음":
|
||||
return value < condition.값;
|
||||
case "크거나같음":
|
||||
return value >= condition.값;
|
||||
case "작거나같음":
|
||||
return value <= condition.값;
|
||||
case "같음":
|
||||
return value === condition.값;
|
||||
case "다름":
|
||||
return value !== condition.값;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "사이":
|
||||
return typeof value === "number" && value >= condition.아래 && value <= condition.위;
|
||||
case "글포함":
|
||||
return typeof value === "string" && value.includes(condition.글);
|
||||
case "수식":
|
||||
return evalFormula(condition.식);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export interface CondFmtEnv {
|
||||
value(r: number, c: number): CondFmtValue;
|
||||
/** 없으면 「수식이 참」 조건은 늘 거짓 */
|
||||
evalFormula?(formula: string, r: number, c: number): boolean;
|
||||
}
|
||||
|
||||
/** 칸 하나에 먹일 조건부 서식 — 처음 맞는 규칙(우선순위 위) · 없으면 null. */
|
||||
export function resolveCondFmtStyle(
|
||||
sheet: Sheet,
|
||||
r: number,
|
||||
c: number,
|
||||
env: CondFmtEnv,
|
||||
): CondFmtStyle | null {
|
||||
for (const rule of sheet.조건부서식 ?? []) {
|
||||
if (!rule.범위.some((range) => inRange(range, r, c))) continue;
|
||||
const value = env.value(r, c);
|
||||
const hit = testCondition(rule.조건, value, (식) => env.evalFormula?.(식, r, c) ?? false);
|
||||
if (hit) return rule.스타일;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 행 · 열 넣기 · 지우기 뒤 범위 옮김 — `shift` 가 null 이면 그 범위는 버림(규칙이 범위를 다 잃으면 규칙째 버림). */
|
||||
export function remapCondFmtRanges(
|
||||
rules: CondFmtRule[],
|
||||
shift: (range: CellRange) => CellRange | null,
|
||||
): CondFmtRule[] {
|
||||
const out: CondFmtRule[] = [];
|
||||
for (const rule of rules) {
|
||||
const moved = rule.범위.map(shift).filter((r): r is CellRange => r !== null);
|
||||
if (moved.length) out.push({ ...rule, 범위: moved });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function applySheet(ctx: SpreadsheetContext, next: Sheet): void {
|
||||
ctx.dispatch({ 종류: "시트통째", 시트: next });
|
||||
}
|
||||
|
||||
let seq = 0;
|
||||
const newId = (): string => `cf${Date.now().toString(36)}${seq++}`;
|
||||
|
||||
export function addCondFmtRule(ctx: SpreadsheetContext, rule: Omit<CondFmtRule, "id">): void {
|
||||
const sheet = ctx.sheet();
|
||||
applySheet(ctx, {
|
||||
...sheet,
|
||||
조건부서식: [...(sheet.조건부서식 ?? []), { ...rule, id: newId() }],
|
||||
});
|
||||
}
|
||||
|
||||
export function removeCondFmtRule(ctx: SpreadsheetContext, id: string): void {
|
||||
const sheet = ctx.sheet();
|
||||
applySheet(ctx, { ...sheet, 조건부서식: (sheet.조건부서식 ?? []).filter((r) => r.id !== id) });
|
||||
}
|
||||
|
||||
/** 우선순위 한 칸 옮김(엑셀 「규칙 관리」 위 · 아래 화살표와 같음). */
|
||||
export function moveCondFmtRule(ctx: SpreadsheetContext, id: string, dir: -1 | 1): void {
|
||||
const sheet = ctx.sheet();
|
||||
const list = [...(sheet.조건부서식 ?? [])];
|
||||
const idx = list.findIndex((r) => r.id === id);
|
||||
const swapWith = idx + dir;
|
||||
if (idx < 0 || swapWith < 0 || swapWith >= list.length) return;
|
||||
[list[idx], list[swapWith]] = [list[swapWith], list[idx]];
|
||||
applySheet(ctx, { ...sheet, 조건부서식: list });
|
||||
}
|
||||
|
||||
const conditionText = (c: CondFmtCondition): string => {
|
||||
switch (c.종류) {
|
||||
case "비교":
|
||||
return `${c.연산} ${c.값}`;
|
||||
case "사이":
|
||||
return `${c.아래} ~ ${c.위} 사이`;
|
||||
case "글포함":
|
||||
return `「${c.글}」 포함`;
|
||||
case "수식":
|
||||
return `수식 =${c.식}`;
|
||||
}
|
||||
};
|
||||
|
||||
/** 규칙 관리 창 — 지금 시트 규칙 목록 + 고른 범위에 규칙 더하기. 잇는 법 머리 주석 참고. */
|
||||
export function createCondFmtPanel(ctx: SpreadsheetContext): PartHandle {
|
||||
const root = el("div", { className: "ss-condfmt" });
|
||||
const list = el("ul", { className: "ss-condfmt__list" });
|
||||
|
||||
const compareOps: CondFmtCompareOp[] = [
|
||||
"보다큼",
|
||||
"보다작음",
|
||||
"크거나같음",
|
||||
"작거나같음",
|
||||
"같음",
|
||||
"다름",
|
||||
];
|
||||
const kindSelect = createSelectField({
|
||||
label: "조건",
|
||||
options: [
|
||||
{ value: "비교", text: "값 비교" },
|
||||
{ value: "사이", text: "사이" },
|
||||
{ value: "글포함", text: "글 포함" },
|
||||
{ value: "수식", text: "수식이 참" },
|
||||
],
|
||||
});
|
||||
const opSelect = createSelectField({
|
||||
label: "연산",
|
||||
options: compareOps.map((op) => ({ value: op, text: op })),
|
||||
});
|
||||
const val1 = createInputField({ label: "값(또는 아래 · 글 · 수식)" });
|
||||
const val2 = createInputField({ label: "위(사이일 때만)" });
|
||||
const fill = createInputField({ label: "채움색", type: "text", placeholder: "#RRGGBB" });
|
||||
const fontColor = createInputField({ label: "글자색", type: "text", placeholder: "#RRGGBB" });
|
||||
const addBtn = createButton({ label: "규칙 더하기" });
|
||||
|
||||
addBtn.addEventListener("click", () => {
|
||||
const 범위 = ctx.selection.범위;
|
||||
if (!범위.length) return;
|
||||
const 조건 = buildCondition(kindSelect.select.value, opSelect.select.value, val1, val2);
|
||||
if (!조건) return;
|
||||
const 스타일: CondFmtStyle = {};
|
||||
if (fill.input.value) 스타일.채움 = fill.input.value;
|
||||
if (fontColor.input.value) 스타일.글자색 = fontColor.input.value;
|
||||
addCondFmtRule(ctx, { 범위: [...범위], 조건, 스타일 });
|
||||
render();
|
||||
});
|
||||
|
||||
function buildCondition(
|
||||
kind: string,
|
||||
op: string,
|
||||
a: ReturnType<typeof createInputField>,
|
||||
b: ReturnType<typeof createInputField>,
|
||||
): CondFmtCondition | null {
|
||||
if (kind === "비교") {
|
||||
const n = Number(a.input.value);
|
||||
return Number.isFinite(n) ? { 종류: "비교", 연산: op as CondFmtCompareOp, 값: n } : null;
|
||||
}
|
||||
if (kind === "사이") {
|
||||
const 아래 = Number(a.input.value);
|
||||
const 위 = Number(b.input.value);
|
||||
return Number.isFinite(아래) && Number.isFinite(위) ? { 종류: "사이", 아래, 위 } : null;
|
||||
}
|
||||
if (kind === "글포함") return a.input.value ? { 종류: "글포함", 글: a.input.value } : null;
|
||||
if (kind === "수식") return a.input.value ? { 종류: "수식", 식: a.input.value } : null;
|
||||
return null;
|
||||
}
|
||||
|
||||
function render(): void {
|
||||
const rules = ctx.sheet().조건부서식 ?? [];
|
||||
list.replaceChildren(
|
||||
...rules.map((rule, idx) => {
|
||||
const li = el("li", { className: "ss-condfmt__row" });
|
||||
const swatch = el("span", { className: "ss-condfmt__swatch" });
|
||||
if (rule.스타일.채움) swatch.style.background = rule.스타일.채움;
|
||||
const label = el("span", {
|
||||
text: `${rule.범위.map(rangeToA1).join(", ")} — ${conditionText(rule.조건)}`,
|
||||
});
|
||||
const up = createButton({ label: "↑", variant: "ghost" });
|
||||
up.disabled = idx === 0;
|
||||
up.addEventListener("click", () => {
|
||||
moveCondFmtRule(ctx, rule.id, -1);
|
||||
render();
|
||||
});
|
||||
const down = createButton({ label: "↓", variant: "ghost" });
|
||||
down.disabled = idx === rules.length - 1;
|
||||
down.addEventListener("click", () => {
|
||||
moveCondFmtRule(ctx, rule.id, 1);
|
||||
render();
|
||||
});
|
||||
const del = createButton({ label: "지움", variant: "danger" });
|
||||
del.addEventListener("click", () => {
|
||||
removeCondFmtRule(ctx, rule.id);
|
||||
render();
|
||||
});
|
||||
li.append(swatch, label, up, down, del);
|
||||
return li;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const form = el("div", {
|
||||
className: "ss-condfmt__form",
|
||||
children: [
|
||||
kindSelect.root,
|
||||
opSelect.root,
|
||||
val1.root,
|
||||
val2.root,
|
||||
fill.root,
|
||||
fontColor.root,
|
||||
addBtn,
|
||||
],
|
||||
});
|
||||
root.append(el("h4", { text: "조건부 서식 규칙" }), list, form);
|
||||
render();
|
||||
|
||||
return {
|
||||
root,
|
||||
refresh: render,
|
||||
destroy(): void {
|
||||
root.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/* =============================================================================
|
||||
* spreadsheet_stage3.css (주인 sub_laptop_4)
|
||||
* 조건부 서식 · 데이터 유효성 · 이름 정의 관리 창 — 화면 스타일 한 벌. 색 · 간격은 테마 토큰만.
|
||||
* ========================================================================== */
|
||||
|
||||
.ss-condfmt,
|
||||
.ss-validation,
|
||||
.ss-names {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-8);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.ss-condfmt__list,
|
||||
.ss-validation__list,
|
||||
.ss-names__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.ss-condfmt__row,
|
||||
.ss-validation__row,
|
||||
.ss-names__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--spacing-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.ss-condfmt__row > span:first-of-type,
|
||||
.ss-validation__row > span:first-of-type,
|
||||
.ss-names__row > span:first-of-type {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ss-condfmt__swatch {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ss-condfmt__form,
|
||||
.ss-validation__form,
|
||||
.ss-names__form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-4);
|
||||
align-items: flex-end;
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding-top: var(--spacing-8);
|
||||
}
|
||||
|
||||
.ss-validation__dropdown {
|
||||
position: absolute;
|
||||
z-index: 30;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ss-validation__dropdown-item {
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.ss-validation__dropdown-item:hover {
|
||||
background: var(--color-mist-violet);
|
||||
}
|
||||
@@ -48,6 +48,10 @@
|
||||
* E spreadsheet_menu.ts 우클릭 메뉴 ~300
|
||||
* E spreadsheet_clipboard.ts 복사 · 잘라내기 · 붙여넣기(TSV · HTML) ~550
|
||||
* E spreadsheet_toolbar.css ~150
|
||||
* 3 spreadsheet_condfmt.ts 조건부 서식 규칙 · 관리 창(주인 sub_laptop_4 · 안 이어짐) ~260
|
||||
* 3 spreadsheet_validation.ts 데이터 유효성 규칙 · 관리 창(주인 sub_laptop_4 · 안 이어짐) ~230
|
||||
* 3 spreadsheet_names.ts 이름 정의 · 이름 관리 창(주인 sub_laptop_4 · 안 이어짐) ~220
|
||||
* 3 spreadsheet_stage3.css 위 셋 화면 스타일 한 벌 ~90
|
||||
* F common_util/common_util_spreadsheet_node.ts Node 진입(재계산 · 폴더 밖) ~40
|
||||
* F common_util/common_util_spreadsheet.py 파이썬 껍데기(common_util_node_bundle) ~60
|
||||
* F package.json `build:spreadsheet` · M02 `basis` 종류(Store · Layers) · 옛 산출근거 옮김 ·
|
||||
@@ -147,6 +151,13 @@ export interface Sheet {
|
||||
기본?: { 서식?: number; 열폭?: number; 행높이?: number };
|
||||
/** 칸 메모 — A1 열쇠 → 메모 글. 행열 넣기 · 지우기 · 잘라 붙이기 때 칸과 같이 옮김(지운 칸 메모는 지움). */
|
||||
comments?: Record<string, string>;
|
||||
/** 조건부 서식 — 배열 앞쪽이 우선순위 위(`spreadsheet_condfmt.ts`). */
|
||||
조건부서식?: CondFmtRule[];
|
||||
/** 데이터 유효성(`spreadsheet_validation.ts`). */
|
||||
유효성?: DataValidationRule[];
|
||||
/** 이 시트에서 정의한 이름 — 범위는 이 시트 안(`spreadsheet_names.ts`). 식 나무의 `{type:"name"}` 는
|
||||
* A 의 eval 이 `resolveName` 으로 풀어야 함(지금 안 이어짐 — 8-3 절 참고). */
|
||||
이름정의?: NamedRange[];
|
||||
}
|
||||
|
||||
/** 새 시트 기본값 — 값은 사용자 협의로 확정(9_엑셀검토 8장 · 임의 확정 금지). 없으면 A 의 임시값. */
|
||||
@@ -432,3 +443,49 @@ export interface ClipBlock {
|
||||
원점: CellAddress | null;
|
||||
출처: "aislo" | "google" | "excel" | "text";
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// 9. 3단계 — 조건부 서식 · 데이터 유효성 · 이름 정의 (Sheet 확장 · sub_laptop_4 계약 더함 · 안 이어짐)
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export type CondFmtCompareOp =
|
||||
"보다큼" | "보다작음" | "크거나같음" | "작거나같음" | "같음" | "다름";
|
||||
|
||||
export type CondFmtCondition =
|
||||
| { 종류: "비교"; 연산: CondFmtCompareOp; 값: number }
|
||||
| { 종류: "사이"; 아래: number; 위: number }
|
||||
| { 종류: "글포함"; 글: string }
|
||||
/** 임의 수식 글 — A 의 파서 · eval 이 있어야 풀림(`spreadsheet_condfmt.ts` 머리 「잇는 법」). */
|
||||
| { 종류: "수식"; 식: string };
|
||||
|
||||
/** 조건부 서식이 먹이는 스타일 — `서식` 표 번호를 안 씀(규칙마다 직접 가짐). */
|
||||
export interface CondFmtStyle {
|
||||
채움?: string;
|
||||
글자색?: string;
|
||||
굵게?: boolean;
|
||||
}
|
||||
|
||||
export interface CondFmtRule {
|
||||
id: string;
|
||||
범위: CellRange[];
|
||||
조건: CondFmtCondition;
|
||||
스타일: CondFmtStyle;
|
||||
}
|
||||
|
||||
export type DataValidationKind =
|
||||
{ 종류: "목록"; 값들: string[] } | { 종류: "수범위"; 아래?: number; 위?: number };
|
||||
|
||||
export interface DataValidationRule {
|
||||
id: string;
|
||||
범위: CellRange[];
|
||||
규칙: DataValidationKind;
|
||||
/** 어긴 값을 넣으려 하면 막을지 · 경고만 하고 넣을지(둘 다 D 편집기 잇기 몫 · 지금 안 이어짐). */
|
||||
어기면: "막음" | "경고";
|
||||
메시지?: string;
|
||||
}
|
||||
|
||||
/** 이름 정의 — 이 이름을 정의한 시트 안 범위만 가리킴(다른 시트를 가리키는 이름은 3단계 밖). */
|
||||
export interface NamedRange {
|
||||
이름: string;
|
||||
범위: CellRange;
|
||||
}
|
||||
|
||||
@@ -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 시험 끝");
|
||||
Reference in New Issue
Block a user