diff --git a/A00_Common/spreadsheet/spreadsheet_sheet_extra.ts b/A00_Common/spreadsheet/spreadsheet_sheet_extra.ts new file mode 100644 index 00000000..a254ed7d --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_sheet_extra.ts @@ -0,0 +1,113 @@ +/* ============================================================================= + * spreadsheet_sheet_extra.ts (3단계 · 주인 sub_laptop_4) + * 시트 숨기기 · 보이기 · 시트 탭 색. 저장 자리 `Sheet.숨김` · `Sheet.탭색`(0 계약에 방금 더함). + * 이 파일은 관리 창(전체 시트 목록 + 숨김 · 색 조작)만 그림 — 실제 탭 줄(`spreadsheet_tabs.ts`, + * E 파일)을 안 건드림. + * + * 잇는 법(E `spreadsheet_tabs.ts` · sub7 — 지금은 하나도 안 이어짐): + * 1) `mountTabs` 의 `render()` 가 `ctx.book.시트` 를 돌 때 `숨김` 인 시트는 탭을 안 그림. + * 2) 탭 엘리먼트에 `sheet.탭색` 이 있으면 밑줄 · 배경을 그 색으로(엑셀처럼). + * 3) 숨은 시트를 다시 보이려면 탭 우클릭 메뉴에 [숨긴 시트 보이기] 항목을 두고 이 파일의 + * `createSheetExtraPanel(ctx)` 를 열거나, `showSheet(ctx, id)` 를 바로 부름. + * ========================================================================== */ + +import { createButton, el } from "@ui/ui_template_elements"; +import type { Sheet } from "./spreadsheet_types"; +import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types"; + +// css 는 stage3.css 에 얹음(잇는 법 머리 참고) — 실제로 붙일 때 `import "./spreadsheet_stage3.css";` 더함. + +function applySheet(ctx: SpreadsheetContext, sheetId: string, next: Sheet): void { + ctx.dispatch({ 종류: "시트통째", 시트: { ...next, id: sheetId } }); +} + +/** 시트를 숨김 — 마지막 보이는 시트는 못 숨김(까닭 글 돌려줌 · 되면 null). 활성 시트를 숨기면 + * 다른 보이는 시트로 활성을 옮김. */ +export function hideSheet(ctx: SpreadsheetContext, id: string): string | null { + const others = ctx.book.시트.filter((s) => s.id !== id && !s.숨김); + if (!others.length) return "마지막 보이는 시트는 못 숨김"; + const sheet = ctx.book.시트.find((s) => s.id === id); + if (!sheet || sheet.숨김) return null; + applySheet(ctx, id, { ...sheet, 숨김: true }); + if (ctx.book.활성 === id) ctx.showSheet(others[0].id); + return null; +} + +/** 숨긴 시트를 다시 보임. */ +export function unhideSheet(ctx: SpreadsheetContext, id: string): void { + const sheet = ctx.book.시트.find((s) => s.id === id); + if (!sheet || !sheet.숨김) return; + applySheet(ctx, id, { ...sheet, 숨김: false }); +} + +/** 탭 색 — `color` 가 null 이면 색을 지움(기본 탭 색으로). */ +export function setSheetTabColor(ctx: SpreadsheetContext, id: string, color: string | null): void { + const sheet = ctx.book.시트.find((s) => s.id === id); + if (!sheet) return; + const next: Sheet = { ...sheet }; + if (color) next.탭색 = color; + else delete next.탭색; + applySheet(ctx, id, next); +} + +/** 시트 관리 창 — 전체 시트 목록 + 숨김 토글 · 탭 색 고르기. */ +export function createSheetExtraPanel(ctx: SpreadsheetContext): PartHandle { + const root = el("div", { className: "ss-sheetextra" }); + const list = el("ul", { className: "ss-sheetextra__list" }); + let error: HTMLElement | null = null; + + const showError = (msg: string | null): void => { + error?.remove(); + error = null; + if (!msg) return; + error = el("p", { text: msg }); + error.style.color = "var(--color-danger, red)"; + root.append(error); + }; + + function render(): void { + list.replaceChildren( + ...ctx.book.시트.map((sheet) => { + const li = el("li", { className: "ss-sheetextra__row" }); + const label = el("span", { text: sheet.이름 + (sheet.숨김 ? "(숨김)" : "") }); + const colorInput = el("input", { + attrs: { type: "color", value: sheet.탭색 ?? "#ffffff" }, + }) as HTMLInputElement; + colorInput.addEventListener("change", () => + setSheetTabColor(ctx, sheet.id, colorInput.value), + ); + const colorClear = createButton({ label: "색 지움", variant: "ghost" }); + colorClear.addEventListener("click", () => { + setSheetTabColor(ctx, sheet.id, null); + render(); + }); + const toggle = createButton({ + label: sheet.숨김 ? "보이기" : "숨기기", + variant: "ghost", + }); + toggle.addEventListener("click", () => { + if (sheet.숨김) { + unhideSheet(ctx, sheet.id); + showError(null); + } else { + showError(hideSheet(ctx, sheet.id)); + } + render(); + }); + li.append(label, colorInput, colorClear, toggle); + return li; + }), + ); + } + + root.append(el("h4", { text: "시트 관리" }), list); + render(); + + return { + root, + refresh: render, + destroy(): void { + root.remove(); + }, + }; +} diff --git a/A00_Common/spreadsheet/spreadsheet_stage3.css b/A00_Common/spreadsheet/spreadsheet_stage3.css index 9f7d71cd..68cf46de 100644 --- a/A00_Common/spreadsheet/spreadsheet_stage3.css +++ b/A00_Common/spreadsheet/spreadsheet_stage3.css @@ -1,11 +1,13 @@ /* ============================================================================= * spreadsheet_stage3.css (주인 sub_laptop_4) - * 조건부 서식 · 데이터 유효성 · 이름 정의 관리 창 — 화면 스타일 한 벌. 색 · 간격은 테마 토큰만. + * 조건부 서식 · 데이터 유효성 · 이름 정의 · 상태 표시줄 · 시트 관리 창 — 화면 스타일 한 벌. + * 색 · 간격은 테마 토큰만. * ========================================================================== */ .ss-condfmt, .ss-validation, -.ss-names { +.ss-names, +.ss-sheetextra { display: flex; flex-direction: column; gap: var(--spacing-8); @@ -18,7 +20,8 @@ .ss-condfmt__list, .ss-validation__list, -.ss-names__list { +.ss-names__list, +.ss-sheetextra__list { list-style: none; margin: 0; padding: 0; @@ -29,7 +32,8 @@ .ss-condfmt__row, .ss-validation__row, -.ss-names__row { +.ss-names__row, +.ss-sheetextra__row { display: flex; align-items: center; gap: var(--spacing-4); @@ -85,3 +89,23 @@ .ss-validation__dropdown-item:hover { background: var(--color-mist-violet); } + +.ss-statusbar { + display: flex; + align-items: center; + gap: var(--spacing-8); + padding: 2px var(--spacing-8); + background: var(--color-surface); + border-top: 1px solid var(--color-border); + font-size: 0.8rem; +} + +.ss-statusbar__stats { + display: flex; + gap: var(--spacing-8); + flex: 1; +} + +.ss-statusbar__zoom { + color: var(--color-border); +} diff --git a/A00_Common/spreadsheet/spreadsheet_types.ts b/A00_Common/spreadsheet/spreadsheet_types.ts index 94d6f73c..1b344892 100644 --- a/A00_Common/spreadsheet/spreadsheet_types.ts +++ b/A00_Common/spreadsheet/spreadsheet_types.ts @@ -55,7 +55,10 @@ * 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 + * 3 spreadsheet_statusbar.ts 아래 상태 표시줄 — 합계 · 평균 · 개수 등(주인 sub_laptop_4 · 안 이어짐) ~150 + * 3 spreadsheet_autosum.ts 자동 합계(Alt+= · Σ, 주인 sub_laptop_4 · 안 이어짐) ~90 + * 3 spreadsheet_sheet_extra.ts 시트 숨김 · 보이기 · 탭 색(주인 sub_laptop_4 · 계약 더함 · 안 이어짐) ~150 + * 3 spreadsheet_stage3.css 위 여섯 화면 스타일 한 벌 ~140 * 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) · 옛 산출근거 옮김 · @@ -172,6 +175,10 @@ export interface Sheet { /** 이 시트에서 정의한 이름 — 범위는 이 시트 안(`spreadsheet_names.ts`). 식 나무의 `{type:"name"}` 는 * A 의 eval 이 `resolveName` 으로 풀어야 함(지금 안 이어짐 — 8-3 절 참고). */ 이름정의?: NamedRange[]; + /** 시트 숨김(`spreadsheet_sheet_extra.ts`) — 탭은 `spreadsheet_tabs.ts`(E)가 이 값으로 걸러야 함(안 이어짐). */ + 숨김?: boolean; + /** 시트 탭 색 `#RRGGBB`(`spreadsheet_sheet_extra.ts`) — 탭 칠은 E 몫(안 이어짐). */ + 탭색?: string; } /** 새 시트 기본값 — 값은 사용자 협의로 확정(9_엑셀검토 8장 · 임의 확정 금지). 없으면 A 의 임시값. */ diff --git a/resources/tester/spreadsheet/test_stage3_sheet_extra.ts b/resources/tester/spreadsheet/test_stage3_sheet_extra.ts new file mode 100644 index 00000000..529fde3c --- /dev/null +++ b/resources/tester/spreadsheet/test_stage3_sheet_extra.ts @@ -0,0 +1,36 @@ +/* ============================================================================= + * test_stage3_sheet_extra.ts — spreadsheet_sheet_extra 시험. 돌리기: npx tsx <이 파일>. + * ========================================================================== */ + +import { + hideSheet, + setSheetTabColor, + unhideSheet, +} from "../../../A00_Common/spreadsheet/spreadsheet_sheet_extra.ts"; +import { assertEqual, makeBook, makeCtx, makeSheet } from "./test_stage2_helpers.ts"; + +const s1 = makeSheet("s1", {}); +const s2 = makeSheet("s2", {}); +const book = makeBook([s1, s2]); +book.활성 = "s1"; +const shown: string[] = []; +const ctx = { ...makeCtx(book, "s1"), showSheet: (id: string) => shown.push(id) }; + +assertEqual(hideSheet(ctx, "s1"), null, "hideSheet 성공 — 까닭 없음"); +assertEqual(book.시트[0].숨김, true, "s1 이 숨겨짐"); +assertEqual(ctx.applied.at(-1)?.종류, "시트통째", "hideSheet 은 시트통째로 나감"); +assertEqual(shown, ["s2"], "활성 시트를 숨기면 다른 보이는 시트로 옮김"); + +assertEqual(hideSheet(ctx, "s2"), "마지막 보이는 시트는 못 숨김", "마지막 보이는 시트는 거절"); +assertEqual(book.시트[1].숨김, undefined, "거절되면 안 바뀜"); + +unhideSheet(ctx, "s1"); +assertEqual(book.시트[0].숨김, false, "unhideSheet 로 다시 보임"); + +setSheetTabColor(ctx, "s2", "#ff0000"); +assertEqual(book.시트[1].탭색, "#ff0000", "탭 색 지정"); +setSheetTabColor(ctx, "s2", null); +assertEqual(book.시트[1].탭색, undefined, "탭 색 지움"); + +if (process.exitCode) process.exit(process.exitCode); +console.log("spreadsheet_sheet_extra 시험 끝");