Skip to content

Time 근태 정정 승인 (BE 참고)

2026-07-18 as-built. 정본 migration은 20260718143000_time_attendance_corrections.sql, rollback smoke는 time_attendance_corrections_smoke.sql이다. 이 slice의 product owner는 time이며 approval·payroll 소유 객체를 참조하지 않는다.

상태 흐름

text
request ledger(pending)
  ├─ rejected decision ─────────────────────────▶ rejected
  └─ approved decision → correction event ─────▶ applied

요청, 판단, 적용은 모두 append-only다. 상태 컬럼을 갱신하지 않고 time_attendance_correction_overview가 decision/event 존재 여부로 pending | rejected | applied를 투영한다. 승인 판단과 적용 이벤트는 한 DB 트랜잭션에서 함께 생성되므로 approved인데 정정 이벤트가 없는 중간 상태를 외부에 노출하지 않는다.

데이터 모델

time_attendance_correction_requests

  • Company·Office·Time worker UUID·근무일 scope. 직원명은 요청 당시 표시 snapshot으로 보존한다.
  • correction_kind: add | replace | void
  • replace/void는 immutable attendance_events.id를 원본으로 참조
  • 제안 event type/time, 요청 사유, 원본 evidence fingerprint, requester, Company-scoped request key 보존
  • UPDATE/DELETEtime_attendance_correction_history_append_only로 거부

time_attendance_correction_decisions

  • 요청당 정확히 한 행: approved | rejected
  • 판단 사유, actor, request key 보존
  • 전자결재 문서 FK 없음. 후속 adapter가 외부 연결을 별도 원장으로 보존해야 한다.

time_attendance_correction_events

  • 승인 요청당 정확히 한 행
  • add/replace는 corrected type/time, void는 원본 참조만 보존
  • 원본 attendance event는 수정·삭제하지 않는다.

조회 projection

  • time_attendance_correction_overview: 요청+판단+적용 상태. security_invoker=true.
  • time_attendance_effective_events: 아직 supersede되지 않은 원본과 승인된 add/replace를 합친 유효 기록. void는 원본을 유효 목록에서만 제외한다.

두 view는 기반 테이블 RLS를 그대로 적용한다. 관리자는 Office 범위를, 직원은 본인 또는 자신이 요청한 행만 읽는다. authenticated direct write는 모두 revoke하고 mutation은 RPC로만 수행한다.

RPC 계약

정정 요청

sql
request_time_attendance_correction(
  p_company_id uuid,
  p_office_id uuid,
  p_employee_id uuid,
  p_employee_display_name text,
  p_work_date date,
  p_correction_kind text,
  p_original_event_id uuid,
  p_proposed_event_type text,
  p_proposed_occurred_at timestamptz,
  p_reason text,
  p_request_key text
) returns jsonb

처리 순서:

  1. auth와 Office time entitlement 확인
  2. Time 출퇴근 event의 actor binding으로 본인 여부를 확인하거나 근태 관리자 권한 확인
  3. 원본 event가 있으면 Company·Office·worker·근무일 scope 확인
  4. correction shape와 5자 이상 사유 검증
  5. 원본 scope를 검증하고 evidence fingerprint 고정
  6. Company request key 멱등성 확인
  7. immutable request 추가. 이 요청 원장 자체가 직원 actor·사유·증거를 보존하는 감사증거다.

본인 actor binding은 동일 Time Office에서 과거에 본인이 기록한 attendance event로 증명한다. event가 전혀 없는 worker의 최초 add 요청은 관리자가 대리 생성한다. 이는 Human Capital identity를 직접 참조하지 않으면서 Time을 독립 상품으로 유지하기 위한 현재 계약이다.

승인·반려

sql
decide_time_attendance_correction(
  p_company_id uuid,
  p_office_id uuid,
  p_request_id uuid,
  p_decision text,
  p_reason text,
  p_expected_period_revision integer,
  p_allow_reopen boolean,
  p_reopen_reason text,
  p_request_key text
) returns jsonb

관리자만 호출할 수 있다. 승인 전 원본 fingerprint를 재검사한다. 반려는 decision만 추가한다.

마감기간 원자적 gate

열린 기간은 expected revision을 확인하고 decision→correction event를 추가한다. 닫힌 기간은 p_allow_reopen=true와 5자 이상 reopen reason이 필수이며 다음을 한 트랜잭션에서 호출한다.

text
reopen_attendance_period(expected revision N, key:reopen)
  → approved decision
  → correction event
  → close_attendance_period(expected revision N+1, key:reclose)

