다크모드
HR·전자결재 Company/Office 계약 — BE 핸드오프
소유권
- 모든 레코드는
company_id필수. - 직원·조직·계약·급여 run·인사문서·퇴직정산·전자결재 문서는 Company 원장.
- 출퇴근·업무일지·휴가 발생은
office_id필수. 사용자의office_memberships안에서만 처리. - 급여/문서/퇴직 조회 시 Office 필터를 제공할 수 있으나 원장의 소유권을 Office로 바꾸지 않는다.
HR 불변식
- 활성 직원과 유효한 근로계약이 있어야 근태/급여 대상이 된다.
- 급여는 payroll-owned 입력이 준비되고 unresolved input이 0건일 때 확정한다. 근태 마감본은 선택 provider다.
- 급여
paid는 종결 상태다. 정정은 원 run 변조가 아니라 별도 조정 run/다음 달 소급으로 남긴다. - 명세서·증명서·퇴직정산서는 발급 시점 스냅샷이며 재계산으로 바뀌지 않는다.
- 민감정보·첨부는 필드별 암호화, 최소권한, 다운로드 감사로그, 보존·파기 정책을 적용한다.
전자결재 불변식
- 현재 단계 결재자 또는 기간·범위가 유효한 수임자만 처리한다.
- 완료/반려는 종결 상태이며 이후 단계 전이를 거부한다.
- 처리 이력은 append-only, 문서 본문/결재선은 상신 시점 스냅샷이다.
company_id + request_id멱등키와 row version으로 중복 승인·동시 처리를 막는다.- 알림 실패는 결재 상태를 되돌리지 않고 별도 outbox에서 재시도한다.
API 오류 계약
권장 코드: COMPANY_SCOPE_MISMATCH, OFFICE_ACCESS_DENIED, ATTENDANCE_NOT_CLOSED, PAYROLL_ALREADY_PAID, APPROVAL_NOT_CURRENT_ACTOR, APPROVAL_TERMINAL, VERSION_CONFLICT. FE가 빈 상태·권한 차단·재시도 가능 오류를 구분할 수 있도록 HTTP 상태와 함께 안정적으로 반환한다.
근태 원장과 월 마감 계약
마이그레이션 20260717061000_attendance_period_close_gate.sql이 다음 경계를 구현한다.
attendance_events: Company·Office·직원·근무일 기준 append-only 출퇴근 원장. 로그인 사용자는 활성workforce_identity와workforce_employee, 해당 일자에 유효한 Office assignment가 모두 있어야record_attendance_event를 실행할 수 있다.attendance_periods:(company_id, office_id, period_start)당 한 행.open | closed상태와 단조 증가revision을 가진다.attendance_period_transitions: close/reopen causal history.(company_id, request_key)가 재시도를 멱등 처리하고, update/delete trigger가 과거 변조를 거부한다.private.enforce_attendance_period_open:attendance_eventsINSERT 직전 현재 월이 닫혔는지 재확인한다. 브라우저 RPC뿐 아니라 이후 service 경로의 직접 INSERT도 닫힌 월에는 실패한다.
close_attendance_period는 Office 행 잠금과 expected_revision을 사용한다. 마지막 이벤트가 clock_in인 직원·일자가 하나라도 있거나 live 미해결 예외가 있으면 전체 트랜잭션을 거부한다. 성공 시 이벤트 수와 전체·해결·미해결 예외 수를 close_summary에 고정하고 attendance.period_closed 감사 이벤트를 같은 트랜잭션에서 남긴다.
reopen_attendance_period는 닫힌 기간에만 허용하며 5자 이상의 사유, 최신 revision, HR Company 관리자 또는 Office 관리자/운영자 권한을 요구한다. 성공 시 attendance.period_reopened 감사 이벤트와 append-only transition에 사유를 남긴다.
RLS는 Company 관리자, 해당 Office membership, 유효한 HR role grant만 기간을 읽게 한다. 일반 직원은 자신의 출퇴근만 읽고, 관리자는 Office 이벤트를 읽는다. authenticated의 테이블 직접 INSERT/UPDATE/DELETE는 허용하지 않는다.
교대·근무 일정 계약
마이그레이션 20260717075727_workforce_shift_schedule.sql이 출퇴근 이전의 예정 근무 기준을 구현한다.
workforce_shift_schedules:(company_id, office_id, period_start)당 월 근무표 한 행.draft | published, 단조 증가revision, 확정 actor/시각을 보존한다.workforce_scheduled_shifts: 직원·유효 Office assignment·근무일·시작/종료·무급 휴게분·역할을 보존하는 append-only 근무 구간이다. 익일 종료 교대는 허용하고 한 구간은 최대 36시간이다.workforce_shift_schedule_transitions:draft → published원인 이력. update/delete는 trigger가 거부한다.private.workforce_shift_schedule_commands: create/publish 입력과 응답을 Company 요청키로 보존해 재시도를 멱등 처리한다.
create_workforce_scheduled_shift는 Office 관리권한, 월/Revision, 직원의 해당 일자 Office assignment, draft 상태, 근태 open 상태를 확인한다. 직원 UUID를 키로 transaction advisory lock을 잡고 기존 구간과 existing.starts_at < new.ends_at AND existing.ends_at > new.starts_at을 검사해 동시 겹침을 차단한다. INSERT trigger도 같은 assignment·draft·마감·겹침 검사를 실행하므로 privileged 직접 INSERT가 도메인 불변식을 우회하지 못한다. 성공 시 schedule revision을 1 증가시키고 workforce.shift_scheduled 감사를 남긴다.
publish_workforce_shift_schedule은 최신 Revision과 한 건 이상의 근무, 근태 open을 확인한 뒤 근무표를 확정한다. 확정은 이번 slice에서 불가역이며 workforce.shift_schedule_published 감사와 append-only transition을 기록한다.
RLS는 HR/Office 관리자가 Office 근무표를 읽고, 일반 직원은 활성 identity로 연결된 자기 근무만 읽도록 한다. authenticated의 직접 write는 전부 revoke하고 두 RPC만 명시적으로 grant한다. 새 public 테이블에는 RLS와 explicit SELECT grant를 함께 적용했다.
SQL rollback smoke는 supabase/tests/workforce_shift_schedule_smoke.sql이다. 멱등 생성·익일 교대·겹침 거부·확정 재시도·확정 후 쓰기 거부·감사·append-only를 검증한다.
예정 근무 연동 출퇴근·근태 예외 계약
마이그레이션 20260717110723_workforce_attendance_exceptions.sql이 예정과 실제의 DB 경계를 잇는다.
attendance_events.scheduled_shift_id는 Company·Office·직원까지 포함한 composite FK로 확정 예정 근무를 참조한다. shift별 출근/퇴근은 각각 한 건만 허용한다.record_scheduled_attendance_event는 invoker public wrapper와 최소권한 private definer 구현으로 나뉜다. 로그인 직원·published shift·시간창·마감 상태·요청키를 재검증한다. 야간 익일 퇴근은 열린 출근의scheduled_shift_id와work_date를 승계하므로 달력 날짜가 바뀌어도 원 근무일에 묶인다.attendance_exception_overview WITH (security_invoker=true)는 published shift와 append-only punch를 live 조합해absent | missing_checkout | late | early_leave | unscheduled을 투영한다. 미래 근무는 결근/퇴근누락으로 만들지 않는다.attendance_exception_resolutions는 append-only HR 판단 원장이다.exception_key + evidence_fingerprint에 판단을 묶어 출퇴근 증거가 바뀌면 기존 해결을 자동 무효화한다.resolve_attendance_exception은 Office HR 관리권한, open 기간, 현재 fingerprint, 5자 이상 사유, idempotency와 advisory lock을 검증하고attendance.exception_resolved감사를 같은 트랜잭션에 남긴다.- attendance close trigger는 live 미해결 건수를 재검사하고 1건 이상이면
attendance_period_unresolved_exceptions로 거부한다. 성공한close_summary에는exceptionCount,resolvedExceptionCount,unresolvedExceptionCount가 저장된다. - 마감·재오픈은 payroll table을 조회하지 않고 각각
time.payroll-input.v1ready/invalidation event를 남긴다. 선택 adapter가 payroll-owned input으로 전달하며, consumer 장애는 근태 로컬 전이를 되돌리지 않는다.
resolution 테이블은 self/관리자 SELECT RLS, authenticated direct write revoke를 적용한다. 두 public RPC는 authenticated/service role만 실행하고 private definer는 정확한 signature grant만 유지한다. SQL rollback smoke supabase/tests/workforce_attendance_exceptions_smoke.sql은 익일 퇴근 pairing, 5종 projection, fingerprint stale 방어, 멱등 해결, 마감 gate/summary, 직접 write 차단과 감사를 검증한다. Payroll 연결은 time_payroll_input_provider_smoke.sql이 별도로 검증한다.
휴가 로컬 결정·선택 근태 snapshot 계약
forward migration 20260717160116_workforce_local_decision_ledgers.sql은 휴가 핵심 흐름을 Human Capital 단독으로 전환한다.
workforce_leave_requests.approval_document_id는 nullable opaque legacy reference이며 provider FK가 없다.workforce_leave_decisions는review_requested → approved | rejected두 event를 보존하는 HR append-only ledger다.create_workforce_leave_request는 self 또는 HR 관리자, Officehuman-capitalentitlement, 활성 assignment, 승인 근로계약, 기존 진행/승인 휴가 비중복을 검증한다. 결재선 입력은 없다.decide_workforce_leave_request는 HR 관리자, expected revision, pending 상태를 검증한다. 승인 시 중복 휴가와 연차 잔여를 다시 검사하고 balance consume, terminal decision, 휴가 상태, 감사를 같은 HR 트랜잭션에 저장한다.- Time capability가 available일 때만
workforce_leave_time_check_snapshot이 published shift 충돌 건수를 캡처하고 승인 시 재검사한다. unavailable이면{ availability: 'unavailable' }snapshot을 보존하고 휴가 핵심 흐름은 계속한다. - 기존 Approval private outbox trigger/consumer와 consumption 원장은 제거했다. Approval 장애가 HR 상태를 롤백하는 경로가 없다.
workforce_leave_request_overview는security_invoker=true이고time_availability, nullable conflict count,integration_status를 제공한다.private.workforce_local_decision_commands와 employee advisory lock이 요청키 재시도·동시 중복 신청/승인을 막는다.
SQL rollback smoke supabase/tests/workforce_local_decision_ledgers_smoke.sql은 Approval entitlement 없이 계약·휴가 review/approve, Time unavailable no-op, 잔여 차감, overdraft rollback, append-only/direct-write 거부를 검증한다.
휴가 발생·잔여 원장 계약
마이그레이션 20260717104943_workforce_leave_balance_ledger.sql은 법정 발생 정책을 추정하지 않고 Company·직원·귀속연도의 append-only 연차 원장을 추가한다.
workforce_leave_balance_entries:grant | consume | adjustment의 signeddelta_days원장이다. 연차와 반차는leave_bank='annual'을 공유한다.- 잔액은
Σ delta_days, Revision은 동일 Company·직원·연도·bank의 단조 증가 entry revision이다. 과거 update/delete는 trigger가 거부한다. grant_workforce_leave_balance/adjust_workforce_leave_balance: HR·Office 관리권한, 현재 assignment, 대상 연도와 겹치는 승인 근로계약, expected revision, 0.5일 단위, 멱등 요청키를 검증한다. 조정 후 음수 잔액은 거부한다.- 휴가 local decision RPC는 기존 employee lock 안에서 annual/half-day 잔액을 다시 읽고
consumeentry를 추가한다. 부족하면 HR terminal event·휴가 상태·잔액 소진이 함께 rollback되고 요청은 검토 대기로 남는다. - 연도 경계 annual/half-day 요청은 귀속 원장이 모호해지지 않도록 분할 신청을 요구한다. rejected와 비잔액 휴가 종류는 소비하지 않는다.
workforce_leave_balance_overview WITH (security_invoker=true)는 현재 Office assignment별 Company 직원 합계만 제공한다. self, Company HR, 현재 배치 Office 관리자만 읽고 authenticated direct write는 없다.
공통 감사 키는 workforce.leave_balance_granted|adjusted|consumed이다. 신규 local decision smoke와 기존 잔액 원장 테스트가 부여 멱등, half → half_day 호환, 승인 소진, 잔액 부족 HR decision rollback, 조정, projection, 직접 write 차단과 감사를 검증한다.
자동 법정 발생, 입사일/회계연도 정책, carry-over/expiry는 effective-dated 정책과 휴일·근무일 계약이 필요하므로 후속이다. 기본 15일을 서버에 하드코딩하지 않는다.
현재 미구현 범위는 법정 자동 휴가 발생·이월·소멸, 확정 근무 수정/취소, 반차 시간대별 충돌, 위치·기기 증명, 수정요청 승인, 예외 자동 시정과 근무표 기반 급여 시간 계산이다. 외부 API나 타사 서비스 계약은 추가하지 않았다.