다크모드
HR 급여 (BE 참고)
2026-07-18 as-built. 계산 정본은
payrollMock.js, 상태·저장 seam은usePayrollRun.js와payrollRepo.js, 입력 경계 정본은20260717160105_payroll_local_input_snapshots.sql이다. 근태 provider는 선택 계약이며 운영 Supabase 배포는 별도 단계다.
상태와 선행 게이트
text
manual/csv/optional-provider ──capture──▶ payroll-input.ready
│
▼
payroll.draft ──confirm-v2──▶ payroll.confirmed ──mark_paid──▶ payroll.paid
▲ │
└────────── revert ────────────┘confirm-v2: 같은 Company·Office·월의 유효한 payroll-owned input snapshot이 필수다.- 근태 상품은
time.payroll-input.v1read/event만 제공한다. payroll은 time table·FK·RPC를 참조하지 않는다. - 근태 entitlement가 없어도
manual또는csv입력으로 급여 핵심 흐름을 완료한다. revert: confirmed에서만 허용하며 사유 5자 이상이 필요하다. current snapshot 연결만 해제하고 과거 snapshot은 삭제하지 않는다.mark_paid: confirmed에서만 허용하며 paid는 불가역이다.- optional provider의 원본 변경은 input invalidation을 추가한다. 이미 confirmed/paid인 payroll snapshot은 변조하지 않는다.
- 모든 전이는
expected_revision낙관적 잠금과 Company-scopedrequest_key멱등성을 사용한다.
데이터 모델
payroll_runs
- Company·Office·월당 1행:
unique(company_id, office_id, period_start). - 내부 UUID와 Company-scoped 불변
management_code를 분리한다. - 현재 상태, revision, current snapshot FK, confirmed/paid actor와 시각만 소유한다.
- authenticated는 SELECT만 가능하며 direct INSERT/UPDATE/DELETE는 금지한다.
payroll_input_snapshots / payroll_input_invalidations
- payroll이 확정 선행 입력을 소유하며
manual | csv | time_adapter | legacy_bridge출처를 opaque metadata로 보존한다. - contract version, payload, summary, canonical hash, actor를 append-only로 보존한다.
- invalidation도 별도 append-only 원장이다. 무효화된 입력은 새 confirm에 사용할 수 없다.
- 기존 attendance FK는 제거했고 과거 confirmation은
legacy_bridge입력으로 backfill했다.
payroll_run_snapshots
- 매 confirm마다 1행을 추가하는 append-only 원장이다.
input_snapshot_idpayroll-local FK와 입력 hash·출처를 함께 보존한다.calculation_version, 직원별 지급·공제·계좌 snapshot, 서버가 재집계한 totals를 보존한다.- update/delete trigger는
payroll_history_append_only로 거부한다.
payroll_run_transitions
- confirm/revert/mark_paid의 from/to status, expected/result revision, actor, reason, request key를 append-only로 보존한다.
private.payroll_run_commands가 동일 요청 재시도 응답과 payload 충돌을 판정한다.
확정 RPC
sql
confirm_payroll_run_v2(
p_company_id uuid,
p_office_id uuid,
p_period_start date,
p_input_snapshot_id uuid,
p_expected_revision integer,
p_calculation_version text,
p_snapshot jsonb,
p_request_key text
) returns jsonb처리 순서:
- auth와 Office-scoped
human_capital_admin권한을 확인한다. - request payload 멱등성을 확인한다.
- payroll input이 같은 Company·Office·월이고 무효화되지 않았으며
ready=true,unresolvedCount=0인지 확인한다. - run을 생성하거나
FOR UPDATE로 잠근 뒤 draft와 revision을 확인한다. - snapshot 직원 중복, 금액 비음수,
net = gross - deductionTotal을 확인한다. - 각 직원이 Company 소속이며 급여기간과 겹치는 유효한 Office 배치와 approved 근로계약을 갖는지 확인한다. 월중 퇴직자는
termination_date가 급여기간과 겹치면 포함할 수 있다. - totals는 client 값을 신뢰하지 않고 lines에서 서버가 재집계한다.
- immutable snapshot, current run, transition, operational audit, command 응답을 한 트랜잭션에 기록한다.
브라우저가 계산한 snapshot을 전달하는 것은 현재 Prototype 계산 엔진을 유지하기 위한 전환기 계약이다. 상용화 단계에서는 임금·공제 기준정보를 서버 원장으로 옮기고 서버 계산 결과만 확정해야 한다.
조회 권한과 민감정보
human_capital_admin: 부여된 Company/Office 급여 조회·전이.human_capital_viewer: 부여된 Company/Office 급여 조회만.- Company 운영 관리자: Company 범위 조회·전이.
- 일반 Office membership만으로는 급여를 읽을 수 없다.
- 급여 snapshot에는 계좌와 임금이 포함되므로 service role을 제외한 direct write를 모두 막고 RLS를 적용한다.
- 후속 필수: 계좌 column/envelope 암호화, 키 회전, 보존/파기, 조회 감사, export 통제.
프런트 계약
payrollRepo.js 서버 adapter:
loadAll({ companyId, officeId })loadInputReadiness({ companyId, officeId, periodStart })captureInput({ companyId, officeId, periodStart, contractVersion, inputData, inputSummary, requestKey })confirmRun({ companyId, officeId, periodStart, inputSnapshotId, expectedRevision, calculationVersion, snapshot, requestKey })revertRun({ companyId, runId, expectedRevision, reason, requestKey })markPaid({ companyId, runId, expectedRevision, requestKey })
테스트 모드 또는 Supabase 미설정 환경은 localStorage adapter를 사용한다. 운영 DB migration이 배포된 환경에서만 서버 RPC가 권위 원장이다.
이체·원천세 계약
- 이체 CSV: UTF-8 BOM +
은행코드,계좌번호,금액,예금주,내용; 금액은 snapshotnet. Σ(lines.net) = snapshot.totals.net = CSV amount 합계.- 원천세 요약: A01, headcount, taxablePaid, incomeTax, localIncomeTax, 익월 10일.
- 실제 은행 API, 전자신고 파일, 회계 분개는 이번 slice 밖이다.
검증
- SQL rollback smoke:
supabase/tests/payroll_local_input_snapshots_smoke.sql. - optional provider smoke:
supabase/tests/time_payroll_input_provider_smoke.sql. - migration contract:
src/composables/__tests__/payrollInputSnapshotMigration.spec.js. - repository:
src/composables/payrollRepo.test.js. - 상태·근태 게이트·스냅샷·CSV·원천세:
src/composables/usePayrollRun.test.js.
Smoke는 payroll-only entitlement의 manual capture·confirm-v2 재시도, legacy backfill, append-only, direct write 차단과 RLS를 검증한다. Provider smoke는 time-only entitlement의 close ready event, versioned read contract, reopen invalidation event와 payroll 객체 무참조를 검증한다.