재마감 전 effective event가 출근→퇴근으로 교대하는지, 마지막 출근에 대응하는 퇴근이 있는지 다시 검증한다. 재마감 성공 revision은 N+2다. 재마감 gate(열린 출퇴근·미해결 예외 등)가 실패하면 reopen과 correction 전체가 rollback된다. request와 decision RPC는 payload-aware Company request key를 사용한다.

동일 원본 동시 승인 불변식

  • time_attendance_correction_events_original_key partial unique index가 original_event_id is not null인 correction event를 원본당 최대 1건으로 제한한다.
  • 승인 RPC는 원본 attendance_events 행을 FOR UPDATE로 잠근 뒤 기존 correction을 재검사한다. 서로 다른 request 행을 잠근 동시 승인도 원본 행에서 직렬화된다.
  • unique 경쟁의 loser는 transaction 전체가 rollback되고 time_attendance_correction_original_already_corrected(23505) 계약으로 반환된다. decision만 남거나 마감 revision만 증가하는 부분 성공은 허용하지 않는다.

선택 급여 입력과 스냅샷 불변

이 migration은 Human Capital과 payroll table, FK, RPC를 읽거나 쓰지 않는다. worker UUID는 Time event의 opaque subject이며 표시명은 요청 snapshot이다. 기존 Time-owned attendance-period status trigger가 다음 provider event만 낸다.

  • reopen: time.payroll-input.v1 invalidated event
  • reclose: 새 source revision의 ready event

consumer는 새 입력을 캡처할 수 있지만 이미 만들어진 payroll input/run snapshot은 append-only 규칙에 따라 그대로 남는다. Time correction이 확정 급여를 직접 되돌리거나 변조하지 않는 것이 핵심 불변식이다. smoke는 correction RPC의 information_schema.routine_table_usagepayroll% 객체가 없고 reopen/reclose provider event가 각각 추가되는지 검증한다.

통합 라우트 계약

  • 콘솔 진입 경로는 /human-capital/time/attendance-corrections로 고정한다.
  • 이 경로는 기존 Time 상품의 moduleRouteAccess 게이트를 그대로 적용한다.
  • 공용 통합 브랜치가 view와 최소 메뉴 링크를 연결했다. 이는 UI 진입점 변경이며 본 문서의 RPC·RLS·원장 계약에는 변경이 없다.

직원 RLS 투영

private.can_read_time_attendance_correction은 security invoker로 실행된다. 관리자 권한·본인 요청자 조건 외에도 플랫폼의 기존 안정 binding인 private.current_workforce_employee_id(company_id)p_employee_id를 직접 비교한다. 신규 HR workflow를 만들지 않으며, 관리자가 대신 만든 요청과 correction도 대상 직원에게 보인다. 이 직접 binding은 original event가 null이고 기존 attendance event가 0건인 최초 완전 누락도 포함한다. time_attendance_effective_events 역시 security-invoker view이므로 직원에게 보이는 correction이 원본을 가리고 manager-created replacement 또는 add를 현재 유효 기록으로 투영한다.

오류·엣지케이스

  • time_attendance_correction_evidence_changed: 원본 증거 변경, 재조회 필요
  • attendance_period_version_conflict: 다른 actor가 마감 상태 변경
  • time_attendance_correction_reopen_confirmation_required: 닫힌 기간 명시 동의 없음
  • attendance_reopen_reason_required: reopen reason 5자 미만
  • time_attendance_correction_already_decided: 요청 중복 판단
  • time_attendance_correction_original_already_corrected: 동일 원본의 중복 correction overlay 차단
  • time_attendance_correction_effective_sequence_invalid: 정정 후 출근·퇴근 교대 순서 위반
  • time_attendance_correction_effective_open_shifts_exist: 정정 후 열린 출근이 남아 재마감 불가
  • 같은 request key + 다른 payload: idempotency conflict
  • 승인 후 정정 원장을 업데이트·삭제: append-only trigger 거부

검증

  • SQL rollback smoke: supabase/tests/time_attendance_corrections_smoke.sql
  • migration contract: src/composables/__tests__/attendanceCorrectionMigration.spec.js
  • local repository: src/composables/attendanceCorrectionRepo.test.js
  • UI: src/components/time/attendance-corrections/AttendanceCorrectionPanelOrg.spec.js

UI Select 정합 2026-07-19 KST

근태 예외·정정·판단·출퇴근 화면의 native Select를 EDS 공식 anatomy로 정리했다. correctionKind, originalEventId, proposedEventType, decision, resolutionKind, scheduledShiftId payload와 RPC·RLS·원장 계약에는 변화가 없다.