다크모드
시설 작업지시 백엔드 계약
목적과 범위
이 문서는 /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_kind는equipment|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에 복제
- version 1: 기존
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 uuidp_office_id uuidp_subject_kind textp_subject_ref textp_facility_asset_id uuid|nullp_property_space_id uuid|nullp_title textp_description textp_priority textp_registration_code text|nullp_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 uuidp_work_order_id uuidp_to_status textp_expected_revision bigintp_assignee_actor_user_id uuid|null—assigned전이의 Office 사용자p_assignee_party_id uuid|null—assigned전이의 Office partyp_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_adminoperational role grant
변경 RPC는 먼저 require_office_product(company, office, 'facility')를 통과한 뒤 다음 중 하나를 요구합니다.
- Company 운영 관리자
- 활성 Office
office_admin|operator - 유효한
facility_work_order_adminrole 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_required | Company/Office 변경 권한 없음 |
facility_work_order_assignee_required | actor/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:facility | Office 시설관리 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도 함께 실행해 기존 시설 내부 연결을 회귀 검증합니다.