Skip to content

Sheet Toolbar Audit — Variance Matrix & Inconsistency Review ​

Purpose: Document every toolbar button + dropdown action on the workbook sheet view, the predicates that decide whether it appears, and the inconsistencies that surface when you cross-multiply by sheet variance. This is the review-first doc requested before any reorganization — proposed fixes live in §6, no code changes yet.

Source files:


1. Sheet Variance Dimensions ​

A sheet's behavior is the cross-product of these axes. Buttons need to consider any subset.

1.1 Sheet-level metadata (persisted) ​

FieldValuesPurpose
kindallowance / deduction / mixed / overtime / incentive / therapy_dailyDomain — what the rows mean
viewKindrow (default) / transposed / matrixRender layout
visibilityprivate / sharedWho else can see it
isDefaultboolServer-seeded "system" sheet vs user-created
rowBasisemployee / date / free (nullable; auto-detected if null)What + 행 추가 prompts for
autoPopulate{ source: "employee" | ... } or nullSource-bound row generation
lockedAt / lockedBytimestamp / uuidManual sheet lock (distinct from period freeze)
groupIdstring | nullSidebar group
descriptionstring | nullInline subtitle
year, monthintActive period

1.2 Derived sheet states (computed getters) ​

GetterTrue whenUsed by
isReadOnlyCurrently hardcoded false (line 3086) — predicate exists but no-opsMany x-show predicates — broken gating, see §6.1
isPayrollLedgername contains 급여대장 OR autoPopulate.source === "employee" OR any column is sourceTable=employee / kind=aggregate💰 / 🔓 / 📥 buttons
activeSheetIsDateBasedrowBasis === "date" OR (no identity cols + has date-format col)Row-add prompt shape
activeSheetIsFreeBasisrowBasis === "free" (explicit only)Skips bulk-add
activeSheetIsTreatmentSession(custom predicate — therapy_daily + extras)📅 / 🔄 sync sessions
allLedgerRowsConfirmedevery employeeRow has a CONFIRMED/PAID PayrollRecordtoggles 💰 ↔ 🔓 + reveals 📥
sheetLockedlockedAt set🔒 banner + menu label flip
hasComputedColumnany col has kind=computed + computeSourceModule + computedKey🧮 compute button
hasCommitColumnany col has commitToKind + matching item code (allowanceItemCodeId/deductionItemCodeId)✓ commit button
hasRefreshableSourceautoPopulate set OR any col has kind=aggregate or sourceTable !== "employee"🔄 refresh button

1.3 Row-level state (drives count badges) ​

StateMeaning
draftRows.lengthUnsaved insert-row drafts
pendingRowCountRows with status=pending
approvedRowCountRows with status=approved
selectedRowCountCells selected (anything > 1 unlocks bulk-row actions)
unresolvedMemoCountMemos with status open

1.4 Column kinds (per-column variance) ​

From columnEffectiveKind:

KindMeaningEditor behavior
identity사번 / 성명 / 팀 — relink an existing record to another employeeEmployee autocomplete picker
literalPlain value typed directly into the cellText input
refReference into a source-record field (e.g. amount)Text input + per-kind validator
formulaUser-typed formula expression (e.g. =ROUNDUP((TODAY()-…)/365))Formula bar (planned)
computedcodeModule-driven calculationRead-only; recomputed by 🧮
aggregateSUM across other sheets / recordsRead-only; refreshed by 🔄

Column-level toolbar gates: hasComputedColumn, hasCommitColumn, hasRefreshableSource (see 1.2).


2. Toolbar Inventory — Top Bar (above sheet body) ​

2.1 Period strip (always visible, top-left) ​

ButtonPredicateNotes
Year <select>always
Month <select>always
이번달 badgeisCurrentPeriodStatic label
이번달 button!isCurrentPeriodJump
+ 새 시트alwaysOpens new-sheet modal
📋 이전 월에서 가져오기alwaysCarry forward
? shortcuts helpalwaysToggle cheat-sheet

