Skip to content

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) ​

kindMeaningValue lives inEditable
identity사번/이름/팀 from row.employeeId join— (derived)no
literalUser-typed valueSheetCell.literalValueyes
refPointer to a source record fieldSheetCell.ref*RecordId → recordlimited
formula=SUM(...)-style expressionformulaExpression + cached computedValueexpression only
computedCode-module output (e.g. insurance calc)computedValue + inputSnapshotno (manual override skips)
aggregateSUM of committed records — 급여대장 columnsnever stored, derived at readno
multiselect / employeeselect / treatmentcountSpecial editorsliteralValueyes

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 SourceTable to 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 index UQ_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 deletedBy ownership; restore is deleter-only and drops the slug if a live sheet re-claimed the slot (data recovery never hard-fails).

File Map ​

LayerPathWhat
Entitypackages/database/src/entities/sheet-view.entity.tsSheetView (period, slug, layout, autoPopulate, computeConfig, commit stamps)
Entitypackages/database/src/entities/sheet-row.entity.tsSheetRow (manual/auto, source discriminator, protection)
Entitypackages/database/src/entities/sheet-cell.entity.tsSheetCell (kind discriminator, UQ(rowId, columnKey), style)
Entitypackages/database/src/entities/sheet-template.entity.tsSheetTemplate (system/user, defaultSlug, sets)
Lineagepackages/database/src/entities/allowance-record.entity.tssourceSheet* NOT NULL lineage + target* aggregate keys
Routerpackages/api/src/routers/sheet-view.router.tsCRUD, queryRows, commit, carry-forward, trash, import/export
Routerpackages/api/src/routers/sheet-row.router.ts / sheet-cell.router.tsmanual rows / cell upsert + guards
Servicepackages/api/src/routers/sheet-rows/auto-populate.ts, materializer.tssource → auto-row sync
Servicepackages/api/src/routers/sheet-rows/resolver.tsResolvedRow projection + aggregate pre-pass
Servicepackages/api/src/routers/sheet-rows/compute.tsrunCompute (code modules) / commitSheet (records + lineage)
Seedspackages/database/src/seeds/seed-sheet-templates.tsSYSTEM_TEMPLATES (idempotent by name)
SDKpackages/sdk/src/services/SheetViewService.ts (+ Row/Cell/Template)typed client surface
Frontendapps/hr-system/src/sheets/workbook/30 slices + compose.ts + types.ts
Frontendapps/hr-system/src/sheet/grid engine (render, virtualizer, selection, history, escape-html)
Frontendapps/hr-system/src/views/tabs/sheets/grid.html, sidebar, modals, context menus