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.
1198 lines
47 KiB
Plaintext
1198 lines
47 KiB
Plaintext
// =============================================================================
|
|
// Startover MVP - Prisma Schema
|
|
// =============================================================================
|
|
// 설계 원칙:
|
|
// - 내부 PK: BigInt @id @default(autoincrement())
|
|
// - 외부 노출 ID: publicId String @unique @default(cuid())
|
|
// - 금액: Decimal @db.Decimal(14, 2)
|
|
// - 시각: DateTime @db.Timestamptz(6)
|
|
// - Prisma 모델명: PascalCase / 필드명: camelCase
|
|
// - DB 테이블/컬럼: snake_case (@@map / @map)
|
|
// - append-only 모델: updatedAt 없이 createdAt만
|
|
// - soft delete 필요 모델만 deletedAt
|
|
//
|
|
// Prisma 미지원 제약 (custom SQL migration에서 추가):
|
|
// 1. partial unique index (열린 매칭 요청 중복 방지 등)
|
|
// 2. CHECK constraints (amount >= 0, exclusiveAreaSqm > 0 등)
|
|
// 3. PostGIS GIST index (향후 locationPoint)
|
|
// =============================================================================
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
previewFeatures = ["postgresqlExtensions"]
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
extensions = []
|
|
}
|
|
|
|
// =============================================================================
|
|
// Enums
|
|
// =============================================================================
|
|
|
|
enum UserRole {
|
|
CLOSING_OWNER
|
|
FOUNDER
|
|
VENDOR_MANAGER
|
|
OPS_MANAGER
|
|
SUBSIDY_OPERATOR
|
|
TRUST_OPERATOR
|
|
FINANCE_OPERATOR
|
|
SUPER_ADMIN
|
|
|
|
@@map("user_role")
|
|
}
|
|
|
|
enum UserStatus {
|
|
PENDING_VERIFICATION
|
|
ACTIVE
|
|
SUSPENDED
|
|
DEACTIVATED
|
|
|
|
@@map("user_status")
|
|
}
|
|
|
|
enum ProfileType {
|
|
CLOSING_OWNER
|
|
FOUNDER
|
|
VENDOR_MANAGER
|
|
OPERATOR
|
|
|
|
@@map("profile_type")
|
|
}
|
|
|
|
enum ConsentType {
|
|
TERMS_OF_SERVICE
|
|
PRIVACY_POLICY_REQUIRED
|
|
PRIVACY_POLICY_MARKETING
|
|
STORE_PUBLICATION_CONSENT
|
|
MATCHED_INFO_DISCLOSURE
|
|
THIRD_PARTY_MATCHED_PARTY
|
|
NOTIFICATION_KAKAO
|
|
|
|
@@map("consent_type")
|
|
}
|
|
|
|
enum StoreReviewStatus {
|
|
DRAFT
|
|
SUBMITTED
|
|
REVIEWING
|
|
APPROVED
|
|
REJECTED
|
|
|
|
@@map("store_review_status")
|
|
}
|
|
|
|
enum StorePublicationStatus {
|
|
PRIVATE
|
|
RESTRICTED
|
|
PUBLISHED
|
|
UNPUBLISHED
|
|
|
|
@@map("store_publication_status")
|
|
}
|
|
|
|
enum StoreDealStatus {
|
|
OPEN
|
|
MATCHING
|
|
RESERVED
|
|
CONTRACTED
|
|
CLOSED
|
|
CANCELLED
|
|
|
|
@@map("store_deal_status")
|
|
}
|
|
|
|
enum PhotoCategory {
|
|
EXTERIOR
|
|
INTERIOR
|
|
KITCHEN
|
|
EQUIPMENT
|
|
FLOOR_PLAN
|
|
DOCUMENT
|
|
|
|
@@map("photo_category")
|
|
}
|
|
|
|
enum VisibilityScope {
|
|
INTERNAL
|
|
MATCHED_ONLY
|
|
PUBLIC_SUMMARY
|
|
|
|
@@map("visibility_scope")
|
|
}
|
|
|
|
enum MatchType {
|
|
ACQUISITION
|
|
DEMOLITION
|
|
INTERIOR
|
|
|
|
@@map("match_type")
|
|
}
|
|
|
|
enum MatchRequestStatus {
|
|
OPEN
|
|
REVIEWING
|
|
ACCEPTED
|
|
REJECTED
|
|
CONTRACTING
|
|
EXPIRED
|
|
CANCELLED
|
|
COMPLETED
|
|
|
|
@@map("match_request_status")
|
|
}
|
|
|
|
enum MatchSourceType {
|
|
USER_REQUEST
|
|
OPERATOR_RECOMMENDATION
|
|
SYSTEM_MATCH
|
|
|
|
@@map("match_source_type")
|
|
}
|
|
|
|
enum SubsidyCaseStatus {
|
|
DRAFT
|
|
ELIGIBILITY_CHECKED
|
|
DOCUMENTS_PENDING
|
|
REVIEWING
|
|
READY_TO_SUBMIT
|
|
SUBMITTED
|
|
APPROVED
|
|
REJECTED
|
|
CLOSED
|
|
|
|
@@map("subsidy_case_status")
|
|
}
|
|
|
|
enum EligibilityResult {
|
|
UNKNOWN
|
|
ELIGIBLE
|
|
CONDITIONALLY_ELIGIBLE
|
|
NOT_ELIGIBLE
|
|
|
|
@@map("eligibility_result")
|
|
}
|
|
|
|
enum ChecklistItemStatus {
|
|
PENDING
|
|
CHECKED
|
|
NOT_APPLICABLE
|
|
|
|
@@map("checklist_item_status")
|
|
}
|
|
|
|
enum DocumentReviewStatus {
|
|
PENDING
|
|
APPROVED
|
|
REJECTED
|
|
RESUBMIT_REQUIRED
|
|
|
|
@@map("document_review_status")
|
|
}
|
|
|
|
enum VendorType {
|
|
DEMOLITION
|
|
INTERIOR
|
|
BOTH
|
|
|
|
@@map("vendor_type")
|
|
}
|
|
|
|
enum VendorCertificationStatus {
|
|
APPLIED
|
|
REVIEWING
|
|
APPROVED
|
|
REJECTED
|
|
SUSPENDED
|
|
EXPIRED
|
|
|
|
@@map("vendor_certification_status")
|
|
}
|
|
|
|
enum ContractType {
|
|
ACQUISITION
|
|
DEMOLITION
|
|
INTERIOR
|
|
|
|
@@map("contract_type")
|
|
}
|
|
|
|
enum ContractStatus {
|
|
DRAFT
|
|
GENERATED
|
|
SIGNING
|
|
SIGNED
|
|
ACTIVE
|
|
COMPLETED
|
|
CANCELLED
|
|
TERMINATED
|
|
|
|
@@map("contract_status")
|
|
}
|
|
|
|
enum EscrowStatus {
|
|
NOT_STARTED
|
|
DEPOSIT_PENDING
|
|
HOLDING
|
|
RELEASE_REVIEW
|
|
RELEASED
|
|
REFUNDED
|
|
DISPUTED
|
|
|
|
@@map("escrow_status")
|
|
}
|
|
|
|
enum EscrowTransactionType {
|
|
DEPOSIT
|
|
RELEASE
|
|
REFUND
|
|
ADJUSTMENT
|
|
HOLD
|
|
|
|
@@map("escrow_transaction_type")
|
|
}
|
|
|
|
enum EscrowTransactionStatus {
|
|
PENDING
|
|
PROCESSING
|
|
COMPLETED
|
|
FAILED
|
|
CANCELLED
|
|
|
|
@@map("escrow_transaction_status")
|
|
}
|
|
|
|
enum SignerRole {
|
|
STORE_OWNER
|
|
BUYER
|
|
VENDOR
|
|
OPERATOR
|
|
WITNESS
|
|
|
|
@@map("signer_role")
|
|
}
|
|
|
|
enum EvidenceType {
|
|
ELECTRONIC_SIGNATURE
|
|
SMS_OTP
|
|
APP_CONSENT
|
|
MANUAL_UPLOAD
|
|
|
|
@@map("evidence_type")
|
|
}
|
|
|
|
enum InspectionType {
|
|
PRE_CONTRACT
|
|
MID_WORK
|
|
FINAL_COMPLETION
|
|
|
|
@@map("inspection_type")
|
|
}
|
|
|
|
enum InspectionStatus {
|
|
REQUESTED
|
|
SUBMITTED
|
|
REVIEWING
|
|
APPROVED
|
|
REJECTED
|
|
|
|
@@map("inspection_status")
|
|
}
|
|
|
|
enum DisputeStatus {
|
|
OPEN
|
|
INVESTIGATING
|
|
MEDIATING
|
|
RESOLVED
|
|
CLOSED
|
|
|
|
@@map("dispute_status")
|
|
}
|
|
|
|
enum PolicyType {
|
|
PRIVACY_POLICY
|
|
TERMS_OF_SERVICE
|
|
STORE_LISTING_POLICY
|
|
CONTRACT_TEMPLATE
|
|
SUBSIDY_CHECKLIST
|
|
|
|
@@map("policy_type")
|
|
}
|
|
|
|
enum PublishStatus {
|
|
PENDING
|
|
PUBLISHED
|
|
FAILED
|
|
|
|
@@map("publish_status")
|
|
}
|
|
|
|
enum RegionType {
|
|
COUNTRY
|
|
SIDO
|
|
SIGUNGU
|
|
ADMIN_DONG
|
|
COMMERCIAL_AREA
|
|
BETA_CLUSTER
|
|
|
|
@@map("region_type")
|
|
}
|
|
|
|
enum UrgencyLevel {
|
|
LOW
|
|
MEDIUM
|
|
HIGH
|
|
CRITICAL
|
|
|
|
@@map("urgency_level")
|
|
}
|
|
|
|
enum RestroomType {
|
|
PRIVATE
|
|
SHARED
|
|
NONE
|
|
|
|
@@map("restroom_type")
|
|
}
|
|
|
|
enum PiiLevel {
|
|
NONE
|
|
LOW
|
|
MEDIUM
|
|
HIGH
|
|
|
|
@@map("pii_level")
|
|
}
|
|
|
|
// =============================================================================
|
|
// 1. 사용자/권한
|
|
// =============================================================================
|
|
|
|
/// 사용자 기본 계정과 로그인 주체
|
|
model User {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
publicId String @unique @default(cuid()) @map("public_id")
|
|
email String @map("email")
|
|
emailNormalized String @unique @map("email_normalized")
|
|
phone String? @map("phone")
|
|
phoneNormalized String? @map("phone_normalized")
|
|
name String @map("name")
|
|
passwordHash String? @map("password_hash")
|
|
image String? @map("image")
|
|
primaryRole UserRole @map("primary_role")
|
|
status UserStatus @default(PENDING_VERIFICATION) @map("status")
|
|
emailVerifiedAt DateTime? @map("email_verified_at") @db.Timestamptz(6)
|
|
lastLoginAt DateTime? @map("last_login_at") @db.Timestamptz(6)
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
profiles UserProfile[]
|
|
consents UserConsent[]
|
|
stores Store[]
|
|
vendors Vendor[]
|
|
matchRequestsAsRequester MatchRequest[] @relation("MatchRequesterUser")
|
|
matchRequestsAsOperator MatchRequest[] @relation("MatchOperatorAssignee")
|
|
subsidyCasesAsApplicant SubsidyCase[] @relation("SubsidyApplicantUser")
|
|
subsidyCasesAsOperator SubsidyCase[] @relation("SubsidyOperatorAssignee")
|
|
subsidyChecklistCheckedBy SubsidyChecklistItem[] @relation("ChecklistCheckedByUser")
|
|
subsidyDocsUploadedBy SubsidyDocument[] @relation("SubsidyDocUploadedByUser")
|
|
subsidyDocsReviewedBy SubsidyDocument[] @relation("SubsidyDocReviewedByUser")
|
|
storePhotosUploadedBy StorePhoto[]
|
|
contractsCreatedBy Contract[] @relation("ContractCreatedByUser")
|
|
contractsAsStoreOwner Contract[] @relation("ContractStoreOwnerUser")
|
|
contractsAsBuyer Contract[] @relation("ContractBuyerUser")
|
|
contractVersionsCreatedBy ContractVersion[]
|
|
signaturesAsSigner SignatureEvidence[]
|
|
escrowRequested EscrowTransaction[] @relation("EscrowRequestedByUser")
|
|
escrowApproved EscrowTransaction[] @relation("EscrowApprovedByUser")
|
|
inspectionsSubmittedBy InspectionRecord[] @relation("InspectionSubmittedByUser")
|
|
inspectionsReviewedBy InspectionRecord[] @relation("InspectionReviewedByUser")
|
|
disputesOpenedBy DisputeCase[] @relation("DisputeOpenedByUser")
|
|
disputesAssignedOperator DisputeCase[] @relation("DisputeAssignedOperator")
|
|
disputesResolvedBy DisputeCase[] @relation("DisputeResolvedByUser")
|
|
certificationReviewedBy VendorCertification[] @relation("CertificationReviewedByUser")
|
|
auditLogs AuditLog[]
|
|
accounts Account[]
|
|
invitesCreated InviteToken[] @relation("InviteCreator")
|
|
|
|
@@index([primaryRole, status], map: "idx_user_role_status")
|
|
@@map("users")
|
|
}
|
|
|
|
/// 역할별 추가 프로필 정보 확장 테이블
|
|
model UserProfile {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
userId BigInt @map("user_id")
|
|
profileType ProfileType @map("profile_type")
|
|
businessRegistrationNumber String? @map("business_registration_number")
|
|
metadataJson Json? @map("metadata_json")
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
user User @relation(fields: [userId], references: [id])
|
|
|
|
@@index([userId], map: "idx_user_profile_user_id")
|
|
@@map("user_profiles")
|
|
}
|
|
|
|
/// 개인정보/마케팅/제3자 제공 동의를 버전 단위로 저장
|
|
model UserConsent {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
userId BigInt @map("user_id")
|
|
consentType ConsentType @map("consent_type")
|
|
policyVersionId BigInt @map("policy_version_id")
|
|
isGranted Boolean @map("is_granted")
|
|
grantedAt DateTime @map("granted_at") @db.Timestamptz(6)
|
|
revokedAt DateTime? @map("revoked_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
user User @relation(fields: [userId], references: [id])
|
|
policyVersion PolicyVersion @relation(fields: [policyVersionId], references: [id])
|
|
|
|
@@index([userId, consentType], map: "idx_user_consent_user_type")
|
|
@@map("user_consents")
|
|
}
|
|
|
|
/// Auth.js 소셜 로그인 계정 연동
|
|
model Account {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
userId BigInt @map("user_id")
|
|
type String @map("type")
|
|
provider String @map("provider")
|
|
providerAccountId String @map("provider_account_id")
|
|
refresh_token String? @map("refresh_token") @db.Text
|
|
access_token String? @map("access_token") @db.Text
|
|
expires_at Int? @map("expires_at")
|
|
token_type String? @map("token_type")
|
|
scope String? @map("scope")
|
|
id_token String? @map("id_token") @db.Text
|
|
session_state String? @map("session_state")
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([provider, providerAccountId])
|
|
@@map("accounts")
|
|
}
|
|
|
|
/// 이메일 인증 토큰
|
|
model VerificationToken {
|
|
identifier String @map("identifier")
|
|
token String @unique @map("token")
|
|
expires DateTime @map("expires") @db.Timestamptz(6)
|
|
|
|
@@id([identifier, token])
|
|
@@map("verification_tokens")
|
|
}
|
|
|
|
/// 운영자 초대 토큰
|
|
model InviteToken {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
email String @map("email")
|
|
role UserRole @map("role")
|
|
token String @unique @default(cuid()) @map("token")
|
|
expires DateTime @map("expires") @db.Timestamptz(6)
|
|
usedAt DateTime? @map("used_at") @db.Timestamptz(6)
|
|
createdBy BigInt @map("created_by")
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
|
|
creator User @relation("InviteCreator", fields: [createdBy], references: [id])
|
|
|
|
@@map("invite_tokens")
|
|
}
|
|
|
|
// =============================================================================
|
|
// 2. 매장 공급 도메인
|
|
// =============================================================================
|
|
|
|
/// 폐업자가 등록한 매장의 중심 aggregate
|
|
model Store {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
publicId String @unique @default(cuid()) @map("public_id")
|
|
ownerUserId BigInt @map("owner_user_id")
|
|
listingTitle String @map("listing_title")
|
|
publicSummary String? @map("public_summary")
|
|
industryLeafId BigInt? @map("industry_leaf_id")
|
|
regionClusterId BigInt? @map("region_cluster_id")
|
|
regionLeafId BigInt? @map("region_leaf_id")
|
|
roadAddress String @map("road_address")
|
|
detailAddress String? @map("detail_address")
|
|
latitude Decimal? @map("latitude") @db.Decimal(10, 7)
|
|
longitude Decimal? @map("longitude") @db.Decimal(10, 7)
|
|
reviewStatus StoreReviewStatus @default(DRAFT) @map("review_status")
|
|
publicationStatus StorePublicationStatus @default(PRIVATE) @map("publication_status")
|
|
dealStatus StoreDealStatus @default(OPEN) @map("deal_status")
|
|
policyVersionId BigInt? @map("policy_version_id")
|
|
publishedAt DateTime? @map("published_at") @db.Timestamptz(6)
|
|
approvedAt DateTime? @map("approved_at") @db.Timestamptz(6)
|
|
rejectedAt DateTime? @map("rejected_at") @db.Timestamptz(6)
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
ownerUser User @relation(fields: [ownerUserId], references: [id])
|
|
industryLeaf IndustryTaxonomy? @relation(fields: [industryLeafId], references: [id])
|
|
regionCluster RegionHierarchy? @relation("StoreRegionCluster", fields: [regionClusterId], references: [id])
|
|
regionLeaf RegionHierarchy? @relation("StoreRegionLeaf", fields: [regionLeafId], references: [id])
|
|
policyVersion PolicyVersion? @relation(fields: [policyVersionId], references: [id])
|
|
|
|
lease StoreLease?
|
|
facility StoreFacility?
|
|
lifecycle StoreLifecycle?
|
|
photos StorePhoto[]
|
|
matchRequests MatchRequest[]
|
|
subsidyCases SubsidyCase[]
|
|
contracts Contract[]
|
|
|
|
@@index([ownerUserId, createdAt], map: "idx_store_owner_created")
|
|
@@index([reviewStatus, createdAt], map: "idx_store_review_created")
|
|
@@index([regionClusterId, industryLeafId, publicationStatus, dealStatus, publishedAt], map: "idx_store_search")
|
|
@@map("stores")
|
|
}
|
|
|
|
/// 임대/권리금 관련 정보 (Store 1:1 확장)
|
|
/// CHECK constraint: amount fields >= 0 (custom SQL migration)
|
|
model StoreLease {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
storeId BigInt @unique @map("store_id")
|
|
depositAmount Decimal? @map("deposit_amount") @db.Decimal(14, 2)
|
|
monthlyRentAmount Decimal? @map("monthly_rent_amount") @db.Decimal(14, 2)
|
|
maintenanceFeeAmount Decimal? @map("maintenance_fee_amount") @db.Decimal(14, 2)
|
|
premiumAmount Decimal? @map("premium_amount") @db.Decimal(14, 2)
|
|
transferable Boolean? @map("transferable")
|
|
leaseExpiresAt DateTime? @map("lease_expires_at") @db.Timestamptz(6)
|
|
remainingLeaseMonths Int? @map("remaining_lease_months")
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
store Store @relation(fields: [storeId], references: [id])
|
|
|
|
@@map("store_leases")
|
|
}
|
|
|
|
/// F&B 필터링과 인수 판단에 필요한 설비 정보 (Store 1:1 확장)
|
|
/// CHECK constraint: exclusiveAreaSqm > 0 (custom SQL migration)
|
|
model StoreFacility {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
storeId BigInt @unique @map("store_id")
|
|
exclusiveAreaSqm Decimal? @map("exclusive_area_sqm") @db.Decimal(8, 2)
|
|
floorLevel Int? @map("floor_level")
|
|
seatCount Int? @map("seat_count")
|
|
hasGas Boolean? @map("has_gas")
|
|
hasDrainage Boolean? @map("has_drainage")
|
|
hasDuct Boolean? @map("has_duct")
|
|
electricCapacityKw Decimal? @map("electric_capacity_kw") @db.Decimal(6, 1)
|
|
kitchenEquipmentSummary String? @map("kitchen_equipment_summary")
|
|
parkingCount Int? @map("parking_count")
|
|
restroomType RestroomType? @map("restroom_type")
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
store Store @relation(fields: [storeId], references: [id])
|
|
|
|
@@map("store_facilities")
|
|
}
|
|
|
|
/// 폐업/인수/철거 일정 정보 (Store 1:1 확장)
|
|
model StoreLifecycle {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
storeId BigInt @unique @map("store_id")
|
|
closingPlannedAt DateTime? @map("closing_planned_at") @db.Timestamptz(6)
|
|
takeoverAvailableAt DateTime? @map("takeover_available_at") @db.Timestamptz(6)
|
|
demolitionPreferredAt DateTime? @map("demolition_preferred_at") @db.Timestamptz(6)
|
|
vacateByAt DateTime? @map("vacate_by_at") @db.Timestamptz(6)
|
|
isCurrentlyOperating Boolean? @map("is_currently_operating")
|
|
urgencyLevel UrgencyLevel? @map("urgency_level")
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
store Store @relation(fields: [storeId], references: [id])
|
|
|
|
@@map("store_lifecycles")
|
|
}
|
|
|
|
/// 매장 사진과 공개 범위 (append-only)
|
|
model StorePhoto {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
storeId BigInt @map("store_id")
|
|
storageKey String @map("storage_key")
|
|
photoCategory PhotoCategory @map("photo_category")
|
|
visibilityScope VisibilityScope @default(INTERNAL) @map("visibility_scope")
|
|
sortOrder Int @default(0) @map("sort_order")
|
|
uploadedByUserId BigInt @map("uploaded_by_user_id")
|
|
checksumSha256 String? @map("checksum_sha256")
|
|
width Int? @map("width")
|
|
height Int? @map("height")
|
|
takenAt DateTime? @map("taken_at") @db.Timestamptz(6)
|
|
isRepresentative Boolean @default(false) @map("is_representative")
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
store Store @relation(fields: [storeId], references: [id])
|
|
uploadedByUser User @relation(fields: [uploadedByUserId], references: [id])
|
|
|
|
@@index([storeId, sortOrder], map: "idx_store_photo_store_sort")
|
|
@@map("store_photos")
|
|
}
|
|
|
|
// =============================================================================
|
|
// 3. 매칭 도메인
|
|
// =============================================================================
|
|
|
|
/// 창업자/업체/운영자 추천 기반 매칭 요청
|
|
/// matchType에 따라 필요한 FK가 달라지므로 raw SQL CHECK 제약 추가 (custom SQL migration)
|
|
/// partial unique index: 열린 상태에서 동일 사용자/동일 매장/동일 요청 타입 중복 금지 (custom SQL migration)
|
|
model MatchRequest {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
publicId String @unique @default(cuid()) @map("public_id")
|
|
storeId BigInt @map("store_id")
|
|
matchType MatchType @map("match_type")
|
|
requesterUserId BigInt? @map("requester_user_id")
|
|
requesterVendorId BigInt? @map("requester_vendor_id")
|
|
sourceType MatchSourceType @map("source_type")
|
|
status MatchRequestStatus @default(OPEN) @map("status")
|
|
operatorAssigneeId BigInt? @map("operator_assignee_id")
|
|
message String? @map("message")
|
|
decisionReasonCode String? @map("decision_reason_code")
|
|
acceptedAt DateTime? @map("accepted_at") @db.Timestamptz(6)
|
|
rejectedAt DateTime? @map("rejected_at") @db.Timestamptz(6)
|
|
closedAt DateTime? @map("closed_at") @db.Timestamptz(6)
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
store Store @relation(fields: [storeId], references: [id])
|
|
requesterUser User? @relation("MatchRequesterUser", fields: [requesterUserId], references: [id])
|
|
requesterVendor Vendor? @relation(fields: [requesterVendorId], references: [id])
|
|
operatorAssignee User? @relation("MatchOperatorAssignee", fields: [operatorAssigneeId], references: [id])
|
|
|
|
contract Contract?
|
|
|
|
@@index([storeId, status, createdAt], map: "idx_match_store_status_created")
|
|
@@index([requesterUserId, status, createdAt], map: "idx_match_requester_status_created")
|
|
@@index([requesterVendorId, status, createdAt], map: "idx_match_vendor_status_created")
|
|
@@map("match_requests")
|
|
}
|
|
|
|
// =============================================================================
|
|
// 4. 지원금 도메인
|
|
// =============================================================================
|
|
|
|
/// 매장 기준 지원금 진행 건
|
|
/// partial unique index: 같은 storeId + programCode 조합의 active case는 1개 (custom SQL migration)
|
|
model SubsidyCase {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
publicId String @unique @default(cuid()) @map("public_id")
|
|
storeId BigInt @map("store_id")
|
|
applicantUserId BigInt @map("applicant_user_id")
|
|
programCode String @map("program_code")
|
|
status SubsidyCaseStatus @default(DRAFT) @map("status")
|
|
eligibilityResult EligibilityResult @default(UNKNOWN) @map("eligibility_result")
|
|
policyVersionId BigInt? @map("policy_version_id")
|
|
operatorAssigneeId BigInt? @map("operator_assignee_id")
|
|
checklistVersionCode String? @map("checklist_version_code")
|
|
submissionReadyAt DateTime? @map("submission_ready_at") @db.Timestamptz(6)
|
|
submittedAt DateTime? @map("submitted_at") @db.Timestamptz(6)
|
|
reviewedAt DateTime? @map("reviewed_at") @db.Timestamptz(6)
|
|
rejectionReasonCode String? @map("rejection_reason_code")
|
|
eligibilitySnapshotJson Json? @map("eligibility_snapshot_json")
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
store Store @relation(fields: [storeId], references: [id])
|
|
applicantUser User @relation("SubsidyApplicantUser", fields: [applicantUserId], references: [id])
|
|
policyVersion PolicyVersion? @relation(fields: [policyVersionId], references: [id])
|
|
operatorAssignee User? @relation("SubsidyOperatorAssignee", fields: [operatorAssigneeId], references: [id])
|
|
|
|
checklistItems SubsidyChecklistItem[]
|
|
documents SubsidyDocument[]
|
|
|
|
@@index([storeId, status, createdAt], map: "idx_subsidy_store_status_created")
|
|
@@index([applicantUserId, status, createdAt], map: "idx_subsidy_applicant_status_created")
|
|
@@index([operatorAssigneeId, status, updatedAt], map: "idx_subsidy_operator_status_updated")
|
|
@@map("subsidy_cases")
|
|
}
|
|
|
|
/// 정책 버전에 따라 생성된 체크리스트 항목
|
|
model SubsidyChecklistItem {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
subsidyCaseId BigInt @map("subsidy_case_id")
|
|
itemCode String @map("item_code")
|
|
isRequired Boolean @default(true) @map("is_required")
|
|
status ChecklistItemStatus @default(PENDING) @map("status")
|
|
checkedAt DateTime? @map("checked_at") @db.Timestamptz(6)
|
|
checkedByUserId BigInt? @map("checked_by_user_id")
|
|
|
|
// Relations
|
|
subsidyCase SubsidyCase @relation(fields: [subsidyCaseId], references: [id])
|
|
checkedByUser User? @relation("ChecklistCheckedByUser", fields: [checkedByUserId], references: [id])
|
|
|
|
@@index([subsidyCaseId], map: "idx_subsidy_checklist_case")
|
|
@@map("subsidy_checklist_items")
|
|
}
|
|
|
|
/// 지원금 첨부 서류와 검토 상태
|
|
model SubsidyDocument {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
subsidyCaseId BigInt @map("subsidy_case_id")
|
|
documentTypeCode String @map("document_type_code")
|
|
storageKey String @map("storage_key")
|
|
reviewStatus DocumentReviewStatus @default(PENDING) @map("review_status")
|
|
uploadedByUserId BigInt @map("uploaded_by_user_id")
|
|
uploadedAt DateTime @default(now()) @map("uploaded_at") @db.Timestamptz(6)
|
|
reviewedAt DateTime? @map("reviewed_at") @db.Timestamptz(6)
|
|
reviewedByUserId BigInt? @map("reviewed_by_user_id")
|
|
|
|
// Relations
|
|
subsidyCase SubsidyCase @relation(fields: [subsidyCaseId], references: [id])
|
|
uploadedByUser User @relation("SubsidyDocUploadedByUser", fields: [uploadedByUserId], references: [id])
|
|
reviewedByUser User? @relation("SubsidyDocReviewedByUser", fields: [reviewedByUserId], references: [id])
|
|
|
|
@@index([subsidyCaseId], map: "idx_subsidy_doc_case")
|
|
@@map("subsidy_documents")
|
|
}
|
|
|
|
// =============================================================================
|
|
// 5. 업체 도메인
|
|
// =============================================================================
|
|
|
|
/// 철거/인테리어 업체의 기본 프로필
|
|
model Vendor {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
publicId String @unique @default(cuid()) @map("public_id")
|
|
ownerUserId BigInt @map("owner_user_id")
|
|
vendorType VendorType @map("vendor_type")
|
|
businessName String @map("business_name")
|
|
contactName String @map("contact_name")
|
|
contactPhoneNormalized String? @map("contact_phone_normalized")
|
|
businessRegistrationNumber String? @map("business_registration_number")
|
|
serviceIntro String? @map("service_intro")
|
|
primaryRegionId BigInt? @map("primary_region_id")
|
|
certificationStatus VendorCertificationStatus @default(APPLIED) @map("certification_status")
|
|
latestCertificationId BigInt? @map("latest_certification_id")
|
|
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
ownerUser User @relation(fields: [ownerUserId], references: [id])
|
|
primaryRegion RegionHierarchy? @relation(fields: [primaryRegionId], references: [id])
|
|
|
|
coverageRegions VendorCoverageRegion[]
|
|
certifications VendorCertification[]
|
|
matchRequests MatchRequest[]
|
|
contracts Contract[]
|
|
|
|
@@index([ownerUserId], map: "idx_vendor_owner")
|
|
@@index([vendorType, certificationStatus, createdAt], map: "idx_vendor_type_cert_created")
|
|
@@map("vendors")
|
|
}
|
|
|
|
/// 업체 서비스 가능 지역 (다대다 조인)
|
|
model VendorCoverageRegion {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
vendorId BigInt @map("vendor_id")
|
|
regionId BigInt @map("region_id")
|
|
isPrimary Boolean @default(false) @map("is_primary")
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
vendor Vendor @relation(fields: [vendorId], references: [id])
|
|
region RegionHierarchy @relation(fields: [regionId], references: [id])
|
|
|
|
@@unique([vendorId, regionId], map: "uq_vendor_coverage_region")
|
|
@@index([regionId, vendorId], map: "idx_vendor_coverage_region_region")
|
|
@@map("vendor_coverage_regions")
|
|
}
|
|
|
|
/// 업체 인증 이력
|
|
model VendorCertification {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
vendorId BigInt @map("vendor_id")
|
|
status VendorCertificationStatus @map("status")
|
|
requestedScopeCode String? @map("requested_scope_code")
|
|
documentChecklistJson Json? @map("document_checklist_json")
|
|
appliedAt DateTime @default(now()) @map("applied_at") @db.Timestamptz(6)
|
|
reviewedAt DateTime? @map("reviewed_at") @db.Timestamptz(6)
|
|
reviewedByUserId BigInt? @map("reviewed_by_user_id")
|
|
reasonCode String? @map("reason_code")
|
|
validUntil DateTime? @map("valid_until") @db.Timestamptz(6)
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
vendor Vendor @relation(fields: [vendorId], references: [id])
|
|
reviewedByUser User? @relation("CertificationReviewedByUser", fields: [reviewedByUserId], references: [id])
|
|
|
|
@@index([vendorId, status], map: "idx_vendor_cert_vendor_status")
|
|
@@map("vendor_certifications")
|
|
}
|
|
|
|
// =============================================================================
|
|
// 6. 신뢰 인프라 도메인
|
|
// =============================================================================
|
|
|
|
/// 매칭에서 생성된 계약의 현재 상태와 참여자
|
|
model Contract {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
publicId String @unique @default(cuid()) @map("public_id")
|
|
matchRequestId BigInt @unique @map("match_request_id")
|
|
storeId BigInt @map("store_id")
|
|
contractType ContractType @map("contract_type")
|
|
status ContractStatus @default(DRAFT) @map("status")
|
|
createdByUserId BigInt @map("created_by_user_id")
|
|
storeOwnerUserId BigInt @map("store_owner_user_id")
|
|
buyerUserId BigInt? @map("buyer_user_id")
|
|
vendorId BigInt? @map("vendor_id")
|
|
policyVersionId BigInt? @map("policy_version_id")
|
|
templateCode String? @map("template_code")
|
|
currentVersionId BigInt? @map("current_version_id")
|
|
escrowStatus EscrowStatus @default(NOT_STARTED) @map("escrow_status")
|
|
signedAt DateTime? @map("signed_at") @db.Timestamptz(6)
|
|
effectiveAt DateTime? @map("effective_at") @db.Timestamptz(6)
|
|
completedAt DateTime? @map("completed_at") @db.Timestamptz(6)
|
|
cancelledAt DateTime? @map("cancelled_at") @db.Timestamptz(6)
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
matchRequest MatchRequest @relation(fields: [matchRequestId], references: [id])
|
|
store Store @relation(fields: [storeId], references: [id])
|
|
createdByUser User @relation("ContractCreatedByUser", fields: [createdByUserId], references: [id])
|
|
storeOwnerUser User @relation("ContractStoreOwnerUser", fields: [storeOwnerUserId], references: [id])
|
|
buyerUser User? @relation("ContractBuyerUser", fields: [buyerUserId], references: [id])
|
|
vendor Vendor? @relation(fields: [vendorId], references: [id])
|
|
policyVersion PolicyVersion? @relation(fields: [policyVersionId], references: [id])
|
|
|
|
versions ContractVersion[]
|
|
signatures SignatureEvidence[]
|
|
escrowTransactions EscrowTransaction[]
|
|
inspections InspectionRecord[]
|
|
disputes DisputeCase[]
|
|
|
|
@@index([storeId, status, createdAt], map: "idx_contract_store_status_created")
|
|
@@index([vendorId, status, createdAt], map: "idx_contract_vendor_status_created")
|
|
@@index([buyerUserId, status, createdAt], map: "idx_contract_buyer_status_created")
|
|
@@map("contracts")
|
|
}
|
|
|
|
/// 계약 문서 불변 버전 (append-only)
|
|
model ContractVersion {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
contractId BigInt @map("contract_id")
|
|
versionNo Int @map("version_no")
|
|
templateCode String? @map("template_code")
|
|
templateVersion String? @map("template_version")
|
|
documentStorageKey String? @map("document_storage_key")
|
|
documentSha256 String? @map("document_sha256")
|
|
renderedSnapshotJson Json? @map("rendered_snapshot_json")
|
|
changeSummary String? @map("change_summary")
|
|
createdByUserId BigInt @map("created_by_user_id")
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
contract Contract @relation(fields: [contractId], references: [id])
|
|
createdByUser User @relation(fields: [createdByUserId], references: [id])
|
|
signatures SignatureEvidence[]
|
|
|
|
@@unique([contractId, versionNo], map: "uq_contract_version")
|
|
@@map("contract_versions")
|
|
}
|
|
|
|
/// 전자서명 또는 동의 증적 (append-only)
|
|
model SignatureEvidence {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
contractId BigInt @map("contract_id")
|
|
contractVersionId BigInt @map("contract_version_id")
|
|
signerRole SignerRole @map("signer_role")
|
|
signerUserId BigInt @map("signer_user_id")
|
|
evidenceType EvidenceType @map("evidence_type")
|
|
providerCode String? @map("provider_code")
|
|
providerEventId String? @unique @map("provider_event_id")
|
|
signedAt DateTime @map("signed_at") @db.Timestamptz(6)
|
|
signatureHash String? @map("signature_hash")
|
|
payloadJson Json? @map("payload_json")
|
|
ipAddress String? @map("ip_address")
|
|
userAgent String? @map("user_agent")
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
contract Contract @relation(fields: [contractId], references: [id])
|
|
contractVersion ContractVersion @relation(fields: [contractVersionId], references: [id])
|
|
signerUser User @relation(fields: [signerUserId], references: [id])
|
|
|
|
@@index([contractVersionId, signedAt], map: "idx_signature_version_signed")
|
|
@@map("signature_evidences")
|
|
}
|
|
|
|
/// 결제/환불/정산/보류 관련 금전 이벤트 immutable ledger (append-only)
|
|
/// CHECK constraint: amount >= 0 (custom SQL migration)
|
|
model EscrowTransaction {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
contractId BigInt @map("contract_id")
|
|
transactionType EscrowTransactionType @map("transaction_type")
|
|
providerCode String? @map("provider_code")
|
|
providerTransactionId String? @unique @map("provider_transaction_id")
|
|
status EscrowTransactionStatus @default(PENDING) @map("status")
|
|
amount Decimal @map("amount") @db.Decimal(14, 2)
|
|
currencyCode String @default("KRW") @map("currency_code")
|
|
requestedByUserId BigInt? @map("requested_by_user_id")
|
|
approvedByUserId BigInt? @map("approved_by_user_id")
|
|
idempotencyKey String @unique @map("idempotency_key")
|
|
rawPayloadJson Json? @map("raw_payload_json")
|
|
occurredAt DateTime @map("occurred_at") @db.Timestamptz(6)
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
contract Contract @relation(fields: [contractId], references: [id])
|
|
requestedByUser User? @relation("EscrowRequestedByUser", fields: [requestedByUserId], references: [id])
|
|
approvedByUser User? @relation("EscrowApprovedByUser", fields: [approvedByUserId], references: [id])
|
|
|
|
@@index([contractId, occurredAt], map: "idx_escrow_contract_occurred")
|
|
@@index([status, occurredAt], map: "idx_escrow_status_occurred")
|
|
@@map("escrow_transactions")
|
|
}
|
|
|
|
/// 검수 요청과 검토 결과
|
|
model InspectionRecord {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
contractId BigInt @map("contract_id")
|
|
inspectionType InspectionType @map("inspection_type")
|
|
status InspectionStatus @default(REQUESTED) @map("status")
|
|
submittedByUserId BigInt? @map("submitted_by_user_id")
|
|
submittedAt DateTime? @map("submitted_at") @db.Timestamptz(6)
|
|
reviewedByUserId BigInt? @map("reviewed_by_user_id")
|
|
reviewedAt DateTime? @map("reviewed_at") @db.Timestamptz(6)
|
|
reviewMemo String? @map("review_memo")
|
|
evidencePayloadJson Json? @map("evidence_payload_json")
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
contract Contract @relation(fields: [contractId], references: [id])
|
|
submittedByUser User? @relation("InspectionSubmittedByUser", fields: [submittedByUserId], references: [id])
|
|
reviewedByUser User? @relation("InspectionReviewedByUser", fields: [reviewedByUserId], references: [id])
|
|
|
|
@@index([contractId, status], map: "idx_inspection_contract_status")
|
|
@@map("inspection_records")
|
|
}
|
|
|
|
/// 분쟁 접수, 조사, 중재, 종료 상태
|
|
/// partial unique index: 계약당 active dispute는 1건 (custom SQL migration)
|
|
model DisputeCase {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
contractId BigInt @map("contract_id")
|
|
openedByUserId BigInt @map("opened_by_user_id")
|
|
assignedOperatorId BigInt? @map("assigned_operator_id")
|
|
status DisputeStatus @default(OPEN) @map("status")
|
|
reasonCode String @map("reason_code")
|
|
description String? @map("description")
|
|
resolutionCode String? @map("resolution_code")
|
|
resolutionSummary String? @map("resolution_summary")
|
|
evidencePayloadJson Json? @map("evidence_payload_json")
|
|
openedAt DateTime @default(now()) @map("opened_at") @db.Timestamptz(6)
|
|
resolvedAt DateTime? @map("resolved_at") @db.Timestamptz(6)
|
|
resolvedByUserId BigInt? @map("resolved_by_user_id")
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
contract Contract @relation(fields: [contractId], references: [id])
|
|
openedByUser User @relation("DisputeOpenedByUser", fields: [openedByUserId], references: [id])
|
|
assignedOperator User? @relation("DisputeAssignedOperator", fields: [assignedOperatorId], references: [id])
|
|
resolvedByUser User? @relation("DisputeResolvedByUser", fields: [resolvedByUserId], references: [id])
|
|
|
|
@@index([contractId, status, openedAt], map: "idx_dispute_contract_status_opened")
|
|
@@index([assignedOperatorId, status, openedAt], map: "idx_dispute_operator_status_opened")
|
|
@@map("dispute_cases")
|
|
}
|
|
|
|
// =============================================================================
|
|
// 7. 운영/분석 도메인
|
|
// =============================================================================
|
|
|
|
/// 정책 버전을 모델에 귀속시키기 위한 기준 테이블
|
|
model PolicyVersion {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
policyType PolicyType @map("policy_type")
|
|
versionCode String @map("version_code")
|
|
contentHash String @map("content_hash")
|
|
effectiveFrom DateTime @map("effective_from") @db.Timestamptz(6)
|
|
isActive Boolean @default(true) @map("is_active")
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
stores Store[]
|
|
contracts Contract[]
|
|
subsidyCases SubsidyCase[]
|
|
userConsents UserConsent[]
|
|
|
|
@@index([policyType, isActive], map: "idx_policy_type_active")
|
|
@@map("policy_versions")
|
|
}
|
|
|
|
/// 누가 무엇을 왜 바꿨는지를 운영 감사 기준으로 남긴다 (append-only)
|
|
model AuditLog {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
resourceType String @map("resource_type")
|
|
resourceId String @map("resource_id")
|
|
actionType String @map("action_type")
|
|
actorUserId BigInt? @map("actor_user_id")
|
|
actorRole String? @map("actor_role")
|
|
reasonCode String? @map("reason_code")
|
|
memo String? @map("memo")
|
|
beforeJson Json? @map("before_json")
|
|
afterJson Json? @map("after_json")
|
|
requestId String? @map("request_id")
|
|
correlationId String? @map("correlation_id")
|
|
ipHash String? @map("ip_hash")
|
|
userAgent String? @map("user_agent")
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
|
|
// Relations
|
|
actorUser User? @relation(fields: [actorUserId], references: [id])
|
|
|
|
@@index([resourceType, resourceId, createdAt], map: "idx_audit_resource_created")
|
|
@@index([actorUserId, createdAt], map: "idx_audit_actor_created")
|
|
@@index([actionType, createdAt], map: "idx_audit_action_created")
|
|
@@map("audit_logs")
|
|
}
|
|
|
|
/// 도메인 이벤트를 append-only로 보관
|
|
model EventLog {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
aggregateType String @map("aggregate_type")
|
|
aggregateId String @map("aggregate_id")
|
|
eventName String @map("event_name")
|
|
eventVersion Int @default(1) @map("event_version")
|
|
eventKey String @unique @map("event_key")
|
|
payloadJson Json @map("payload_json")
|
|
actorUserId BigInt? @map("actor_user_id")
|
|
causationId String? @map("causation_id")
|
|
correlationId String? @map("correlation_id")
|
|
piiLevel PiiLevel @default(NONE) @map("pii_level")
|
|
occurredAt DateTime @map("occurred_at") @db.Timestamptz(6)
|
|
recordedAt DateTime @default(now()) @map("recorded_at") @db.Timestamptz(6)
|
|
|
|
@@index([aggregateType, aggregateId, occurredAt], map: "idx_event_aggregate_occurred")
|
|
@@index([eventName, occurredAt], map: "idx_event_name_occurred")
|
|
@@map("event_logs")
|
|
}
|
|
|
|
/// 외부 발행/재시도/실패 처리를 EventLog와 분리해 관리
|
|
model OutboxEvent {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
aggregateType String @map("aggregate_type")
|
|
aggregateId String @map("aggregate_id")
|
|
eventName String @map("event_name")
|
|
payloadJson Json @map("payload_json")
|
|
publishStatus PublishStatus @default(PENDING) @map("publish_status")
|
|
availableAt DateTime @default(now()) @map("available_at") @db.Timestamptz(6)
|
|
retryCount Int @default(0) @map("retry_count")
|
|
lastError String? @map("last_error")
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
|
|
@@index([publishStatus, availableAt], map: "idx_outbox_status_available")
|
|
@@map("outbox_events")
|
|
}
|
|
|
|
/// 웹훅과 중복 요청 방지를 위한 키 저장소
|
|
model IdempotencyKey {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
scope String @map("scope")
|
|
idempotencyKey String @map("idempotency_key")
|
|
requestHash String? @map("request_hash")
|
|
responseHash String? @map("response_hash")
|
|
expiresAt DateTime? @map("expires_at") @db.Timestamptz(6)
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
|
|
@@unique([scope, idempotencyKey], map: "uq_idempotency_scope_key")
|
|
@@map("idempotency_keys")
|
|
}
|
|
|
|
/// 기능 플래그 토글
|
|
model FeatureFlag {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
flagKey String @unique @map("flag_key")
|
|
isEnabled Boolean @default(false) @map("is_enabled")
|
|
description String? @map("description")
|
|
enabledAt DateTime? @map("enabled_at") @db.Timestamptz(6)
|
|
disabledAt DateTime? @map("disabled_at") @db.Timestamptz(6)
|
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
|
|
@@map("feature_flags")
|
|
}
|
|
|
|
// =============================================================================
|
|
// 8. 마스터 데이터
|
|
// =============================================================================
|
|
|
|
/// 지역 계층 마스터 (단일 계층 테이블 + self relation)
|
|
model RegionHierarchy {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
code String @unique @map("code")
|
|
nameKo String @map("name_ko")
|
|
fullNameKo String? @map("full_name_ko")
|
|
regionType RegionType @map("region_type")
|
|
parentId BigInt? @map("parent_id")
|
|
depth Int @default(0) @map("depth")
|
|
pathCode String? @map("path_code")
|
|
sortOrder Int @default(0) @map("sort_order")
|
|
isActive Boolean @default(true) @map("is_active")
|
|
isBetaEnabled Boolean @default(false) @map("is_beta_enabled")
|
|
latitude Decimal? @map("latitude") @db.Decimal(10, 7)
|
|
longitude Decimal? @map("longitude") @db.Decimal(10, 7)
|
|
externalCode String? @map("external_code")
|
|
|
|
// Self relation
|
|
parent RegionHierarchy? @relation("RegionParentChild", fields: [parentId], references: [id])
|
|
children RegionHierarchy[] @relation("RegionParentChild")
|
|
|
|
// Relations
|
|
storesAsCluster Store[] @relation("StoreRegionCluster")
|
|
storesAsLeaf Store[] @relation("StoreRegionLeaf")
|
|
vendorsPrimaryRegion Vendor[]
|
|
vendorCoverageRegions VendorCoverageRegion[]
|
|
|
|
@@index([parentId], map: "idx_region_parent")
|
|
@@index([regionType, isActive], map: "idx_region_type_active")
|
|
@@index([isBetaEnabled, isActive], map: "idx_region_beta_active")
|
|
@@map("region_hierarchies")
|
|
}
|
|
|
|
/// 업종 분류 마스터 (단일 계층 테이블 + self relation)
|
|
model IndustryTaxonomy {
|
|
id BigInt @id @default(autoincrement()) @map("id")
|
|
code String @unique @map("code")
|
|
nameKo String @map("name_ko")
|
|
parentId BigInt? @map("parent_id")
|
|
depth Int @default(0) @map("depth")
|
|
sortOrder Int @default(0) @map("sort_order")
|
|
isLeaf Boolean @default(false) @map("is_leaf")
|
|
isActive Boolean @default(true) @map("is_active")
|
|
isBetaEnabled Boolean @default(false) @map("is_beta_enabled")
|
|
externalCode String? @map("external_code")
|
|
searchAliases String[] @map("search_aliases")
|
|
|
|
// Self relation
|
|
parent IndustryTaxonomy? @relation("IndustryParentChild", fields: [parentId], references: [id])
|
|
children IndustryTaxonomy[] @relation("IndustryParentChild")
|
|
|
|
// Relations
|
|
stores Store[]
|
|
|
|
@@index([parentId], map: "idx_industry_parent")
|
|
@@index([isLeaf, isActive], map: "idx_industry_leaf_active")
|
|
@@index([isBetaEnabled, isActive], map: "idx_industry_beta_active")
|
|
@@map("industry_taxonomies")
|
|
}
|