다크모드
구매·재고 기본 흐름 — Backend 계약
도메인 경계
구매·재고는 회계 매입보다 앞선 운영 원장이다. GoodsReceipt 생성은 Purchase 확정과 같지 않다. 입고 후 매입등록 후보를 만들고 증빙·공급가액·세액을 검토한 뒤 기존 매입 원장에 연결한다.
As-built — 구매요청 제출 (2026-07-17)
마이그레이션 20260717025956_procurement_request_submission.sql이 첫 서버 vertical slice를 소유한다.
procurement_requests: Company·Office 범위의 요청 header, 불변 관리코드, 고객 노출 구매요청 등록번호, 요청자 employee·유효 assignment·snapshot,revisionprocurement_request_lines: 요청 품목·수량·단위·예상 단가,asset_registration_expectedprivate.procurement_request_commands: 명령명 + 요청 키별 입력 payload·응답 snapshotpublic.create_procurement_request_draft: 인증 사용자에서 actor를 유도해 초안을 생성한다.public.submit_procurement_request:expected_revision이 일치하는 초안만 제출한다.
이 첫 마이그레이션은 draft → submitted를 소유한다. 후속 20260717055810_procurement_approval_order_workflow.sql이 결재·발주 전이를 추가한다.
As-built — 결재 연결과 발주 (2026-07-17)
public.request_procurement_approval: 요청 소유자 또는 구매 관리자가submittedrevision을 제시해야 한다. 공통 private helper로 결재문서와approval_subject_linkssnapshot을 만들고 즉시 제출한다. 브라우저에 범용 subject-link 생성 RPC는 열지 않는다.private.approval_subject_outbox_events: 공통 결재 계약이approved/rejectedterminal event를 append-only로 발행한다.public.consume_procurement_approval_result:service_role전용 consumer다. outbox UUID와 요청 키를private.procurement_approval_result_consumptions에 보존하고submitted → approved|rejected, revision2 → 3을 원자적으로 반영한다. 동일 event 재전달은 저장된 응답을 반환한다.public.create_purchase_order_from_request: 구매 관리자만approvedrevision과 같은 Office의 활성ap_vendorassignment를 제시해 발주한다.purchase_orders와purchase_order_lines는 승인된 요청 line·공급사·결재 링크·actor snapshot을 복사한 불변 원장이다. 요청은ordered, revision4가 된다.- 반려 요청은 발주 RPC가
procurement_request_not_approved로 거부한다.
결재 결과 actor는 terminal approval event의 employee·assignment·snapshot을 감사에 인과 관계로 남긴다. 구매 화면용 list_procurement_request_workflow_summaries는 링크 상태·결재번호/상태/version·발주번호/상태만 반환한다. 결재 본문, 의견, 수신/참조 정보의 RLS는 넓히지 않는다.
권한은 요청/발주 table RLS와 도메인 RPC에서 모두 다시 검사한다. 발주는 Company 관리자 또는 유효한 procurement_admin만 가능하고, public table의 authenticated 직접 쓰기는 금지한다. 명령 ledger와 advisory lock, expected revision, append-only outbox 소비 ledger가 재시도와 동시 실행을 방어한다.
식별자와 불변식
- 내부
id: UUID, UI 비노출 management_code: Company 안에서 unique이며 최초 생성 뒤 불변. 상세 보조정보용이다.request_number: 고객 노출 등록번호. 최초 값 뒤 불변이며 중복 가능하다. 미입력 시 관리코드와 별개의PR-연도-난수초기값을 사용한다.- Company·Office·요청자 employee·유효 assignment는 복합 FK로 같은 Company 범위를 강제한다. 로그인 사용자가 현재 유효한 직원·배치로 해석되지 않으면 생성·제출을 거부한다.
- 제출 시
submitted_at이 필수이고revision을 1 증가시킨다. - 제출 후 line insert/update/delete를 trigger로 거부한다. UPDATE는 이전·새 요청을 모두 검사해 제출 line을 다른 초안으로 옮기는 우회도 막는다.
권한·멱등·감사
- public table은 RLS를 켜고 직접 쓰기 권한을 주지 않는다. 활성 Company 구성원 중 해당 Office 구성원, Company 관리자, 범위가 맞는
procurement_admin만 읽고 제출할 수 있다. - RPC는
auth.uid()로 actor를 구한다. 클라이언트 actor ID는 받지 않는다. (company_id, command_name, request_key)unique + advisory transaction lock으로 재시도를 직렬화한다.- 같은 키·같은 payload는 기존 응답을 반환하고, 같은 키·다른 payload는
procurement_request_key_payload_conflict로 거부한다. - 제출 멱등 응답도 요청 소유자/관리자 권한과 현재 유효한 직원·배치를 먼저 확인한 뒤 반환한다.
- 제출은
expected_revision으로 stale write를 거부한다. - 생성·제출 모두 공통
operational_audit_events에 before/after, 요청 키, 행위자의 유효 assignment ID를 남긴다. 요청자 snapshot은 identity와 함께 불변이며 직원 식별·표시정보만 담고 민감정보는 담지 않는다.
검증 파일은 supabase/tests/procurement_request_submission_smoke.sql, JS 계약은 src/composables/__tests__/procurementRequestMigration.spec.js와 procurementRequestRepo.spec.js다.
전자결재 결재선의 정본은 approval v2 participantId다. 구매 repository는 get_approval_participant_directory가 반환한 auth user를 사용하며, 기존 구매 RPC의 JSON employeeId/assignmentId 키에는 전환 기간 동안 동일한 participant user UUID를 전달한다. 구매 도메인이 HR 직원을 결재 권한으로 재검증하지 않는다.
배포 순서는 마이그레이션 적용 → 로컬/스테이징 스모크 → VITE_PROCUREMENT_REQUEST_PERSISTENCE=true 활성화다. 플래그 기본값은 꺼짐이며, 운영 Supabase에 마이그레이션을 적용하기 전에 앱이 미배포 RPC를 호출하지 않도록 한다.
권장 엔터티
procurement_requests: Company, Office, 요청자, 필요일, 상태procurement_request_lines: 품목, 수량, 예상 단가purchase_orders: 공급사, 발주일, 예정일, 상태purchase_order_lines: 발주량, 누적 입고량, 단가goods_receipts: 입고일, 검수자, 검수 상태goods_receipt_lines: 입고량, 합격량, 보류량stock_movements: 입고·출고·이동·조정 원자stock_balances: Office·창고·로케이션·품목별 projectionpurchase_registration_candidates: 입고와 기존 매입 원장 연결 상태asset_registration_candidates: 자산 추적 품목별 개체 후보
상태와 명령
text
PurchaseRequest(as-built): DRAFT → SUBMITTED → APPROVED | REJECTED
PurchaseRequest(as-built): APPROVED → ORDERED
PurchaseOrder(as-built): ORDERED
PurchaseOrder(next): ORDERED → PARTIALLY_RECEIVED → RECEIVED
PurchaseCandidate: PENDING → LINKED | EXCLUDED
AssetCandidate: PENDING → REGISTERED | EXCLUDED- 승인 요청·결과 소비·발주는 권한, subject snapshot, 낙관적 잠금 버전을 검사한다.
- 입고 명령은
idempotency_key를 필수로 받고 중복 재시도 시 같은 결과를 반환한다. received_quantity + incoming_quantity <= ordered_quantity를 트랜잭션 안에서 강제한다.- 입고·재고 이동·후속 후보 생성을 한 DB 트랜잭션으로 커밋한다.
- 재고 잔량을 직접 덮어쓰지 않고
stock_movements합계로 projection을 갱신한다. - 요청·승인·발주·입고·취소·연결에는 actor, timestamp, Office, 변경 전후 값을 감사로그에 남긴다.
기존 구매·매입채무 연결
purchase_registration_candidates는 다음 자료를 제공하되 회계 필드를 임의 확정하지 않는다.
- 공급사와 거래처 코드
- 발주·입고 참조번호
- 실제 입고 품목·수량·발주 단가의 합계
- Office와 검수일
세금유형, 공급가액, VAT, 비용·자산 계정, 증빙유형은 기존 매입 등록 단계에서 확인한다. 연결 완료 시 purchase_id를 기록하며 같은 후보를 두 번 연결하지 않도록 unique constraint를 둔다.
자산관리 연결
요청 line은 asset_registration_expected=true로 자산등록 필요 의도만 남긴다. 이 단계에서 asset_id나 자산 관리코드를 만들지 않는다. 후속 입고 원장은 asset_tracked=true 품목의 합격 수량만큼 자산 후보를 만들고, 자산 확정 전 시리얼번호·사용자·공간·감가상각 방법을 수집한다. 재고 후보와 회계 고정자산 레코드는 동일 개체 식별자를 공유하되 각 원장의 책임은 분리한다.
제외 범위
제조 BOM, MRP, 생산오더, 공정·재공품, 공급망 수요예측, 물류 최적화는 별도 제품 결정 전 구현하지 않는다.