Skip to content

시설 작업지시 백엔드 계약

목적과 범위

이 문서는 /facility/task/{unit|equipment}/task/general에서 소비하는 facility_work_orders 출시 수직 슬라이스의 as-built 계약입니다. 과거 /facility/maintenance/*/task/general은 같은 정식 경로로 redirect되며 별도 원장이나 API를 만들지 않습니다. subject_kind(equipment|unit) + subject_ref(text) 독립 계약은 계속 필수이며, 선택적으로 설비는 facility-owned facility_asset_id, 공간은 platform-core property_space_id를 함께 저장합니다.

설비 운영 현황판은 subject_kind='equipment'와 자산 subject_ref가 정확히 같은 미검수 행만 선택 연결합니다. 이름 유사도나 부분 일치로 연결하지 않으며, 작업지시 원장이 없어도 자산 현황은 독립적으로 동작합니다. 읽기 모델 상세는 facility-equipment-operations-board.md를 따릅니다.

기본 마이그레이션은 supabase/migrations/20260717090625_facility_work_orders.sql, exact target 확장은 supabase/migrations/20260718230000_facility_inspection_work_order_closure_loop.sql입니다. 실행형 검증은 기존 smoke와 supabase/tests/facility_inspection_work_order_closure_loop_smoke.sql입니다.

데이터 모델

public.facility_work_orders

  • 내부 고유코드: id uuid
  • 고객 비노출 안정키: management_code, Company 범위 unique
  • 고객 등록코드: registration_code, 선택값이며 중복 허용
  • 범위: company_id + office_id 복합 FK
  • 대상: subject_kindequipment|unit, subject_ref는 비어 있지 않은 독립 현장 참조
  • 선택 exact target: 설비는 facility_asset_id, 공간은 property_space_id. 둘 다 NULL이면 legacy/manual 독립 작업
  • 대상 종류·참조·선택 exact target은 생성 뒤 immutable
  • 상태: requested|assigned|in_progress|completed|verified
  • 우선순위: low|normal|high|urgent
  • 낙관적 동시성: revision bigint
  • 담당자 계약:
    • version 1: 기존 assignee_employee_id + assignee_assignment_id 호환 데이터. FK는 제거됐고 신규 기록에는 사용하지 않음
    • version 2: assignee_actor_user_id 또는 assignee_party_id 중 정확히 하나와 서버 생성 assignee_snapshot
    • snapshot은 배정 뒤 변경할 수 없으며 이후 모든 transition에 복제

public.facility_work_order_transitions

생성 이벤트와 모든 상태 전이를 기록하는 append-only 이력입니다. 생성은 NULL → requested, 이후에는 정확히 한 단계만 허용합니다. update/delete trigger는 facility_work_order_history_append_only를 발생시킵니다.

private.facility_work_order_commands

멱등 키, 정규화된 요청 payload, 응답 snapshot을 저장하는 비공개 ledger입니다. (company_id, command_name, request_key)가 unique이고 공개 Data API 권한은 없습니다. 같은 키와 다른 payload는 facility_work_order_idempotency_conflict입니다.

상태 머신

text
requested -> assigned -> in_progress -> completed -> verified
  • 역전, 건너뛰기, terminal 이후 전이는 facility_work_order_transition_invalid입니다.
  • expected_revision이 현재 revision과 다르면 facility_work_order_revision_conflict입니다.
  • assigned 전이는 assignee_actor_user_id 또는 assignee_party_id 중 정확히 하나가 필수입니다.
  • actor는 현재 Office membership·Company 관리자·유효한 operational role 중 하나여야 합니다.
  • party는 같은 Company의 활성 party이며 현재 Office의 활성 party assignment가 있어야 합니다.
  • snapshot은 플랫폼 공통 office_operational_actor_snapshot 또는 office_operational_party_snapshot에서 생성합니다. 클라이언트 표시값을 신뢰하지 않습니다.
  • 신규 version 2 행의 HR employee/assignment ID는 항상 NULL입니다.
  • 배정 이후 assignee identity는 변경하지 않습니다.

공개 RPC

create_facility_work_order_v2

입력:

  • p_company_id uuid
  • p_office_id uuid
  • p_subject_kind text
  • p_subject_ref text
  • p_facility_asset_id uuid|null
  • p_property_space_id uuid|null
  • p_title text
  • p_description text
  • p_priority text
  • p_registration_code text|null
  • p_request_key text

현재 설비·공간 생성 UI가 이 RPC를 소비합니다. 설비 선택 시 같은 Company·Office의 facility_assets(equipment), 공간 선택 시 활성 property_spaces를 검증합니다. 두 ID를 함께 보내거나 대상 종류와 다른 ID를 보내면 거부합니다. 관리코드, 요청 상태, revision, 요청일시와 actor는 서버가 정합니다. 기존 create_facility_work_order는 독립 subject_ref 호환 API로 유지합니다.

subject_ref는 선택 ID 유무와 무관하게 필수인 현장 참조 문자열입니다. exact ID가 있으면 현황판과 점검은 이를 우선 사용하고, 둘 다 NULL인 legacy/manual 행만 byte-exact 참조 또는 공간 별칭으로 연결합니다. 유사도·부분 일치·이름 추측은 금지합니다.

transition_facility_work_order

입력:

  • p_company_id uuid
  • p_work_order_id uuid
  • p_to_status text
  • p_expected_revision bigint
  • p_assignee_actor_user_id uuid|nullassigned 전이의 Office 사용자
  • p_assignee_party_id uuid|nullassigned 전이의 Office party
  • p_request_key text

두 public 함수는 SECURITY INVOKER wrapper이며 실제 변경은 private.*_impl에서 수행합니다. actor는 입력받지 않고 auth.uid()에서 파생합니다. authenticated는 테이블 SELECT만 가능하고 INSERT/UPDATE/DELETE는 revoke되어 있습니다. work-order guard trigger도 RPC transaction marker가 없는 쓰기를 거부합니다.

인가와 RLS

조회는 유효한 facility Office entitlement와 Company/Office 일치, 그리고 다음 중 하나를 요구합니다.

  • Company 운영 관리자
  • 활성 Office membership
  • facility_work_order_viewer 또는 facility_work_order_admin operational role grant

변경 RPC는 먼저 require_office_product(company, office, 'facility')를 통과한 뒤 다음 중 하나를 요구합니다.

  • Company 운영 관리자
  • 활성 Office office_admin|operator
  • 유효한 facility_work_order_admin role grant

두 공개 테이블은 RLS가 활성화되어 있고 authenticated에는 명시적 SELECT만 부여됩니다. private command ledger는 authenticated에서 직접 읽을 수 없습니다.

상세 화면 조회 계약

목록과 상세 화면은 별도 비보호 상세 RPC를 만들지 않고 Company·Office·대상 종류로 범위가 제한된 facility_work_orders SELECT 결과를 공유합니다. 단조 상태 머신에서 각 단계는 최대 한 번만 통과하므로 상세의 상태 이력은 행의 requested_at, assigned_at, started_at, completed_at, verified_at를 순서대로 사용합니다. 이 값은 facility_work_order_transitions의 단계별 발생일시와 일치해야 하며, 미도달 단계는 NULL입니다. 원장 감사나 actor 단위 이력 조회가 필요해질 때만 append-only transition 조회 계약을 별도로 노출합니다.

목록 원장과 get_office_operational_participants() 담당자 후보 원장은 독립 ledger입니다. participant 조회 실패를 facility_work_orders 목록·생성 실패로 승격하지 않습니다. 단, requested → assigned 전이는 유효한 participant identity가 필요하므로 후보 원장이 복구될 때까지 배정만 완료할 수 없습니다.

클라이언트는 Company·Office scope를 캡처한 명령에만 성공 결과를 반영합니다. Office 전환 뒤 도착한 create/transition 응답은 새 Office 목록·오류·pending 상태를 변경하거나 성공 UI를 닫는 근거가 될 수 없습니다. 서버 RLS와 product entitlement 검사는 이 클라이언트 차단과 별도로 항상 적용합니다.

감사

생성과 전이는 같은 transaction에서 private.append_operational_audit_event_impl을 호출합니다.

  • 생성: facility.work_order_requested
  • 전이: facility.work_order_assigned|in_progress|completed|verified
  • subject: facility_work_order
  • before/after snapshot, 관리코드, Office, expected revision, request correlation을 기록

감사 실패 시 작업지시 변경도 rollback됩니다.

오류 계약

코드의미
facility_work_order_permission_requiredCompany/Office 변경 권한 없음
facility_work_order_assignee_requiredactor/party가 없거나 둘 다 전달됨
office_operational_actor_not_found현재 Office에서 유효한 actor가 아님
office_operational_party_not_found현재 Office에서 유효한 party가 아님
facility_work_order_assignee_immutable배정 snapshot 또는 identity 변경 시도
product_not_entitled:facilityOffice 시설관리 entitlement 없음
facility_work_order_revision_conflict예상 revision 불일치
facility_work_order_transition_invalid단조 상태 전이 위반
facility_work_order_idempotency_conflict같은 요청키의 payload 불일치
facility_work_order_rpc_write_required테이블 직접 쓰기 시도

검증

로컬 disposable Supabase에서 마이그레이션 적용 후 supabase/tests/facility_work_orders_smoke.sql을 실행합니다. HR fixture 없이 entitlement 차단, actor·party 배정, 신규 HR ID NULL, snapshot 불변, 생성/전이 멱등성, 단계 건너뛰기 차단, terminal revision, history/audit 이벤트를 확인하고 전부 rollback합니다. 점검→작업지시 연결 smoke도 함께 실행해 기존 시설 내부 연결을 회귀 검증합니다.