시트 아키텍처
시트(워크북) 시스템의 구현 전모 — hr-system의 Alpine.js 그리드부터 최종 급여 레코드까지. 시트는 데이터 입력과 급여 조립의 중심 화면입니다: 급여대장에 도달하는 모든 수당/공제는 시트 커밋(✓ 커밋)에서 태어납니다.
소스 코드 위치는 파일 맵 참고. 급여 레코드 모델 자체는 Payroll Schema Refactor 문서와
CLAUDE.md의 Payroll 규칙 참고. (영문판: Sheet Architecture)
1. 레이어 개요
핵심 아이디어: 시트는 프로젝션(projection) 레이어입니다. 소스 테이블은 관측값(시간, 세션, 상태)만 보관하고, sheet_cells에는 사용자 수정값과 계산 캐시만 저장되며, 금액 결과는 오직 allowance_records / deduction_records에만 존재합니다 — 이 레코드들은 시트 커밋(✓ 커밋)으로만 생성되고 항상 시트 계보(lineage)를 가집니다.
2. 데이터 모델
컬럼 종류 (columnLayout[].kind)
| kind | 의미 | 값 저장 위치 | 편집 |
|---|---|---|---|
identity | row.employeeId 조인에서 오는 사번/이름/팀 | — (파생) | 불가 |
literal | 사용자가 직접 입력한 값 | SheetCell.literalValue | 가능 |
ref | 소스 레코드 필드 포인터 | SheetCell.ref*RecordId → 레코드 | 제한적 |
formula | =SUM(...) 형식 수식 | formulaExpression + computedValue 캐시 | 수식만 |
computed | 코드 모듈 출력 (예: 보험료 계산) | computedValue + inputSnapshot | 불가 (수동 오버라이드는 스킵) |
aggregate | 커밋된 레코드의 SUM — 급여대장 컬럼 | 저장 안 함, 조회 시 파생 | 불가 |
multiselect / employeeselect / treatmentcount | 특수 에디터 | literalValue | 가능 |
행 값 결정 순서 (resolver.ts)
행 × 컬럼마다 resolver가 아래 우선순위로 첫 번째 값을 선택합니다:
즉 수동 수정은 소스 값을 변형하지 않고 *가리기(shadow)*만 하며, 셀을 지우면 소스 값 폴스루가 복원됩니다.
3. 한 달 동안의 시트 라이프사이클
4. 주요 흐름 (시퀀스)
4.1 시트 열기 — materialize → resolve
4.2 셀 편집
4.3 ✓ 커밋 → 레코드 → 급여대장
계보 3요소 덕분에 급여대장의 모든 금액은 그 값을 만든 정확한 시트 셀까지 역추적할 수 있고, 커밋은 멱등입니다(재커밋은 갱신만 하고 중복 생성하지 않음).
5. 프론트엔드 구성
워크북은 2026-06 리팩터로 18,000줄 단일 파일에서 30개의 관심사 슬라이스로 분리되었고, compose()(getter 를 보존하는 descriptor 병합)로 합쳐집니다:
- 엔진 vs 워크북: 엔진(
src/sheet/)은 프레임워크 무관 그리드 상태 머신(렌더, 가상화, 선택, 히스토리)이고, 워크북 슬라이스는 이를 SDK·도메인 로직과 잇는 Alpine 글루입니다. - 어댑터는
SourceTable을 기본 컬럼 + 컬럼별 렌더러에 매핑합니다. 그래서 자동 채움된 연장근무 시트가 시트별 설정 없이도 연장/야간연장 라벨을 알 수 있습니다.
6. 정체성 · 재사용 규칙
- slug — 시트의 기간 간 안정 정체성(
payroll_ledger,overtime_all,attendance_summary_all등). live 행 기준(org, year, month)당 유일(부분 인덱스UQ_sheet_views_org_period_slug). aggregate 와 시트 간 참조는 UUID 가 아니라 slug 에 바인딩되므로 월이 바뀌어도 유지됩니다. - 템플릿: 시스템 템플릿(
isSystem)은 SHARED 로, 사용자 템플릿은 PRIVATE 으로 적용됩니다. 템플릿 세트(setSlug+isSetUmbrella)는 여러 시트를 픽커의 항목 하나로 함께 적용합니다(예: 퇴직 정산 3종). - 이전월에서 가져오기는 설정(layout, slug, autoPopulate, computeConfig)만 복제하고 데이터는 복제하지 않습니다 — 행은 기간 스코프 소스 테이블에서 다시 materialize 되고, 레코드는
(year, month)별입니다. - 휴지통:
deletedBy소유권이 있는 soft delete. 복원은 삭제자 본인만 가능하며, live 시트가 slug 슬롯을 재점유했으면 slug 를 비워 복원합니다(데이터 복구는 절대 하드 실패하지 않음).
파일 맵
| 레이어 | 경로 | 내용 |
|---|---|---|
| 엔티티 | packages/database/src/entities/sheet-view.entity.ts | SheetView (기간, slug, layout, autoPopulate, computeConfig, 커밋 스탬프) |
| 엔티티 | packages/database/src/entities/sheet-row.entity.ts | SheetRow (manual/auto, 소스 판별자, 행 보호) |
| 엔티티 | packages/database/src/entities/sheet-cell.entity.ts | SheetCell (kind 판별자, UQ(rowId, columnKey), style) |
| 엔티티 | packages/database/src/entities/sheet-template.entity.ts | SheetTemplate (system/user, defaultSlug, 세트) |
| 계보 | packages/database/src/entities/allowance-record.entity.ts | sourceSheet* NOT NULL 계보 + target* aggregate 키 |
| 라우터 | packages/api/src/routers/sheet-view.router.ts | CRUD, queryRows, 커밋, 이전월 가져오기, 휴지통, import/export |
| 라우터 | packages/api/src/routers/sheet-row.router.ts / sheet-cell.router.ts | 수동 행 / 셀 upsert + 가드 |
| 서비스 | packages/api/src/routers/sheet-rows/auto-populate.ts, materializer.ts | 소스 → auto 행 동기화 |
| 서비스 | packages/api/src/routers/sheet-rows/resolver.ts | ResolvedRow 프로젝션 + aggregate 사전 집계 |
| 서비스 | packages/api/src/routers/sheet-rows/compute.ts | runCompute (코드 모듈) / commitSheet (레코드 + 계보) |
| 시드 | packages/database/src/seeds/seed-sheet-templates.ts | SYSTEM_TEMPLATES (이름 기준 멱등) |
| SDK | packages/sdk/src/services/SheetViewService.ts (+ Row/Cell/Template) | 타입 안전 클라이언트 표면 |
| 프론트엔드 | apps/hr-system/src/sheets/workbook/ | 슬라이스 30개 + compose.ts + types.ts |
| 프론트엔드 | apps/hr-system/src/sheet/ | 그리드 엔진 (render, virtualizer, selection, history, escape-html) |
| 프론트엔드 | apps/hr-system/src/views/tabs/sheets/ | grid.html, 사이드바, 모달, 컨텍스트 메뉴 |