다크모드
전자문서 발행 원장 — BE 핸드오프
독자: BE 개발자/AI. 전송기관에 종속되지 않는 발행 요청 원장, 불변 스냅샷, append-only 이벤트와 서버 전송 경계를 정의한다.
수정발행 진입을 하나로 합치고 판매·구매 현황을 관련 업무 링크로 표시한 변경은 프런트 IA다. 세금계산서·계산서 유형, 발행 요청, 외부 전송 상태, 기존 호환 라우트와 서버 식별 키는 변경하지 않는다.
Wave 11 발행 원장
정본 migration은 20260717185750_electronic_document_issue_ledger.sql이다.
electronic_document_issue_requests: 문서종류, 작성일자, opaque source key/revision, 발행자·수신자·라인·합계·원천 snapshot과 현재 local status를 저장한다.electronic_document_issue_events: 요청 생성, 대기열 등록, 서버 전송 결과, 취소를 append-only로 저장한다.private.electronic_document_issue_commands: create/cancel/retry의 full request와 응답을 보존하는 멱등 원장이다.create_electronic_document_issue_request(...): 권한과tax-invoiceentitlement를 확인해 로컬 요청을 만들고, 선택 가능한 서버 delivery가 있으면queued로 전이한다.cancel_electronic_document_issue_request(...): revision과 상태를 확인해 즉시 취소 또는cancel_pending을 기록한다.
브라우저는 공개 테이블을 SELECT하고 create/cancel/retry RPC만 호출한다. request/event 테이블 직접 쓰기와 private function 실행권은 없다.
불변 스냅샷
발행 요청은 다음 스냅샷을 생성 시점에 고정한다.
issuer_snapshot,recipient_snapshotline_items_snapshot,totals_snapshotsource_snapshot,snapshot_contract_version,snapshot_fingerprint
생성 뒤 업무 필드는 update/delete할 수 없다. 원천 상품 테이블·FK·RPC를 참조하지 않으며, 원천 마스터의 이후 변경도 이미 저장한 요청을 바꾸지 않는다. credential, token, password, secret, 외부 연동 key 성격의 값은 재귀 검사를 통해 snapshot에 저장하지 않는다.
동일 request key와 동일 full payload의 재시도는 기존 응답을 반환한다. 같은 key로 payload를 바꾸면 electronic_document_issue_idempotency_conflict다.
상태와 이벤트
local status는 다음 범위다.
adapter_pending → queued → submitted → succeeded | failed
취소는 현재 상태에 따라 cancel_pending | cancelled로 전이한다.
- create가 성공했다는 사실은 발행 요청이 저장됐다는 뜻이지 외부 전송 성공이 아니다.
queued는 서버 delivery가 등록됐다는 뜻이다.submitted,succeeded,failed는 서버 전용private.record_electronic_document_delivery_result(...)가 결과 이벤트를 기록할 때만 생긴다.- 결과 번호와 전송시각은
succeeded이벤트 payload와 occurred_at에서 읽는다. 성공 이벤트가 없으면 결과 번호를 만들거나 추정하지 않는다. - 이벤트는 update/delete할 수 없다. 이력은 현재 상태에서 역산하지 않고 저장된 이벤트만 표시한다.
전송 대기열과 재시도 경계
private.request_electronic_document_platform_delivery(...)는 공통 integration event와 delivery를 best-effort로 예약한다. adapter가 없거나 delivery 예약이 실패해도 전자문서 자체 요청은 롤백하지 않는다.
Wave 12 migration 20260717202832_electronic_document_public_retry.sql은 authenticated 사용자용 request_electronic_document_delivery_retry(...)를 추가한다.
- 입력:
company_id,issue_request_id,expected_revision,request_key - 권한: 로그인 사용자, 대상 Office 관리 권한, 활성
tax-invoiceentitlement를 모두 확인한다.anon,PUBLIC,service_role에는 execute를 주지 않는다. - 상태:
local_status='failed'에서만 허용한다. 성공하면queued, revision은 1 증가한다. - 동시성:
expected_revision이 현재 revision과 다르면 conflict다. - 멱등성:
(company_id, command_name='retry', request_key)의 full command payload를 저장한다. 동일 payload exact replay는 기존 응답을 반환하고, 같은 key의 다른 issue/revision은 idempotency conflict다. - 이벤트:
delivery_retry_queuedappend-only issue event를 남긴다.
재시도는 기존 issue/retry event에 연결된 claimable 또는 처리 중 delivery(pending|processing|failed)만 재사용한다. 그런 delivery가 없으면 electronic_document.issue_retry_requested라는 전송기관 중립 platform event와 claimable delivery를 새로 만든다. electronic_document.issue_cancel_requested에 연결된 cancel delivery는 조회 event type에서 제외되므로 절대 재사용하지 않는다.
공개 RPC의 성공은 대기열 등록 성공이다. 외부 전송 성공은 여전히 service worker가 결과를 기록할 때만 succeeded가 된다. backoff·최대 재시도 횟수·운영자 재처리 정책은 후속 운영 정책이다.
FE 소비 계약
electronicDocumentIssueRepo가 다음만 감싼다.
list({ companyId, officeId })listEvents({ companyId, officeId, issueRequestId })create(input)cancel(input)requestDeliveryRetry(input): Supabase는 공개 retry RPC에 company/request/revision/requestKey를 전달하고, mock도 같은 failed-only·revision 계약을 재현한다.
useTaxInvoiceIssuance는 요청과 이벤트를 hydrate하고 화면 상태를 다음처럼 번역한다.
adapter_pending | queued→전송대기submitted→전송중succeeded→전송완료failed→오류cancel_pending | cancelled→취소대기 | 취소완료
issue()는 create 응답까지만 처리한다. 성공 상태를 즉시 만들지 않는다. 상세 이력은 listEvents() 결과를 역순으로 렌더링하고 결과 번호도 성공 이벤트에서만 파생한다.
문서 구분과 회계 경계
- 과세 공급은 세금계산서, 면세 공급은 계산서, 영세율 공급은 영세율세금계산서다.
- 사업자등록번호가 없는 개인과 공급가액 0원은 현재 발행 대상에서 제외한다.
- 과세·면세가 함께 있으면 문서별 opaque source key로 각각 요청한다.
- 발행은 증빙 이벤트이며 추가 분개를 만들지 않는다. 매출·세액 인식은 원천 청구 확정의 책임이다.
- 현재 관리비 actual 화면은
useAllocations.taxInvoicesByProfile()로 후보 행을 산출하지만, 저장 이후 요청·상태·이력은 전자문서 원장만 소비한다. 다른 billing line의 일부 행은 아직 mock이다.
검증
- SQL smoke:
supabase/tests/electronic_document_issue_ledger_smoke.sql - 구조 감사:
electronicDocumentIssueLedgerMigration.spec.js - FE repo:
src/composables/__tests__/electronicDocumentIssueRepo.spec.js - 화면 상태:
src/composables/__tests__/useTaxInvoiceIssuance.spec.js - 공개 retry 구조:
src/composables/__tests__/electronicDocumentPublicRetryMigration.spec.js - 공개 retry SQL smoke:
supabase/tests/electronic_document_public_retry_smoke.sql
핵심 회귀는 create/retry가 queued에서 멈추고 server event만 success를 만들며, retry exact replay·revision·failed-only·권한·entitlement를 서버가 검증하고 cancel delivery를 재사용하지 않는다는 점이다.