7f59b94dcf
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.
1590 lines
47 KiB
Markdown
1590 lines
47 KiB
Markdown
# 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. [목적 및 설계 원칙](#1-목적-및-설계-원칙)
|
|
2. [공통 이벤트 엔벨로프](#2-공통-이벤트-엔벨로프)
|
|
3. [도메인별 이벤트 목록 및 payload 스키마](#3-도메인별-이벤트-목록-및-payload-스키마)
|
|
4. [Outbox 발행 정책](#4-outbox-발행-정책)
|
|
5. [향후 확장](#5-향후-확장)
|
|
|
|
---
|
|
|
|
## 1. 목적 및 설계 원칙
|
|
|
|
### 1-1. 목적
|
|
|
|
이 문서는 Startover 플랫폼에서 `EventLog` 테이블에 저장되는 도메인 이벤트의 규격을 정의한다.
|
|
|
|
이벤트 로그는 세 가지 목적으로 활용된다.
|
|
|
|
- **감사 추적**: 도메인 객체의 상태 전환을 불변(immutable) append-only 방식으로 보존한다.
|
|
- **비동기 연동**: `OutboxEvent`를 통해 외부 시스템(알림, 정산, 분석)에 이벤트를 발행한다.
|
|
- **KPI 분석**: 매칭 전환율, 지원금 케이스 성공률, 에스크로 정산 속도 등 지표 산출에 활용한다.
|
|
|
|
`EventLog`는 `AuditLog`와 구분된다. `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` 유지. 소비자는 미지 필드를 무시해야 한다. |
|
|
| 필드 타입 변경 또는 필수 필드 제거 | `eventVersion`을 `v2`, `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는 아래 최상위 필드를 포함하도록 권고한다. 도메인별 필드는 추가로 확장한다.
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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):
|
|
|
|
```json
|
|
{
|
|
"aggregateId": "store_abc123",
|
|
"storeId": "store_abc123",
|
|
"previousStatus": "REVIEWING",
|
|
"currentStatus": "APPROVED",
|
|
"operatorId": "user_op01",
|
|
"approvedAt": "2026-03-07T14:00:00Z"
|
|
}
|
|
```
|
|
|
|
payloadJson (rejected):
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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`
|
|
|
|
자격 판별이 실행되어 결과가 결정될 때 발행된다. `SubsidyCase`가 `ELIGIBILITY_CHECKED` 상태로 전환될 때 발행한다.
|
|
|
|
| 항목 | 값 |
|
|
|------|----|
|
|
| `eventName` | `subsidy.eligibility_checked` |
|
|
| `aggregateType` | `SubsidyCase` |
|
|
| `piiLevel` | `LOW` |
|
|
| `triggeredBy` | `SYSTEM_AUTO` |
|
|
|
|
payloadJson:
|
|
|
|
```json
|
|
{
|
|
"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):
|
|
|
|
```json
|
|
{
|
|
"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):
|
|
|
|
```json
|
|
{
|
|
"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):
|
|
|
|
```json
|
|
{
|
|
"aggregateId": "scase_abc123",
|
|
"subsidyCaseId": "scase_abc123",
|
|
"storeId": "store_xyz",
|
|
"programCode": "HOPE_RETURN_PKG",
|
|
"previousStatus": "READY_TO_SUBMIT",
|
|
"currentStatus": "SUBMITTED"
|
|
}
|
|
```
|
|
|
|
payloadJson (approved):
|
|
|
|
```json
|
|
{
|
|
"aggregateId": "scase_abc123",
|
|
"subsidyCaseId": "scase_abc123",
|
|
"storeId": "store_xyz",
|
|
"programCode": "HOPE_RETURN_PKG",
|
|
"previousStatus": "SUBMITTED",
|
|
"currentStatus": "APPROVED",
|
|
"operatorId": "user_op01"
|
|
}
|
|
```
|
|
|
|
payloadJson (rejected):
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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):
|
|
|
|
```json
|
|
{
|
|
"aggregateId": "vendor_abc123",
|
|
"vendorId": "vendor_abc123",
|
|
"certificationId": "vcert_xyz",
|
|
"requestedScopeCode": "DEMOLITION_STANDARD",
|
|
"previousCertificationStatus": null,
|
|
"currentCertificationStatus": "APPLIED"
|
|
}
|
|
```
|
|
|
|
payloadJson (certification_approved):
|
|
|
|
```json
|
|
{
|
|
"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):
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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):
|
|
|
|
```json
|
|
{
|
|
"aggregateId": "contract_abc123",
|
|
"contractId": "contract_abc123",
|
|
"storeId": "store_xyz",
|
|
"contractType": "ACQUISITION",
|
|
"previousStatus": "ACTIVE",
|
|
"currentStatus": "COMPLETED",
|
|
"triggeredBy": "SYSTEM_AUTO",
|
|
"relatedEscrowTransactionId": "etx_xyz"
|
|
}
|
|
```
|
|
|
|
payloadJson (cancelled):
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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):
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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 발행 대상 이벤트
|
|
|
|
`OutboxEvent`는 `EventLog`와 별도로 외부 시스템(알림 서비스, 분석 파이프라인 등)으로의 비동기 발행을 담당한다. 아래에 해당하는 이벤트는 `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. 트랜잭션 보장
|
|
|
|
`EventLog`와 `OutboxEvent`는 반드시 동일한 DB 트랜잭션 내에서 도메인 상태 변경과 함께 커밋한다.
|
|
|
|
```
|
|
BEGIN TRANSACTION
|
|
-- 도메인 상태 변경 (Store, MatchRequest, SubsidyCase 등)
|
|
-- EventLog INSERT
|
|
-- OutboxEvent INSERT (발행 대상인 경우만)
|
|
COMMIT
|
|
```
|
|
|
|
애플리케이션 코드에서 `EventLog` 커밋 후 별도로 `OutboxEvent`를 삽입하지 않는다. 트랜잭션 바깥에서 삽입하면 상태 불일치가 발생한다.
|
|
|
|
### 4-3. OutboxEvent 필드
|
|
|
|
| 필드 | 설명 |
|
|
|------|------|
|
|
| `aggregateType` | `EventLog`의 `aggregateType`과 동일 |
|
|
| `aggregateId` | `EventLog`의 `aggregateId`와 동일 |
|
|
| `eventName` | `EventLog`의 `eventName`과 동일 |
|
|
| `payloadJson` | 외부 발행용 payload. `HIGH` PII 필드는 제거 또는 마스킹 후 저장. |
|
|
| `publishStatus` | `PENDING` → `PUBLISHED` / `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.publishStatus`를 `DEAD_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.submitted` → `store.published` |
|
|
| 매칭 요청 → 수락 전환율 | `match.requested` → `match.accepted` |
|
|
| 매칭 → 계약 완료율 | `match.accepted` → `contract.completed` |
|
|
| 지원금 케이스 완료율 | `subsidy.case_created` → `subsidy.approved` |
|
|
| 에스크로 정산 소요 시간 | `escrow.holding` → `escrow.released` |
|
|
| 분쟁 발생률 | `contract.signed` 대비 `dispute.opened` |
|
|
| 분쟁 해결 소요 시간 | `dispute.opened` → `dispute.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 전 도메인) | - |
|