Files
startover/docs/analytics/event-schema.md
T
Johngreen 7f59b94dcf Rename project from Re:Link to Startover
Rebrand repository from "Re:Link" to "Startover" across the codebase. Updates include package names and scopes (@relink/* -> @startover/*), import paths, Next.js transpile settings, vitest name, UI text and docs, Dockerfile and CI/workflow names, deploy scripts and repo paths, and example/production env values. Also add auth-related env vars, an apps/web .env symlink, and small formatting/typing cleanups in several TSX/TS files and tests to accommodate the rename.
2026-03-08 20:22:08 +09:00

47 KiB

Startover 이벤트 스키마 정의서

문서 코드: D004 버전: 1.0.0 작성일: 2026-03-07 적용 범위: MVP Phase 1 전체 도메인 승인 상태: 초안 관련 문서: schema-prisma-draft.md, D001(subsidy-policy.md), D002(contract-escrow-policy.md), D003(data-exposure-policy.md)


목차

  1. 목적 및 설계 원칙
  2. 공통 이벤트 엔벨로프
  3. 도메인별 이벤트 목록 및 payload 스키마
  4. Outbox 발행 정책
  5. 향후 확장

1. 목적 및 설계 원칙

1-1. 목적

이 문서는 Startover 플랫폼에서 EventLog 테이블에 저장되는 도메인 이벤트의 규격을 정의한다.

이벤트 로그는 세 가지 목적으로 활용된다.

  • 감사 추적: 도메인 객체의 상태 전환을 불변(immutable) append-only 방식으로 보존한다.
  • 비동기 연동: OutboxEvent를 통해 외부 시스템(알림, 정산, 분석)에 이벤트를 발행한다.
  • KPI 분석: 매칭 전환율, 지원금 케이스 성공률, 에스크로 정산 속도 등 지표 산출에 활용한다.

EventLogAuditLog와 구분된다. AuditLog는 운영자/사용자 행위의 before/after 스냅샷을 운영 감사 목적으로 남기는 반면, EventLog는 도메인 이벤트를 비즈니스 의미 단위로 기록하고 후속 처리의 입력으로 활용한다.

1-2. 이벤트 네이밍 규칙

이벤트 이름은 {도메인}.{액션} 또는 {도메인}.{엔티티}_{액션} 형태를 따른다.

{도메인}.{액션}
예: store.submitted, match.accepted, subsidy.approved

{도메인}.{엔티티}_{액션}
예: store.deal_status_changed, subsidy.document_uploaded, vendor.certification_approved

규칙:

  • 도메인과 액션은 모두 소문자 스네이크케이스(snake_case)를 사용한다.
  • 도메인과 액션 사이는 점(.)으로 구분한다.
  • 엔티티와 액션 사이는 언더스코어(_)로 구분한다.
  • 액션은 과거형(completed action)으로 표현한다. create가 아니라 created, accept가 아니라 accepted.
  • 한 이벤트는 정확히 하나의 비즈니스 사실을 표현한다. 복수 상태 전환을 하나의 이벤트로 합치지 않는다.

도메인 접두어 목록:

접두어 대응 도메인 / aggregate
user Identity — User, UserConsent
store Store Supply — Store, StorePhoto
match Matching — MatchRequest
subsidy Subsidy — SubsidyCase, SubsidyDocument
vendor Vendor — Vendor, VendorCertification
contract Trust — Contract
escrow Trust — EscrowTransaction
inspection Trust — InspectionRecord
dispute Trust — DisputeCase
operator Backoffice — 운영자 배정
policy Backoffice — PolicyVersion 활성화

1-3. 이벤트 버전 관리 정책

eventVersion 필드는 동일한 eventName의 payload 스키마가 하위 호환이 깨지는 방식으로 변경될 때 증가한다.

상황 처리 방식
새 필드 추가 (선택적) eventVersion 유지. 소비자는 미지 필드를 무시해야 한다.
필드 타입 변경 또는 필수 필드 제거 eventVersionv2, v3으로 증가. 구버전 소비자와 공존 가능 기간을 명시한다.
이벤트 이름 변경 이전 이름을 deprecated로 표시하고 마이그레이션 기간 동안 양쪽을 동시 발행한다.

초기 버전은 v1이다. 버전 문자열 형식: v{N} (예: v1, v2).

1-4. PII 레벨 정책

piiLevel 필드는 payload에 개인정보가 포함되는 정도를 나타낸다. 이 값은 이벤트 스트림을 외부 시스템에 발행하거나 로그를 장기 보관할 때 마스킹 또는 접근 제어 기준으로 사용한다.

레벨 정의 payload 예시
NONE 개인 식별 정보 없음. 집계 ID 또는 상태값만 포함. storeId, status, amount
LOW 간접 식별 가능한 정보 포함. 이름·연락처·주소는 없으나 내부 ID로 역추적 가능. userId, vendorId, matchRequestId
HIGH 직접 식별 정보 포함. 이름, 이메일, 전화번호, 주소, 사업자번호 등. email, phone, businessRegistrationNumber

원칙:

  • payload에 HIGH PII가 포함되어야 한다면 해당 필드를 포함하지 않는 대신 연결 ID(예: userId)만 기록하고 PII는 원본 테이블에서 조회한다.
  • OutboxEvent로 외부 발행 시 HIGH 이벤트는 발행하지 않거나, 발행 전 PII 필드를 제거한 별도 payload를 사용한다.
  • NONE 이벤트는 별도 접근 제어 없이 분석 시스템에 전달할 수 있다.

2. 공통 이벤트 엔벨로프

모든 이벤트는 EventLog 테이블의 다음 컬럼 구조를 공유한다.

2-1. EventLog 컬럼 정의

컬럼 타입 설명
id BigInt 내부 자동 증가 PK. 외부에 노출하지 않는다.
aggregateType String 이벤트가 속한 aggregate 타입. 예: Store, MatchRequest, SubsidyCase
aggregateId String aggregate의 publicId 또는 내부 ID (BigInt를 문자열로 직렬화).
eventName String 이벤트 이름. 네이밍 규칙 참조. 예: store.submitted
eventVersion String payload 스키마 버전. 기본값 v1.
eventKey String 중복 방지용 고유 키. @unique 제약. 형식: {aggregateType}:{aggregateId}:{eventName}:{idempotencyToken}
payloadJson Json 이벤트 상세 데이터. 도메인별 스키마는 섹션 3 참조.
actorUserId BigInt? 이벤트를 발생시킨 사용자 ID. 시스템 자동 이벤트인 경우 null.
causationId String? 이 이벤트를 직접 유발한 이벤트의 eventKey. 이벤트 체이닝 추적에 사용.
correlationId String 동일 비즈니스 트랜잭션을 묶는 ID. HTTP 요청 ID 또는 saga ID.
piiLevel String NONE / LOW / HIGH. 섹션 1-4 참조.
occurredAt DateTime 이벤트가 비즈니스 레벨에서 발생한 시각 (UTC).
recordedAt DateTime DB에 기록된 시각 (UTC). @default(now()).

2-2. payloadJson 공통 구조 권고

모든 payload는 아래 최상위 필드를 포함하도록 권고한다. 도메인별 필드는 추가로 확장한다.

{
  "aggregateId": "store_abc123",
  "previousStatus": "DRAFT",
  "currentStatus": "SUBMITTED",
  "triggeredBy": "USER_ACTION"
}
공통 필드 타입 설명
aggregateId string aggregate 식별자 (publicId).
previousStatus string? 전환 전 상태. 상태 전환 이벤트에서 필수.
currentStatus string? 전환 후 상태. 상태 전환 이벤트에서 필수.
triggeredBy string USER_ACTION / OPERATOR_ACTION / SYSTEM_AUTO / WEBHOOK

2-3. eventKey 생성 규칙

eventKey는 멱등성 보장을 위해 고유해야 한다.

{aggregateType}:{aggregateId}:{eventName}:{ulid 또는 uuid}
예: Store:store_abc123:store.submitted:01HXXXXXXXXXXXXX

재시도 안전성이 필요한 경우 idempotencyToken에 외부에서 주입된 멱등 키를 사용할 수 있다.


3. 도메인별 이벤트 목록 및 payload 스키마


3-1. Identity 도메인

aggregateType: User


user.registered

사용자가 신규 회원가입을 완료했을 때 발행된다.

항목
eventName user.registered
aggregateType User
piiLevel LOW
triggeredBy USER_ACTION

payloadJson:

{
  "aggregateId": "user_abc123",
  "userId": "user_abc123",
  "primaryRole": "CLOSING_OWNER",
  "registrationChannel": "KAKAO_OAUTH"
}
필드 타입 설명
userId string 사용자 publicId
primaryRole string UserRole enum 값. CLOSING_OWNER / FOUNDER / VENDOR_MANAGER / OPS_MANAGER / SUBSIDY_OPERATOR / TRUST_OPERATOR / FINANCE_OPERATOR / SUPER_ADMIN
registrationChannel string KAKAO_OAUTH / NAVER_OAUTH / GOOGLE_OAUTH / EMAIL

user.verified

이메일 또는 전화번호 인증이 완료되었을 때 발행된다.

항목
eventName user.verified
aggregateType User
piiLevel LOW
triggeredBy USER_ACTION

payloadJson:

{
  "aggregateId": "user_abc123",
  "userId": "user_abc123",
  "verificationType": "EMAIL",
  "verifiedAt": "2026-03-07T10:05:00Z"
}
필드 타입 설명
verificationType string EMAIL / PHONE
verifiedAt string ISO 8601 UTC

user.profile_updated

사용자 프로필 정보가 변경되었을 때 발행된다. 변경된 필드 목록만 기록하며 값 자체는 포함하지 않는다(PII 보호).

항목
eventName user.profile_updated
aggregateType User
piiLevel LOW
triggeredBy USER_ACTION

payloadJson:

{
  "aggregateId": "user_abc123",
  "userId": "user_abc123",
  "updatedFields": ["name", "phone"],
  "profileType": "CLOSING_OWNER"
}
필드 타입 설명
updatedFields string[] 변경된 필드명 목록. 실제 값은 포함하지 않는다.
profileType string? UserProfile 변경인 경우 프로필 타입

user.consent_granted / user.consent_revoked

동의 항목이 수락 또는 철회될 때 발행된다.

항목
eventName user.consent_granted 또는 user.consent_revoked
aggregateType User
piiLevel LOW
triggeredBy USER_ACTION

payloadJson:

{
  "aggregateId": "user_abc123",
  "userId": "user_abc123",
  "consentType": "STORE_PUBLICATION_CONSENT",
  "policyVersionId": "polv_xyz",
  "isGranted": true
}
필드 타입 설명
consentType string TERMS_OF_SERVICE / PRIVACY_POLICY_REQUIRED / PRIVACY_POLICY_MARKETING / STORE_PUBLICATION_CONSENT / MATCHED_INFO_DISCLOSURE / THIRD_PARTY_MATCHED_PARTY / NOTIFICATION_KAKAO
policyVersionId string 동의 당시 적용된 PolicyVersion publicId
isGranted boolean true(granted) / false(revoked)

user.suspended / user.reactivated

운영자가 계정을 정지하거나 재활성화할 때 발행된다.

항목
eventName user.suspended 또는 user.reactivated
aggregateType User
piiLevel LOW
triggeredBy OPERATOR_ACTION

payloadJson:

{
  "aggregateId": "user_abc123",
  "userId": "user_abc123",
  "previousStatus": "ACTIVE",
  "currentStatus": "SUSPENDED",
  "reasonCode": "POLICY_VIOLATION",
  "operatorId": "user_op01"
}
필드 타입 설명
previousStatus string UserStatus enum 값. PENDING_VERIFICATION / ACTIVE / SUSPENDED / DEACTIVATED
currentStatus string UserStatus enum 값
reasonCode string 사유 코드
operatorId string 처리한 운영자 userId

3-2. Store Supply 도메인

aggregateType: Store


store.draft_created

폐업자가 매장 등록 초안을 생성했을 때 발행된다.

항목
eventName store.draft_created
aggregateType Store
piiLevel LOW
triggeredBy USER_ACTION

payloadJson:

{
  "aggregateId": "store_abc123",
  "storeId": "store_abc123",
  "ownerUserId": "user_abc123",
  "industryLeafId": 42,
  "regionClusterId": 7
}
필드 타입 설명
storeId string Store publicId
ownerUserId string 폐업자 userId
industryLeafId number 업종 말단 분류 ID (IndustryTaxonomy 참조)
regionClusterId number 지역 클러스터 ID (RegionHierarchy 참조)

store.submitted

폐업자가 매장 등록을 운영자 검토용으로 제출했을 때 발행된다.

항목
eventName store.submitted
aggregateType Store
piiLevel LOW
triggeredBy USER_ACTION

payloadJson:

{
  "aggregateId": "store_abc123",
  "storeId": "store_abc123",
  "ownerUserId": "user_abc123",
  "previousStatus": "DRAFT",
  "currentStatus": "SUBMITTED",
  "submittedAt": "2026-03-07T11:00:00Z"
}
필드 타입 설명
previousStatus string StoreReviewStatus 값. DRAFT
currentStatus string StoreReviewStatus 값. SUBMITTED

store.approved / store.rejected

운영자가 매장 등록 검토를 완료했을 때 발행된다.

항목
eventName store.approved 또는 store.rejected
aggregateType Store
piiLevel LOW
triggeredBy OPERATOR_ACTION

payloadJson (approved):

{
  "aggregateId": "store_abc123",
  "storeId": "store_abc123",
  "previousStatus": "REVIEWING",
  "currentStatus": "APPROVED",
  "operatorId": "user_op01",
  "approvedAt": "2026-03-07T14:00:00Z"
}

payloadJson (rejected):

{
  "aggregateId": "store_abc123",
  "storeId": "store_abc123",
  "previousStatus": "REVIEWING",
  "currentStatus": "REJECTED",
  "operatorId": "user_op01",
  "reasonCode": "INVALID_DOCUMENT",
  "rejectedAt": "2026-03-07T14:00:00Z"
}
필드 타입 설명
operatorId string 처리한 운영자 userId
reasonCode string? store.rejected 시 필수. 반려 사유 코드.

store.published / store.unpublished

매장의 공개 상태(publicationStatus)가 변경될 때 발행된다.

항목
eventName store.published 또는 store.unpublished
aggregateType Store
piiLevel LOW
triggeredBy USER_ACTION 또는 OPERATOR_ACTION

payloadJson:

{
  "aggregateId": "store_abc123",
  "storeId": "store_abc123",
  "previousPublicationStatus": "PRIVATE",
  "currentPublicationStatus": "PUBLISHED",
  "triggeredBy": "USER_ACTION",
  "consentRecordId": "consent_xyz"
}
필드 타입 설명
previousPublicationStatus string StorePublicationStatus enum 값. PRIVATE / RESTRICTED / PUBLISHED / UNPUBLISHED
currentPublicationStatus string StorePublicationStatus enum 값
consentRecordId string? store.published 시 — 정보 공개에 동의한 UserConsent ID

store.deal_status_changed

매장의 거래 상태(dealStatus)가 변경될 때 발행된다. OPEN→MATCHING→RESERVED→CONTRACTED→CLOSED→CANCELLED 전환 모두 이 이벤트로 처리한다.

항목
eventName store.deal_status_changed
aggregateType Store
piiLevel LOW
triggeredBy SYSTEM_AUTO 또는 OPERATOR_ACTION

payloadJson:

{
  "aggregateId": "store_abc123",
  "storeId": "store_abc123",
  "previousDealStatus": "OPEN",
  "currentDealStatus": "MATCHING",
  "triggeredBy": "SYSTEM_AUTO",
  "relatedMatchRequestId": "match_xyz",
  "relatedContractId": null
}
필드 타입 설명
previousDealStatus string StoreDealStatus enum 값. OPEN / MATCHING / RESERVED / CONTRACTED / CLOSED / CANCELLED
currentDealStatus string StoreDealStatus enum 값
relatedMatchRequestId string? 매칭 요청 수락으로 인한 전환인 경우
relatedContractId string? 계약 완료/취소로 인한 전환인 경우

store.photo_uploaded / store.photo_removed

매장 사진이 업로드되거나 삭제될 때 발행된다.

항목
eventName store.photo_uploaded 또는 store.photo_removed
aggregateType Store
piiLevel NONE
triggeredBy USER_ACTION 또는 OPERATOR_ACTION

payloadJson:

{
  "aggregateId": "store_abc123",
  "storeId": "store_abc123",
  "photoId": "12345",
  "photoCategory": "KITCHEN",
  "visibilityScope": "RESTRICTED",
  "uploadedByUserId": "user_abc123"
}
필드 타입 설명
photoId string StorePhoto 내부 ID (BigInt를 문자열로 직렬화)
photoCategory string PhotoCategory enum 값. EXTERIOR / INTERIOR / KITCHEN / EQUIPMENT / FLOOR_PLAN / DOCUMENT
visibilityScope string VisibilityScope enum 값. INTERNAL / MATCHED_ONLY / PUBLIC_SUMMARY
uploadedByUserId string? store.photo_removed 시에는 삭제 실행자 userId

3-3. Matching 도메인

aggregateType: MatchRequest


match.requested

창업자/업체/운영자 추천으로 매칭 요청이 생성될 때 발행된다.

항목
eventName match.requested
aggregateType MatchRequest
piiLevel LOW
triggeredBy USER_ACTION 또는 OPERATOR_ACTION

payloadJson:

{
  "aggregateId": "match_abc123",
  "matchRequestId": "match_abc123",
  "storeId": "store_xyz",
  "matchType": "ACQUISITION",
  "requesterUserId": "user_founder01",
  "requesterVendorId": null,
  "sourceType": "USER_INITIATED"
}
필드 타입 설명
matchType string MatchType enum 값. ACQUISITION / DEMOLITION / INTERIOR
requesterUserId string? 창업자(FOUNDER)인 경우 userId
requesterVendorId string? 업체(VENDOR_MANAGER)인 경우 vendorId
sourceType string USER_INITIATED / OPERATOR_RECOMMENDED / SYSTEM_SUGGESTED

match.accepted / match.rejected

폐업자 또는 운영자가 매칭 요청을 수락하거나 거절했을 때 발행된다.

항목
eventName match.accepted 또는 match.rejected
aggregateType MatchRequest
piiLevel LOW
triggeredBy USER_ACTION 또는 OPERATOR_ACTION

payloadJson:

{
  "aggregateId": "match_abc123",
  "matchRequestId": "match_abc123",
  "storeId": "store_xyz",
  "matchType": "ACQUISITION",
  "previousStatus": "REVIEWING",
  "currentStatus": "ACCEPTED",
  "decisionByUserId": "user_owner01",
  "decisionReasonCode": null
}
필드 타입 설명
previousStatus string MatchRequestStatus enum 값
currentStatus string ACCEPTED 또는 REJECTED
decisionByUserId string 결정한 사용자 또는 운영자 userId
decisionReasonCode string? match.rejected 시 반려 사유 코드

match.expired / match.cancelled

매칭 요청이 만료되거나 취소될 때 발행된다.

항목
eventName match.expired 또는 match.cancelled
aggregateType MatchRequest
piiLevel LOW
triggeredBy SYSTEM_AUTO 또는 USER_ACTION 또는 OPERATOR_ACTION

payloadJson:

{
  "aggregateId": "match_abc123",
  "matchRequestId": "match_abc123",
  "storeId": "store_xyz",
  "previousStatus": "OPEN",
  "currentStatus": "EXPIRED",
  "triggeredBy": "SYSTEM_AUTO",
  "reasonCode": "NO_RESPONSE_TIMEOUT"
}

match.completed

매칭이 정상 완료될 때(계약 완료 이후) 발행된다.

항목
eventName match.completed
aggregateType MatchRequest
piiLevel LOW
triggeredBy SYSTEM_AUTO

payloadJson:

{
  "aggregateId": "match_abc123",
  "matchRequestId": "match_abc123",
  "storeId": "store_xyz",
  "matchType": "ACQUISITION",
  "previousStatus": "CONTRACTING",
  "currentStatus": "COMPLETED",
  "relatedContractId": "contract_xyz"
}

3-4. Subsidy 도메인

aggregateType: SubsidyCase


subsidy.case_created

지원금 케이스가 DRAFT 상태로 생성될 때 발행된다.

항목
eventName subsidy.case_created
aggregateType SubsidyCase
piiLevel LOW
triggeredBy USER_ACTION

payloadJson:

{
  "aggregateId": "scase_abc123",
  "subsidyCaseId": "scase_abc123",
  "storeId": "store_xyz",
  "applicantUserId": "user_abc123",
  "programCode": "HOPE_RETURN_PKG",
  "policyVersionId": "polv_xyz",
  "checklistVersionCode": "HOPE_RETURN_PKG_V1"
}
필드 타입 설명
programCode string 지원 프로그램 코드. MVP는 HOPE_RETURN_PKG
policyVersionId string 케이스 생성 시점에 고정되는 PolicyVersion publicId
checklistVersionCode string 케이스 생성 시점의 체크리스트 버전. 예: HOPE_RETURN_PKG_V1

subsidy.eligibility_checked

자격 판별이 실행되어 결과가 결정될 때 발행된다. SubsidyCaseELIGIBILITY_CHECKED 상태로 전환될 때 발행한다.

항목
eventName subsidy.eligibility_checked
aggregateType SubsidyCase
piiLevel LOW
triggeredBy SYSTEM_AUTO

payloadJson:

{
  "aggregateId": "scase_abc123",
  "subsidyCaseId": "scase_abc123",
  "storeId": "store_xyz",
  "eligibilityResult": "ELIGIBLE",
  "previousStatus": "DRAFT",
  "currentStatus": "ELIGIBILITY_CHECKED",
  "policyVersionId": "polv_xyz"
}
필드 타입 설명
eligibilityResult string EligibilityResult enum 값. ELIGIBLE / CONDITIONALLY_ELIGIBLE / NOT_ELIGIBLE / UNKNOWN

eligibilitySnapshotJson 원본은 SubsidyCase 테이블에 저장하며 이벤트 payload에는 포함하지 않는다(PII 보호).


subsidy.document_uploaded / subsidy.document_reviewed

지원금 서류가 업로드되거나 운영자 검토가 완료될 때 발행된다.

항목
eventName subsidy.document_uploaded 또는 subsidy.document_reviewed
aggregateType SubsidyCase
piiLevel LOW
triggeredBy USER_ACTION 또는 OPERATOR_ACTION

payloadJson (document_uploaded):

{
  "aggregateId": "scase_abc123",
  "subsidyCaseId": "scase_abc123",
  "subsidyDocumentId": "sdoc_xyz",
  "documentTypeCode": "HRP_DOC_001",
  "itemCode": "HRP_DOC_001",
  "uploadedByUserId": "user_abc123",
  "isResubmission": false
}

payloadJson (document_reviewed):

{
  "aggregateId": "scase_abc123",
  "subsidyCaseId": "scase_abc123",
  "subsidyDocumentId": "sdoc_xyz",
  "documentTypeCode": "HRP_DOC_001",
  "reviewResult": "APPROVED",
  "reviewedByOperatorId": "user_op01",
  "reasonCode": null
}
필드 타입 설명
isResubmission boolean 반려 후 재업로드 여부
reviewResult string APPROVED / REJECTED / RESUBMITTED
reasonCode string? REJECTED 시 반려 사유 코드. MISSING_DOCUMENT / INVALID_DOCUMENT / ILLEGIBLE_DOCUMENT / EXPIRED_DOCUMENT / ELIGIBILITY_ISSUE / INCOMPLETE_INFO / MISMATCHED_INFO / WRONG_FORMAT

subsidy.submitted / subsidy.approved / subsidy.rejected

지원금 케이스의 주요 상태 전환 이벤트다.

항목
eventName subsidy.submitted / subsidy.approved / subsidy.rejected
aggregateType SubsidyCase
piiLevel LOW
triggeredBy USER_ACTION 또는 OPERATOR_ACTION

payloadJson (submitted):

{
  "aggregateId": "scase_abc123",
  "subsidyCaseId": "scase_abc123",
  "storeId": "store_xyz",
  "programCode": "HOPE_RETURN_PKG",
  "previousStatus": "READY_TO_SUBMIT",
  "currentStatus": "SUBMITTED"
}

payloadJson (approved):

{
  "aggregateId": "scase_abc123",
  "subsidyCaseId": "scase_abc123",
  "storeId": "store_xyz",
  "programCode": "HOPE_RETURN_PKG",
  "previousStatus": "SUBMITTED",
  "currentStatus": "APPROVED",
  "operatorId": "user_op01"
}

payloadJson (rejected):

{
  "aggregateId": "scase_abc123",
  "subsidyCaseId": "scase_abc123",
  "storeId": "store_xyz",
  "programCode": "HOPE_RETURN_PKG",
  "previousStatus": "SUBMITTED",
  "currentStatus": "REJECTED",
  "operatorId": "user_op01",
  "rejectionReasonCode": "INELIGIBLE_AFTER_REVIEW"
}

3-5. Vendor 도메인

aggregateType: Vendor


vendor.registered

업체가 플랫폼에 신규 등록될 때 발행된다.

항목
eventName vendor.registered
aggregateType Vendor
piiLevel LOW
triggeredBy USER_ACTION

payloadJson:

{
  "aggregateId": "vendor_abc123",
  "vendorId": "vendor_abc123",
  "ownerUserId": "user_vendor01",
  "vendorType": "DEMOLITION",
  "primaryRegionId": 7
}
필드 타입 설명
vendorType string VendorType enum 값. DEMOLITION / INTERIOR / BOTH
primaryRegionId number 주요 서비스 지역 ID (RegionHierarchy 참조)

vendor.certification_applied / vendor.certification_approved / vendor.certification_rejected

업체 인증 신청, 승인, 반려 이벤트다.

항목
eventName vendor.certification_applied / vendor.certification_approved / vendor.certification_rejected
aggregateType Vendor
piiLevel LOW
triggeredBy USER_ACTION 또는 OPERATOR_ACTION

payloadJson (certification_applied):

{
  "aggregateId": "vendor_abc123",
  "vendorId": "vendor_abc123",
  "certificationId": "vcert_xyz",
  "requestedScopeCode": "DEMOLITION_STANDARD",
  "previousCertificationStatus": null,
  "currentCertificationStatus": "APPLIED"
}

payloadJson (certification_approved):

{
  "aggregateId": "vendor_abc123",
  "vendorId": "vendor_abc123",
  "certificationId": "vcert_xyz",
  "previousCertificationStatus": "REVIEWING",
  "currentCertificationStatus": "APPROVED",
  "reviewedByOperatorId": "user_op01",
  "validUntil": "2027-03-07T00:00:00Z"
}

payloadJson (certification_rejected):

{
  "aggregateId": "vendor_abc123",
  "vendorId": "vendor_abc123",
  "certificationId": "vcert_xyz",
  "previousCertificationStatus": "REVIEWING",
  "currentCertificationStatus": "REJECTED",
  "reviewedByOperatorId": "user_op01",
  "reasonCode": "INSUFFICIENT_DOCUMENTS"
}
필드 타입 설명
currentCertificationStatus string VendorCertificationStatus enum 값. APPLIED / REVIEWING / APPROVED / REJECTED / SUSPENDED / EXPIRED
validUntil string? certification_approved 시 인증 유효 기간

3-6. Trust Infrastructure 도메인


Contract 이벤트

aggregateType: Contract


contract.created

매칭 수락 후 계약이 생성될 때 발행된다.

항목
eventName contract.created
aggregateType Contract
piiLevel LOW
triggeredBy SYSTEM_AUTO 또는 OPERATOR_ACTION

payloadJson:

{
  "aggregateId": "contract_abc123",
  "contractId": "contract_abc123",
  "matchRequestId": "match_xyz",
  "storeId": "store_xyz",
  "contractType": "ACQUISITION",
  "templateCode": "ACQUISITION_V1",
  "storeOwnerUserId": "user_owner01",
  "buyerUserId": "user_founder01",
  "vendorId": null
}
필드 타입 설명
contractType string ContractType enum 값. ACQUISITION / DEMOLITION / INTERIOR
templateCode string 사용된 계약서 템플릿 코드
vendorId string? DEMOLITION / INTERIOR 계약인 경우

contract.signed

계약 양측 서명이 완료될 때 발행된다.

항목
eventName contract.signed
aggregateType Contract
piiLevel LOW
triggeredBy SYSTEM_AUTO

payloadJson:

{
  "aggregateId": "contract_abc123",
  "contractId": "contract_abc123",
  "contractVersionId": "cver_xyz",
  "previousStatus": "SIGNING",
  "currentStatus": "SIGNED",
  "signerCount": 2
}

서명자 개인정보(ipAddress, userAgent)는 SignatureEvidence 테이블에만 저장하고 이벤트 payload에는 포함하지 않는다.


contract.completed / contract.cancelled

계약이 완료되거나 취소될 때 발행된다.

항목
eventName contract.completed 또는 contract.cancelled
aggregateType Contract
piiLevel LOW
triggeredBy SYSTEM_AUTO 또는 OPERATOR_ACTION

payloadJson (completed):

{
  "aggregateId": "contract_abc123",
  "contractId": "contract_abc123",
  "storeId": "store_xyz",
  "contractType": "ACQUISITION",
  "previousStatus": "ACTIVE",
  "currentStatus": "COMPLETED",
  "triggeredBy": "SYSTEM_AUTO",
  "relatedEscrowTransactionId": "etx_xyz"
}

payloadJson (cancelled):

{
  "aggregateId": "contract_abc123",
  "contractId": "contract_abc123",
  "storeId": "store_xyz",
  "contractType": "ACQUISITION",
  "previousStatus": "SIGNING",
  "currentStatus": "CANCELLED",
  "triggeredBy": "SYSTEM_AUTO",
  "reasonCode": "SIGNING_TIMEOUT"
}

Escrow 이벤트

에스크로 이벤트는 계약의 부속 상태이므로 Contract를 aggregateType으로 사용한다.

aggregateType: Contract


escrow.deposit_initiated

에스크로 결제 요청이 PG사로 발송될 때 발행된다.

항목
eventName escrow.deposit_initiated
aggregateType Contract
piiLevel NONE
triggeredBy SYSTEM_AUTO

payloadJson:

{
  "aggregateId": "contract_abc123",
  "contractId": "contract_abc123",
  "escrowTransactionId": "etx_xyz",
  "amount": 5000000,
  "currencyCode": "KRW",
  "providerCode": "TOSSPAYMENTS",
  "idempotencyKey": "etx_abc123_deposit_01"
}
필드 타입 설명
amount number 결제 요청 금액 (KRW, 정수)
providerCode string PG사 코드
idempotencyKey string 중복 방지 키

escrow.holding

PG 결제 성공 웹훅이 처리되어 에스크로가 HOLDING 상태로 전환될 때 발행된다.

항목
eventName escrow.holding
aggregateType Contract
piiLevel NONE
triggeredBy WEBHOOK

payloadJson:

{
  "aggregateId": "contract_abc123",
  "contractId": "contract_abc123",
  "escrowTransactionId": "etx_xyz",
  "previousEscrowStatus": "DEPOSIT_PENDING",
  "currentEscrowStatus": "HOLDING",
  "amount": 5000000,
  "currencyCode": "KRW",
  "providerTransactionId": "pg_txn_999"
}

escrow.released

운영자 정산 승인 후 에스크로가 해제될 때 발행된다.

항목
eventName escrow.released
aggregateType Contract
piiLevel NONE
triggeredBy OPERATOR_ACTION

payloadJson:

{
  "aggregateId": "contract_abc123",
  "contractId": "contract_abc123",
  "escrowTransactionId": "etx_xyz",
  "previousEscrowStatus": "RELEASE_REVIEW",
  "currentEscrowStatus": "RELEASED",
  "settledAmount": 4850000,
  "platformFeeAmount": 150000,
  "currencyCode": "KRW",
  "approvedByOperatorId": "user_fin01"
}
필드 타입 설명
settledAmount number 업체에 지급되는 정산 금액 (KRW)
platformFeeAmount number 플랫폼 수수료 금액 (KRW)

escrow.refunded

환불이 실행될 때 발행된다.

항목
eventName escrow.refunded
aggregateType Contract
piiLevel NONE
triggeredBy OPERATOR_ACTION

payloadJson:

{
  "aggregateId": "contract_abc123",
  "contractId": "contract_abc123",
  "escrowTransactionId": "etx_xyz",
  "previousEscrowStatus": "DISPUTED",
  "currentEscrowStatus": "REFUNDED",
  "refundType": "FULL_REFUND",
  "refundAmount": 5000000,
  "currencyCode": "KRW",
  "reasonCode": "DISPUTE_RESOLVED",
  "approvedByOperatorId": "user_fin01"
}
필드 타입 설명
refundType string FULL_REFUND / PARTIAL_REFUND
refundAmount number 환불 금액 (KRW)

Inspection 이벤트

aggregateType: Contract


inspection.submitted

검수 사진이 업로드되어 검수 요청이 완료될 때 발행된다.

항목
eventName inspection.submitted
aggregateType Contract
piiLevel NONE
triggeredBy USER_ACTION

payloadJson:

{
  "aggregateId": "contract_abc123",
  "contractId": "contract_abc123",
  "inspectionRecordId": "insp_xyz",
  "inspectionType": "DEMOLITION_COMPLETION",
  "previousStatus": "REQUESTED",
  "currentStatus": "SUBMITTED",
  "photoCount": 6,
  "submittedByUserId": "user_vendor01"
}

inspection.approved / inspection.rejected

검수가 승인 또는 거절될 때 발행된다. 1차(폐업자)와 2차(운영자) 승인 모두 이 이벤트를 사용하며 approvalStage로 구분한다.

항목
eventName inspection.approved 또는 inspection.rejected
aggregateType Contract
piiLevel LOW
triggeredBy USER_ACTION 또는 OPERATOR_ACTION

payloadJson (approved):

{
  "aggregateId": "contract_abc123",
  "contractId": "contract_abc123",
  "inspectionRecordId": "insp_xyz",
  "previousStatus": "REVIEWING",
  "currentStatus": "APPROVED",
  "approvalStage": "OPERATOR_FINAL",
  "reviewedByUserId": "user_op01"
}
필드 타입 설명
approvalStage string OWNER_PRIMARY (폐업자 1차) / OPERATOR_FINAL (운영자 2차)
reviewedByUserId string 검수를 처리한 사용자 또는 운영자 userId

Dispute 이벤트

aggregateType: Contract


dispute.opened

분쟁이 접수될 때 발행된다. 에스크로가 즉시 DISPUTED 상태로 전환된다.

항목
eventName dispute.opened
aggregateType Contract
piiLevel LOW
triggeredBy USER_ACTION 또는 OPERATOR_ACTION

payloadJson:

{
  "aggregateId": "contract_abc123",
  "contractId": "contract_abc123",
  "disputeCaseId": "disp_xyz",
  "openedByUserId": "user_founder01",
  "reasonCode": "QUALITY_ISSUE",
  "previousEscrowStatus": "HOLDING",
  "currentEscrowStatus": "DISPUTED"
}
필드 타입 설명
reasonCode string QUALITY_ISSUE / SCOPE_DIFFERENCE / TIMELINE_BREACH / DAMAGE / INCOMPLETE_WORK / OTHER
previousEscrowStatus string 분쟁 접수 전 에스크로 상태 (HOLDING 또는 RELEASE_REVIEW)
currentEscrowStatus string DISPUTED (고정값)

dispute.resolved

분쟁이 해결될 때 발행된다.

항목
eventName dispute.resolved
aggregateType Contract
piiLevel LOW
triggeredBy OPERATOR_ACTION

payloadJson:

{
  "aggregateId": "contract_abc123",
  "contractId": "contract_abc123",
  "disputeCaseId": "disp_xyz",
  "previousDisputeStatus": "MEDIATING",
  "currentDisputeStatus": "RESOLVED",
  "resolutionCode": "PARTIAL_REFUND",
  "refundAmount": 2000000,
  "settledAmount": 3000000,
  "resolvedByOperatorId": "user_trust01"
}
필드 타입 설명
resolutionCode string FULL_RELEASE / PARTIAL_REFUND / FULL_REFUND / NEGOTIATED
refundAmount number? 환불 금액. FULL_RELEASE인 경우 0 또는 null
settledAmount number? 정산 금액. FULL_REFUND인 경우 0 또는 null

3-7. Backoffice 도메인


operator.assigned

운영자가 케이스(SubsidyCase, MatchRequest, DisputeCase 등)에 배정될 때 발행된다.

항목
eventName operator.assigned
aggregateType 배정 대상 엔티티 타입 (예: SubsidyCase, MatchRequest)
piiLevel LOW
triggeredBy OPERATOR_ACTION 또는 SYSTEM_AUTO

payloadJson:

{
  "aggregateId": "scase_abc123",
  "targetEntityType": "SubsidyCase",
  "targetEntityId": "scase_abc123",
  "assignedOperatorId": "user_op01",
  "assignedByUserId": "user_manager01"
}
필드 타입 설명
targetEntityType string 배정 대상 엔티티 타입
assignedOperatorId string 배정된 운영자 userId
assignedByUserId string? 배정을 실행한 상위 운영자. 시스템 자동 배정이면 null

policy.version_activated

정책 버전이 활성화될 때 발행된다.

항목
eventName policy.version_activated
aggregateType PolicyVersion
piiLevel NONE
triggeredBy OPERATOR_ACTION

payloadJson:

{
  "aggregateId": "polv_xyz",
  "policyVersionId": "polv_xyz",
  "policyType": "SUBSIDY_CHECKLIST",
  "versionCode": "HOPE_RETURN_PKG_V2",
  "previousVersionCode": "HOPE_RETURN_PKG_V1",
  "effectiveFrom": "2026-04-01T00:00:00Z",
  "activatedByOperatorId": "user_admin01"
}

4. Outbox 발행 정책

4-1. Outbox 발행 대상 이벤트

OutboxEventEventLog와 별도로 외부 시스템(알림 서비스, 분석 파이프라인 등)으로의 비동기 발행을 담당한다. 아래에 해당하는 이벤트는 EventLog 기록과 동시에 OutboxEvent로도 저장한다.

이벤트 발행 이유 소비자
store.approved 공개 전환 안내 알림 트리거 알림 서비스
store.rejected 반려 사유 안내 알림 트리거 알림 서비스
store.published 구독자 알림 (Phase 2 준비) 알림 서비스
match.requested 폐업자 매칭 요청 수신 알림 알림 서비스
match.accepted 창업자/업체 매칭 수락 알림 + 계약 생성 트리거 알림 서비스, 계약 서비스
match.rejected 창업자/업체 매칭 거절 알림 알림 서비스
match.expired 만료 알림 알림 서비스
subsidy.case_created 운영자 큐 등록 트리거 운영 콘솔
subsidy.document_reviewed 서류 검토 결과 알림 알림 서비스
subsidy.submitted 운영자 최종 접수 알림 알림 서비스
subsidy.approved 지원금 승인 결과 알림 알림 서비스
subsidy.rejected 지원금 반려 결과 알림 알림 서비스
vendor.certification_approved 업체 인증 승인 알림 + 매칭 참여 자격 활성화 알림 서비스, 매칭 서비스
vendor.certification_rejected 업체 인증 반려 알림 알림 서비스
contract.signed 에스크로 결제 요청 트리거 결제 서비스
contract.completed 완료 알림 + KPI 집계 트리거 알림 서비스, 분석 파이프라인
escrow.holding 에스크로 보관 확인 알림 알림 서비스
escrow.released 정산 완료 알림 알림 서비스
escrow.refunded 환불 완료 알림 알림 서비스
inspection.submitted 검수 요청 수신 알림 알림 서비스
inspection.approved 정산 검토 단계 진입 트리거 에스크로 서비스
dispute.opened 분쟁 접수 운영자 알림 + 에스크로 보류 트리거 알림 서비스, 에스크로 서비스
dispute.resolved 분쟁 해결 알림 + 정산/환불 트리거 알림 서비스, 결제 서비스

발행 대상이 아닌 이벤트(user.profile_updated, store.photo_uploaded, store.deal_status_changed 등)는 EventLog에만 기록하고 OutboxEvent는 생성하지 않는다.

4-2. 트랜잭션 보장

EventLogOutboxEvent는 반드시 동일한 DB 트랜잭션 내에서 도메인 상태 변경과 함께 커밋한다.

BEGIN TRANSACTION
  -- 도메인 상태 변경 (Store, MatchRequest, SubsidyCase 등)
  -- EventLog INSERT
  -- OutboxEvent INSERT (발행 대상인 경우만)
COMMIT

애플리케이션 코드에서 EventLog 커밋 후 별도로 OutboxEvent를 삽입하지 않는다. 트랜잭션 바깥에서 삽입하면 상태 불일치가 발생한다.

4-3. OutboxEvent 필드

필드 설명
aggregateType EventLogaggregateType과 동일
aggregateId EventLogaggregateId와 동일
eventName EventLogeventName과 동일
payloadJson 외부 발행용 payload. HIGH PII 필드는 제거 또는 마스킹 후 저장.
publishStatus PENDINGPUBLISHED / FAILED / DEAD_LETTER
availableAt 최초 처리 가능 시각. 기본값 NOW(). 재시도 시 갱신.
retryCount 재시도 횟수. 0부터 시작.
lastError 마지막 실패 오류 메시지 (최대 2000자).

4-4. 재시도 정책

발행 실패 시 지수 백오프(Exponential Backoff) 방식으로 재시도한다.

시도 대기 시간
1차 1분 후
2차 5분 후
3차 15분 후
4차 1시간 후
5차 4시간 후

최대 재시도 횟수: 5회.

4-5. Dead-Letter 처리

5회 재시도 모두 실패한 경우 아래 처리를 실행한다.

  1. OutboxEvent.publishStatusDEAD_LETTER로 변경한다.
  2. FINANCE_OPERATOR 또는 OPS_MANAGER에게 내부 알림을 발송한다.
  3. 운영자가 수동으로 재발행하거나 해당 이벤트를 수동 처리한다.
  4. 재발행 실행 시 retryCount를 0으로 초기화하고 availableAt을 현재 시각으로 설정한다.
  5. 모든 수동 재발행 이력은 AuditLog에 기록한다.

Dead-Letter 이벤트는 삭제하지 않는다. 원인 분석과 감사 목적으로 영구 보관한다.


5. 향후 확장

5-1. 알림 트리거 이벤트

Phase 2에서 알림 서비스가 고도화되면 이벤트 기반 알림 발송 규칙을 아래 이벤트 중심으로 선언적으로 정의할 예정이다.

이벤트 알림 수신자 발송 채널
store.approved 폐업자 이메일
store.rejected 폐업자 이메일
match.requested 폐업자 카카오 알림톡
match.accepted 창업자/업체 카카오 알림톡
subsidy.document_reviewed (REJECTED) 폐업자 카카오 알림톡 + 이메일
subsidy.approved 폐업자 카카오 알림톡 + 이메일
escrow.holding 계약 당사자 이메일
escrow.released 계약 당사자 이메일 + 카카오 알림톡
dispute.opened 운영자(TRUST_OPERATOR) 내부 알림

향후 AlertSubscription 모델을 도입하면 업체(VENDOR_MANAGER) 대상 B2B 알림 상품도 이 이벤트 스트림을 기반으로 구현한다(D003 섹션 6-3 참조).

5-2. KPI 집계 이벤트

Phase 2에서 KpiSnapshot 모델을 도입하면 아래 이벤트를 집계 파이프라인의 입력으로 활용한다.

KPI 기반 이벤트
매장 등록 → 공개 전환율 store.submittedstore.published
매칭 요청 → 수락 전환율 match.requestedmatch.accepted
매칭 → 계약 완료율 match.acceptedcontract.completed
지원금 케이스 완료율 subsidy.case_createdsubsidy.approved
에스크로 정산 소요 시간 escrow.holdingescrow.released
분쟁 발생률 contract.signed 대비 dispute.opened
분쟁 해결 소요 시간 dispute.openeddispute.resolved

집계 이벤트는 별도의 KpiSnapshot 테이블 또는 외부 데이터 웨어하우스에서 처리하며, 원본 EventLog에 직접 집계 쿼리를 수행하지 않는다.

5-3. 이벤트 스키마 레지스트리

이벤트 종류가 증가하면 다음 관리 체계를 도입한다.

  • eventName의 JSON Schema를 별도 파일(docs/analytics/event-schemas/{eventName}.json)로 관리한다.
  • 이벤트 발행 전 payload를 JSON Schema로 검증하는 미들웨어를 적용한다.
  • 버전별 스키마 이력을 보존하여 구버전 소비자와의 호환성을 유지한다.

부록

A. 이벤트 이름 전체 목록

이벤트 이름 aggregateType piiLevel OutboxEvent
user.registered User LOW -
user.verified User LOW -
user.profile_updated User LOW -
user.consent_granted User LOW -
user.consent_revoked User LOW -
user.suspended User LOW -
user.reactivated User LOW -
store.draft_created Store LOW -
store.submitted Store LOW -
store.approved Store LOW O
store.rejected Store LOW O
store.published Store LOW O
store.unpublished Store LOW -
store.deal_status_changed Store LOW -
store.photo_uploaded Store NONE -
store.photo_removed Store NONE -
match.requested MatchRequest LOW O
match.accepted MatchRequest LOW O
match.rejected MatchRequest LOW O
match.expired MatchRequest LOW O
match.cancelled MatchRequest LOW -
match.completed MatchRequest LOW -
subsidy.case_created SubsidyCase LOW O
subsidy.eligibility_checked SubsidyCase LOW -
subsidy.document_uploaded SubsidyCase LOW -
subsidy.document_reviewed SubsidyCase LOW O
subsidy.submitted SubsidyCase LOW O
subsidy.approved SubsidyCase LOW O
subsidy.rejected SubsidyCase LOW O
vendor.registered Vendor LOW -
vendor.certification_applied Vendor LOW -
vendor.certification_approved Vendor LOW O
vendor.certification_rejected Vendor LOW O
contract.created Contract LOW -
contract.signed Contract LOW O
contract.completed Contract LOW O
contract.cancelled Contract LOW -
escrow.deposit_initiated Contract NONE -
escrow.holding Contract NONE O
escrow.released Contract NONE O
escrow.refunded Contract NONE O
inspection.submitted Contract NONE O
inspection.approved Contract LOW O
inspection.rejected Contract LOW -
dispute.opened Contract LOW O
dispute.resolved Contract LOW O
operator.assigned (대상 엔티티) LOW -
policy.version_activated PolicyVersion NONE -

B. 관련 문서

문서 경로
Prisma 스키마 초안 /docs/database/schema-prisma-draft.md
지원금 대행 정책서 (D001) /docs/policies/subsidy-policy.md
계약-에스크로-분쟁 정책서 (D002) /docs/policies/contract-escrow-policy.md
정보 공개 정책서 (D003) /docs/policies/data-exposure-policy.md

C. 개정 이력

버전 날짜 변경 내용 작성자
1.0.0 2026-03-07 초안 작성 (MVP Phase 1 전 도메인) -