Sheet Architecture
How the sheet (workbook) system is implemented end-to-end — from the Alpine.js grid in hr-system down to the payroll records it produces. Sheets are the primary data-entry and payroll-assembly surface: every allowance/deduction that reaches the 급여대장 (payroll ledger) is born as a sheet commit.
Source anchors are listed in File Map. For the payroll record model itself see the Payroll Schema Refactor reference and the Payroll rules in
CLAUDE.md.
1. Layered Overview
Key idea: sheets are a projection layer. Source tables hold observations (hours, sessions, statuses); sheet_cells hold only user edits and cached compute results; monetary outcomes live exclusively in allowance_records / deduction_records, which are written only by sheet commit (✓ 커밋) and always carry sheet lineage.
2. Data Model
Column kinds (columnLayout[].kind)
| kind | Meaning | Value lives in | Editable |
|---|---|---|---|
identity | 사번/이름/팀 from row.employeeId join | — (derived) | no |
literal | User-typed value | SheetCell.literalValue | yes |
ref | Pointer to a source record field | SheetCell.ref*RecordId → record | limited |
formula | =SUM(...)-style expression | formulaExpression + cached computedValue | expression only |
computed | Code-module output (e.g. insurance calc) | computedValue + inputSnapshot | no (manual override skips) |
aggregate | SUM of committed records — 급여대장 columns | never stored, derived at read | no |
multiselect / employeeselect / treatmentcount | Special editors | literalValue | yes |
Row resolution order (resolver.ts)
For each row × column, the resolver picks the first available value:
Manual edits therefore shadow source values without mutating them; clearing the cell restores the source fall-through.
3. Sheet Lifecycle in a Month
4. Key Flows (sequence)
4.1 Opening a sheet — materialize → resolve
4.2 Cell edit
4.3 ✓ 커밋 → records → 급여대장
The lineage triple makes every won in the ledger traceable back to the exact sheet cell that produced it, and makes commits idempotent (re-commit updates, never duplicates).
5. Frontend Composition
The workbook was refactored (2026-06) from one 18,000-line file into 30 concern slices merged by compose() (descriptor-merge that preserves getters):
- Engine vs Workbook: the engine (
src/sheet/) is a framework-free grid state machine (render, virtualization, selection, history); the workbook slices are the Alpine glue binding it to the SDK and domain logic. - Adapters map a
SourceTableto default columns and per-column renderers, so an auto-populated overtime sheet knows how to label 연장/야간연장 without per-sheet config.
6. Identity & Reuse Rules
- slug — a sheet's stable cross-period identity (
payroll_ledger,overtime_all,attendance_summary_all, …). Unique per(org, year, month)among live rows (partial indexUQ_sheet_views_org_period_slug). Aggregates and cross-sheet refs bind to slugs, not UUIDs, so they survive month-to-month. - Templates: system templates (
isSystem) apply as SHARED sheets; user templates as PRIVATE. Template sets (setSlug+isSetUmbrella) apply several sheets as one picker entry (e.g. 퇴직 정산 3종). - Carry-forward copies configuration (layout, slug, autoPopulate, computeConfig), never data — rows re-materialize from period-scoped source tables, and records are per-
(year, month). - Trash: soft delete with
deletedByownership; restore is deleter-only and drops the slug if a live sheet re-claimed the slot (data recovery never hard-fails).
File Map
| Layer | Path | What |
|---|---|---|
| Entity | packages/database/src/entities/sheet-view.entity.ts | SheetView (period, slug, layout, autoPopulate, computeConfig, commit stamps) |
| Entity | packages/database/src/entities/sheet-row.entity.ts | SheetRow (manual/auto, source discriminator, protection) |
| Entity | packages/database/src/entities/sheet-cell.entity.ts | SheetCell (kind discriminator, UQ(rowId, columnKey), style) |
| Entity | packages/database/src/entities/sheet-template.entity.ts | SheetTemplate (system/user, defaultSlug, sets) |
| Lineage | packages/database/src/entities/allowance-record.entity.ts | sourceSheet* NOT NULL lineage + target* aggregate keys |
| Router | packages/api/src/routers/sheet-view.router.ts | CRUD, queryRows, commit, carry-forward, trash, import/export |
| Router | packages/api/src/routers/sheet-row.router.ts / sheet-cell.router.ts | manual rows / cell upsert + guards |
| Service | packages/api/src/routers/sheet-rows/auto-populate.ts, materializer.ts | source → auto-row sync |
| Service | packages/api/src/routers/sheet-rows/resolver.ts | ResolvedRow projection + aggregate pre-pass |
| Service | packages/api/src/routers/sheet-rows/compute.ts | runCompute (code modules) / commitSheet (records + lineage) |
| Seeds | packages/database/src/seeds/seed-sheet-templates.ts | SYSTEM_TEMPLATES (idempotent by name) |
| SDK | packages/sdk/src/services/SheetViewService.ts (+ Row/Cell/Template) | typed client surface |
| Frontend | apps/hr-system/src/sheets/workbook/ | 30 slices + compose.ts + types.ts |
| Frontend | apps/hr-system/src/sheet/ | grid engine (render, virtualizer, selection, history, escape-html) |
| Frontend | apps/hr-system/src/views/tabs/sheets/ | grid.html, sidebar, modals, context menus |