Skip to content

HR 급여 (BE 참고)

2026-07-18 as-built. 계산 정본은 payrollMock.js, 상태·저장 seam은 usePayrollRun.jspayrollRepo.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.v1 read/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-scoped request_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_id payroll-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

처리 순서:

  1. auth와 Office-scoped human_capital_admin 권한을 확인한다.
  2. request payload 멱등성을 확인한다.
  3. payroll input이 같은 Company·Office·월이고 무효화되지 않았으며 ready=true, unresolvedCount=0인지 확인한다.
  4. run을 생성하거나 FOR UPDATE로 잠근 뒤 draft와 revision을 확인한다.
  5. snapshot 직원 중복, 금액 비음수, net = gross - deductionTotal을 확인한다.
  6. 각 직원이 Company 소속이며 급여기간과 겹치는 유효한 Office 배치와 approved 근로계약을 갖는지 확인한다. 월중 퇴직자는 termination_date가 급여기간과 겹치면 포함할 수 있다.
  7. totals는 client 값을 신뢰하지 않고 lines에서 서버가 재집계한다.
  8. 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 + 은행코드,계좌번호,금액,예금주,내용; 금액은 snapshot net.
  • Σ(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 객체 무참조를 검증한다.