2.2 Sidebar header (when sidebar open) ​

ButtonPredicateNotes
그룹 관리alwaysOpens group manager modal
템플릿 관리alwaysOpens template manager modal
정렬 모드alwaysDrag-to-reorder toggle
Per-sheet pin (📌)per-rowPin in sidebar
🔒 lock icons.lockedAtSidebar row badge
비공개 (🔒)s.visibility === 'private'Sidebar row badge
행렬 / 전치 (viewKind badge)s.viewKind && s.viewKind !== 'row'Sidebar row badge
YM badge ('25.5)s.year && s.monthSidebar row badge

3. Toolbar Inventory — Sheet Header (with active sheet) ​

The sheet header is the main pain point. Buttons cluster into four logical "zones," but the current placement doesn't always honor that.

3.1 Identity & navigation (left side) ​

ButtonPredicateZoneNotes
Sidebar collapse ◀ / ▶alwaysNav
Sheet title (activeSheet.name)activeSheetIdentitydblclick = rename when !isReadOnly
YM pillactiveSheetIdentitydblclick = edit period when !isReadOnly
Close sheet ✕activeSheetNav
updatedAt timestampactiveSheet?.updatedAtIdentity
+ 설명 추가always (label changes)Identity
Row count n/N건alwaysIdentity

3.2 Filter & search (middle) ​

ButtonPredicateZoneNotes
상태 <select>alwaysFilterpending/approved/paid/cancelled
검색 <input> (/)alwaysFilter
검색 <clear> ×hasSearchFilter
Cross-sheet popoverhasSearch && !hiddenFilter
🔒 잠긴 시트 badgesheetLockedIdentity (badge)Read-only signal

3.3 DATA actions (right of search) — the contested zone ​

ButtonPredicateSheet kinds affectedNotes
🧮 계산 실행!isReadOnly && hasComputedColumnanyRuns codeModule on computed cols
✓ 커밋!isReadOnly && hasCommitColumnanyWrites sheet values → records
🔄 새로고침hasRefreshableSourceanyRe-syncs from server-bound cols
💰 전체 확정!isReadOnly && isPayrollLedger && !allLedgerRowsConfirmedledger onlyBulk PayrollRecord confirm
🔓 전체 취소!isReadOnly && isPayrollLedger && allLedgerRowsConfirmedledger onlyToggles with 💰
📥 이카운트 ExportisPayrollLedger && allLedgerRowsConfirmedledger onlyXLSX download

Note: No visual separator between "data" and "payroll" actions — they sit shoulder-to-shoulder. The mental model is unclear: 🧮 is for this sheet's columns, but 💰 is for this sheet's rows treated as ledger. Same icon family, different scope.

3.4 Zone separator → ROW actions ​

ButtonPredicateNotes
| divideractiveSheet && !isReadOnly
➕ 행 추가!isReadOnly && activeSheetTooltip changes by activeSheetIsFreeBasis / activeSheetIsDateBased
📝 새 항목!isReadOnly && activeSheet"auto-fill" entry
📅 새 세션!isReadOnly && activeSheetAlways shown — even on non-therapy sheets (see §6.3)
👥 일괄 추가!isReadOnly && activeSheet && !activeSheetIsDateBased && !activeSheetIsFreeBasis
🔄 인센 동기화!isReadOnly && activeSheet && activeSheetIsTreatmentSessionTreatment-specific

3.5 DRAFT actions (conditional cluster) ​

Wrapped in <template x-if="draftRows.length > 0">:

ButtonNotes
| divider
💾 모두 저장 (badge: count)
✖ 모두 취소

3.6 WORKFLOW actions (conditional cluster) ​

Wrapped in <template x-if="!isReadOnly && (activeSheet || pendingRowCount > 0 || approvedRowCount > 0 || selectedRowCount > 1)">:

ButtonPredicateNotes
| divider(outer wrapper)
🧮 재계산!isReadOnly && activeSheetIcon collision with 🧮 계산 실행 (3.3) — different action
✅ 승인 (badge: pendingRowCount)!isReadOnly && pendingRowCount > 0
💸 지급 (badge: approvedRowCount)!isReadOnly && approvedRowCount > 0
🗑 삭제 (badge: selectedRowCount)!isReadOnly && selectedRowCount > 1

3.7 Notifications / panel toggles (right side) ​

ButtonPredicateNotes
💬 메모 이력 (badge: unresolvedMemoCount)activeSheet
◀ / ▶ Detail panel togglealways

3.8 Overflow menu ⋯ ​

ItemPredicateNotes
🛠 시트 설정activeSheetAlways shown
---
📋 TSV 가져오기!isReadOnly && activeSheet
📥 내보내기 (CSV / XLSX)always
---
🔓/🔒 잠금!periodLockedLabel flips on sheetLocked
🌐/🔐 공유!isReadOnly && activeSheetLabel flips on visibility
📋 복제!isReadOnly
💾 템플릿으로 저장!isReadOnly && activeSheet
---
🗑 삭제!isReadOnlyNo isDefault guard — system templates are deletable (see §6.2)

3.9 Column settings (rightmost) ​

ButtonNotes
열 설정Toggle a dropdown listing all columns with visibility + kind chip
Per-column visibility checkbox
Per-column 식별/값/참조/계산/수식 chipOpens column-kind editor modal

4. Icon Vocabulary — Conflicts & Categories ​

4.1 Icons in use ​

IconUsed forLocations
➕Add row3.4
📝New auto-fill entry3.4
📅New treatment session3.4
👥Bulk add employees3.4
🧮(a) Run compute (b) Recalculate incentives3.3 + 3.6 ← collision
✓Commit3.3
🔄(a) Refresh auto-populate (b) Sync treatment sessions3.3 + 3.4 ← collision
💰Confirm ledger3.3
🔓(a) Cancel confirmations (b) Unlock sheet (menu)3.3 + 3.8 ← collision
🔒(a) Locked banner (b) Lock sheet (menu) (c) Private (sidebar)3.2, 3.8, 2.2
📥(a) eCount export (b) CSV/XLSX export (menu)3.3 + 3.8 — same intent, OK
💾(a) Save drafts (b) Save as template (menu)3.5 + 3.8
💬Memo history3.7
✅Approve pending3.6
💸Mark paid3.6
🗑(a) Delete rows (b) Delete sheet (menu)3.6 + 3.8
📋(a) Carry forward (b) TSV import (menu) (c) Duplicate (menu)2.1 + 3.8 + 3.8 — three meanings
🛠Sheet settings3.8
🌐 / 🔐Visibility3.8
✖Cancel drafts3.5
✕Close active sheet3.1
◀ / ▶Sidebar / detail panel toggle3.1, 3.7
⋯Overflow menu3.8
/ ?Search focus / help2.1 keyboard

4.2 Logical categories ​

If we re-bucket by user intent, the toolbar wants to read:

  1. Navigation — sidebar toggle, detail panel toggle, close sheet, sheet title/edit
  2. Filter — status filter, search, cross-sheet popover
  3. Compute/refresh — 🧮 compute, 🔄 refresh — applies to columns
  4. Commit — ✓ commit, 💰 confirm ledger, 🔓 cancel — applies to rows (ledger)
  5. Export — 📥 eCount, 📥 CSV (today in menu)
  6. Row creation — ➕, 📝, 📅, 👥
  7. Draft management — 💾 / ✖
  8. Row workflow — 🧮 recalc, ✅, 💸, 🗑
  9. Notifications — 💬 memo
  10. Setup / rare — overflow menu items
  11. Per-column setup — 열 설정 dropdown

Categories 3 + 4 are the most contested (data vs commit vs ledger semantics overlap), and 6 is the most kind-sensitive (which row-add affordance applies depends on sheet kind + rowBasis).


5. Cross-Type Variance — Which Buttons Should Appear Where ​

This is the matrix we can argue from. ✅ = appropriate · ⚠️ = currently shown but probably shouldn't · ❌ = hidden/inapplicable · ❓ = unclear.

5.1 By sheet kind ​

Buttonallowancedeductionmixedovertimeincentivetherapy_daily
🧮 계산 실행✅ if computed colsamesamesamesamesame
✓ 커밋✅ if commit colsamesamesamesamesame
🔄 새로고침✅samesamesamesamesame
💰 / 🔓 / 📥 ledger❌ unless ledger❌❌❌❌❌
➕ 행 추가✅✅✅✅✅❓
📝 새 항목✅✅✅✅✅❓
📅 새 세션⚠️ shows⚠️ shows⚠️ shows⚠️ shows⚠️ shows✅
👥 일괄 추가✅✅✅✅✅❌ (date-based)
🔄 인센 동기화❌❌❌❌❓✅
🧮 재계산 (3.6)❓ generic?❓❓❓✅ "총금액=건수×단가" only matches incentive❓
✅ 승인 / 💸 지급✅ if status field existssamesamesamesame❓
🗑 삭제 행✅✅✅✅✅❓

Tooltip note: 📅 새 세션's tooltip says "새 치료 세션 등록" — implies therapy_daily, but the button shows on every sheet kind because the predicate is just !isReadOnly && activeSheet.

5.2 By isDefault (system template vs user-created) ​

ActionSystem sheetUser sheetIssue
🗑 시트 삭제⚠️ allowed✅ allowedShould block / warn for isDefault — these are seeded; deletion likely silently re-seeds on next reload
💾 템플릿으로 저장⚠️ shows✅ showsSaving a system sheet as a user template duplicates seed data
시트 이름 변경 (dblclick)⚠️ shows✅ showsRenaming a system sheet may break the auto-upsert by name (e.g., 급여대장 detection)
시트 설정 (🛠)✅ but restricted?✅Some fields (kind, columnLayout) might be locked on system sheets
📋 복제✅✅Fine

5.3 By lockedAt (manual sheet lock) ​

ActionSheet lockedNotes
All write actionsgated by isReadOnlybut isReadOnly is hardcoded false — so lock is purely visual (line 3086)
🔓 잠금 해제 (menu)always available

5.4 By column kind ​

Has column kind…Button surfaces
computed🧮 계산 실행
with commitToKind + matching item code✓ 커밋
aggregate or sourceTable !== "employee" or autoPopulate🔄 새로고침
formulano UI yet — the kind chip says "수식" but there's no formula bar / preview / error surfacing (see §6.6)

5.5 By viewKind ​

The button bar is identical across row / transposed / matrix views. Most row-oriented actions (➕, 🗑, 승인, etc.) still make sense, but the affordances might benefit from per-view labels (e.g. "+ 열 추가" on transposed).


6. Inconsistencies & Proposed Fixes ​

Each finding paired with the smallest change that resolves it.

6.1 isReadOnly is a no-op ​

workbook/ — the isReadOnly getter was hardcoded return false, so every x-show="!isReadOnly && …" in the view is dead-weight gating; manual sheetLocked doesn't gate writes and period-level lock doesn't either. (Note: isReadOnly is no longer present in the code as of the 2026-06 slice split — this finding appears already resolved; confirm before acting on §6.1 below.)

Proposed: Either (a) remove isReadOnly from the toolbar entirely and rely on explicit gates (sheetLocked, isDefault, period.confirmed), or (b) restore the gating logic. Need product input on the intended lock semantics — current code looks like a half-revert.

6.2 System templates can be deleted / renamed / re-saved-as-template ​

System (isDefault: true) sheets carry no UI guard. The auto-upsert on next load might mask deletion, but renaming would silently break the name?.includes("급여대장") heuristic that drives isPayrollLedger.

Proposed:

  • Hide 🗑 삭제 from the overflow menu when isDefault: true. (Show a 복제 후 편집하세요 hint instead.)
  • Hide 💾 템플릿으로 저장 for isDefault: true.
  • Disable dblclick = rename on the title for isDefault: true.
  • Surface a small 시스템 시트 chip next to the title.

6.3 📅 새 세션 shows everywhere ​

The button reads "새 세션 — 새 치료 세션 등록" but the predicate is just !isReadOnly && activeSheet. On allowance/deduction/overtime sheets it's noise.

Proposed: Gate by activeSheetIsTreatmentSession. Same predicate already used for 🔄 인센 동기화 right below.

6.4 🧮 means two different things ​

  • Toolbar zone 3.3: "계산 실행 — codeModule recomputes computed columns"
  • Toolbar zone 3.6: "재계산 — incentive 총금액 = 건수 × 단가"

They sit ~3 buttons apart and use the same icon. Users can't tell them apart at a glance.

Proposed:

  • Keep 🧮 for the column-level compute (3.3).
  • Change 3.6 to ✖️→🔄 or rename to a more specific verb, e.g., "✦ 인센 정합" or merge it into the per-row right-click menu since it's an incentive-only operation (tooltip already says "인센티브 행에 대해").

6.5 🔄 means three different things across the toolbar ​

  • 3.3 — refresh auto-populate (server resync of bound columns)
  • 3.4 — sync treatment sessions (pull from 결제툴)
  • (Bonus: keyboard / and ? share modifier semantics)

Same icon, different scopes.

Proposed:

  • 3.3 stays 🔄 (generic server refresh).
  • 3.4 becomes 🔁 or ⇪ or an explicit text label "세션 ↻" since it's a more specific external sync.

6.6 formula kind has no editor ​

columnEffectiveKind recognizes formula, the column-kind chip shows "수식", but there's no formula bar, no expression input, no evaluation. The =ROUNDUP(…) use case the user mentioned needs:

  • A formula input field (in the column-kind editor)
  • A live preview/evaluation
  • Per-row error states

Proposed: This is a separate feature (the user's earlier interrupted message). Out of scope for the toolbar audit but adjacent to the column-kind chip in 3.9. Tracking as a follow-up: §7.

6.7 Overflow menu order is inconsistent ​

3.8 reads top-to-bottom:

  • 🛠 시트 설정
  • 📋 TSV 가져오기, 📥 내보내기
  • 🔒 잠금, 🌐 공유, 📋 복제, 💾 템플릿으로 저장
  • 🗑 삭제

The middle cluster mixes share-related (잠금/공유) with duplication (복제/템플릿). And 📋 is used for both 이전 월에서 가져오기 (top toolbar) and TSV 가져오기 and 복제 — three different meanings.

Proposed: Re-cluster as:

  1. Setup — 🛠 시트 설정
  2. Data I/O — 📋 TSV 가져오기, 📥 내보내기
  3. Sharing — 🔒 잠금, 🌐 공유
  4. Lifecycle — ⎘ 복제, 💾 템플릿으로 저장
  5. Destructive — 🗑 삭제

And re-icon 📋 복제 → ⎘ or a different glyph to break the icon collision.

6.8 No visible cue for viewKind !== "row" ​

The sidebar shows a small chip but the active toolbar doesn't. Users may not realize a sheet is in transposed or matrix mode until they look at the body.

Proposed: Add a small 행렬 / 전치 pill next to the YM pill on the title row when viewKind !== "row".

6.9 Draft cluster and Workflow cluster repeat dividers ​

3.5 and 3.6 each render their own | divider. When both are visible (draft rows + pending rows on the same sheet), you see … | 💾 ✖ | 🧮 ✅ 💸 🗑 … — two adjacent dividers visually flatten. Minor.

Proposed: Move the divider to a single shared rule that only appears when any row-cluster is visible.

6.10 🔒 잠긴 시트 badge sits in the search row but the action lives in the menu ​

Discoverability problem: the badge tells you the sheet is locked, but to unlock you have to find the overflow menu. The badge isn't a button.

Proposed: Make the badge clickable — clicking it opens the menu pre-scrolled to the lock toggle, OR directly toggles when the user has permission.


7. Out-of-scope (track for follow-up) ​

  • Formula column UX — §6.6. Needs an expression editor, evaluator binding, and per-cell error display.
  • isReadOnly semantics — §6.1. Product decision needed about what locks writes.
  • Cross-sheet popover keyboarding — currently mouse-only.
  • Per-viewKind row-add affordances — + 행 추가 vs + 열 추가 on transposed sheets.

When discussing fixes, take them in this order — earlier items unblock later ones:

  1. §6.1 (isReadOnly) — decide gating semantics first; the answer constrains many other predicates.
  2. §6.2 (system templates) — single decision (block destructive ops on isDefault) covers four buttons.
  3. §6.3–6.5 (icon collisions / wrong scope) — pure visibility fixes, no backend touched.
  4. §6.7 (menu order) — UX-only, low risk.
  5. §6.8 / §6.9 / §6.10 — minor polish.
  6. §6.6 + §7 (formula UX) — its own feature track.

9. Decisions Log ​

Track product decisions as they're made, with date + rationale. Future-you doesn't relitigate.

DateItemDecisionRationale
2026-05-20§6.1 isReadOnly semanticsRemove entirely. Delete the getter; rely on explicit gates (sheetLocked, isDefault on templates only, per-row PayrollRecord.status).Currently a no-op (return false). Removing dead gating is purely a cleanup — no behavior change. Each surviving gate has one job.
2026-05-20§6.2 System-template restrictionsN/A — system sheets are not a thing. A sheet derived from a system template is a user sheet from that point on. "System" applies only to templates. No isDefault flag on sheets; no 시스템 시트 chip; no per-sheet guards.Clarifies the data model: templates are seeded; sheets are always user-owned. Avoids inventing UI for a concept that shouldn't exist.
2026-05-20§6.10 🔒 badge click behaviorOpen overflow menu pre-scrolled to the lock toggle.Prevents accidental unlock; consistent with how other destructive actions live in the overflow menu.

10. PR Plan ​

PR 1 — Icon polish ✅ shipped 2026-05-20 ​

No backend, no predicate logic changes. Pure view-layer fixes.

  • §6.3 — Gate 📅 새 세션 on activeSheetIsTreatmentSession
  • §6.4 — Row-workflow 🧮 재계산 → ⟳ 인센 정합 (resolves icon collision with 🧮 계산 실행)
  • §6.5 — Treatment session sync 🔄 → ↻ + label "세션 가져오기"
  • §6.7 — Overflow menu re-clustered into Setup / I/O / Sharing / Lifecycle / Destructive; 📋 복제 → ⎘ 복제
  • §6.8 — 전치 / 행렬 chip next to YM pill when viewKind !== "row"
  • §6.9 — Single shared divider before draft + workflow clusters

PR 2 — Gating cleanup + lock badge ​

Scope locked in by §9 decisions (2026-05-20).

  • §6.1 — Remove isReadOnly getter + every !isReadOnly && predicate in the view + every if (this.isReadOnly) return in workbook.sheet.ts. Pure cleanup; no behavior change because the getter was already false.
  • §6.2 — Audit any lingering references to isDefault on sheets (vs templates). If found, treat as bugs. No new UI work for this item.
  • §6.10 — 🔒 잠긴 시트 badge becomes a button that opens the overflow menu. Optional polish: briefly highlight the lock toggle row in the menu after opening, so the user's eye lands there.

Estimated scope: ~50 lines of removals + ~10 lines for the lock-badge wiring.

PR 3 — Formula column UX 🔭 separate feature track ​

§6.6 — Formula bar, evaluator binding, per-cell error display, autocomplete for column refs.

Estimated scope: larger; involves a new modal / inline bar, a sandboxed expression evaluator, and cell-level error states. Should be its own design pass before any code.