Files
startover/docs/database/schema-prisma-draft.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

16 KiB

Startover schema.prisma 설계 초안

목적

이 문서는 Startover MVP의 데이터 모델을 실제 schema.prisma로 내리기 전에, 엔티티/필드/관계/인덱스/제약 조건을 고정하기 위한 초안이다.

대상 범위는 assisted marketplace MVP이며, 아래 원칙을 따른다.

  • 운영자 개입형 플로우를 우선한다.
  • 정보 공개는 제한 공개가 기본값이다.
  • 결제/정산/분쟁은 append-only 로그와 감사 로그를 남긴다.
  • 마스터 데이터는 enum이 아니라 테이블과 seed로 관리한다.
  • 하드코딩 대신 PolicyVersion, RegionHierarchy, IndustryTaxonomy를 참조한다.

설계 원칙

네이밍

  • Prisma 모델명: PascalCase
  • Prisma 필드명: camelCase
  • 실제 DB 테이블/컬럼명: snake_case
  • 실제 DB명은 @@map, @map으로 매핑한다.

공통 타입

  • 내부 PK: BigInt @id @default(autoincrement())
  • 외부 노출용 ID: publicId String @unique
  • 금액: Decimal @db.Decimal(14, 2)
  • 시각: DateTime @db.Timestamptz(6)
  • 상태: 안정적인 값만 enum으로 관리
  • 자주 바뀌는 코드: enum보다 String 코드 또는 마스터 테이블 사용

공통 컬럼

대부분의 트랜잭션성 모델은 아래 공통 컬럼을 가진다.

  • id
  • publicId 또는 비즈니스 고유 키
  • createdAt
  • updatedAt
  • 필요한 경우 deletedAt

append-only 성격의 모델은 updatedAt, deletedAt 없이 생성 시점만 가진다.

모델 설계

1. 사용자/권한

User

목적: 사용자 기본 계정과 로그인 주체를 관리한다.

주요 필드:

  • id
  • publicId
  • email
  • emailNormalized
  • phone
  • phoneNormalized
  • name
  • primaryRole
  • status
  • emailVerifiedAt
  • lastLoginAt
  • createdAt
  • updatedAt

관계:

  • profiles UserProfile[]
  • consents UserConsent[]
  • stores Store[]
  • vendors Vendor[]

인덱스:

  • @unique(emailNormalized)
  • @@index([primaryRole, status])

UserProfile

목적: 역할별 추가 프로필 정보를 확장 가능하게 저장한다.

주요 필드:

  • id
  • userId
  • profileType
  • businessRegistrationNumber
  • metadataJson
  • createdAt
  • updatedAt

UserConsent

목적: 개인정보, 정보 공개, 마케팅, 제3자 제공 동의를 버전 단위로 저장한다.

주요 필드:

  • id
  • userId
  • consentType
  • policyVersionId
  • isGranted
  • grantedAt
  • revokedAt

2. 매장 공급 도메인

Store

목적: 폐업자가 등록한 매장의 중심 aggregate다.

주요 필드:

  • id
  • publicId
  • ownerUserId
  • listingTitle
  • publicSummary
  • industryLeafId
  • regionClusterId
  • regionLeafId
  • roadAddress
  • detailAddress
  • latitude
  • longitude
  • reviewStatus
  • publicationStatus
  • dealStatus
  • policyVersionId
  • publishedAt
  • approvedAt
  • rejectedAt
  • createdAt
  • updatedAt

핵심 설계:

  • StoreStatus 단일 컬럼 대신 reviewStatus, publicationStatus, dealStatus 3축으로 분리한다.
  • 검색/필터에 쓰는 값은 JSON이 아니라 정식 컬럼으로 둔다.
  • 주소 공개 정책 때문에 상세 주소는 API 응답 단계에서 권한 필터링한다.

인덱스:

  • @@index([ownerUserId, createdAt])
  • @@index([reviewStatus, createdAt])
  • @@index([regionClusterId, industryLeafId, publicationStatus, dealStatus, publishedAt])

StoreLease

목적: 임대/권리금 관련 정보를 1:1 확장 테이블로 저장한다.

주요 필드:

  • storeId
  • depositAmount
  • monthlyRentAmount
  • maintenanceFeeAmount
  • premiumAmount
  • transferable
  • leaseExpiresAt
  • remainingLeaseMonths
  • createdAt
  • updatedAt

제약:

  • storeId unique
  • 금액 필드는 모두 >= 0

StoreFacility

목적: F&B 필터링과 인수 판단에 필요한 설비 정보를 저장한다.

주요 필드:

  • storeId
  • exclusiveAreaSqm
  • floorLevel
  • seatCount
  • hasGas
  • hasDrainage
  • hasDuct
  • electricCapacityKw
  • kitchenEquipmentSummary
  • parkingCount
  • restroomType
  • createdAt
  • updatedAt

제약:

  • storeId unique
  • exclusiveAreaSqm > 0

StoreLifecycle

목적: 폐업/인수/철거 일정 정보를 저장한다.

주요 필드:

  • storeId
  • closingPlannedAt
  • takeoverAvailableAt
  • demolitionPreferredAt
  • vacateByAt
  • isCurrentlyOperating
  • urgencyLevel
  • createdAt
  • updatedAt

StorePhoto

목적: 매장 사진과 공개 범위를 저장한다.

주요 필드:

  • id
  • storeId
  • storageKey
  • photoCategory
  • visibilityScope
  • sortOrder
  • uploadedByUserId
  • checksumSha256
  • width
  • height
  • takenAt
  • isRepresentative
  • createdAt

인덱스:

  • @@index([storeId, sortOrder])

3. 매칭 도메인

MatchRequest

목적: 창업자/업체/운영자 추천 기반 매칭 요청을 관리한다.

주요 필드:

  • id
  • publicId
  • storeId
  • matchType
  • requesterUserId
  • requesterVendorId
  • sourceType
  • status
  • operatorAssigneeId
  • message
  • decisionReasonCode
  • acceptedAt
  • rejectedAt
  • closedAt
  • createdAt
  • updatedAt

핵심 설계:

  • ACQUISITION, DEMOLITION, INTERIOR를 한 테이블로 관리한다.
  • matchType에 따라 필요한 FK가 달라지므로 raw SQL CHECK 제약을 둔다.

인덱스:

  • @@index([storeId, status, createdAt])
  • @@index([requesterUserId, status, createdAt])
  • @@index([requesterVendorId, status, createdAt])
  • partial unique index:
    • 열린 상태에서 동일 사용자/동일 매장/동일 요청 타입 중복 금지

4. 지원금 도메인

SubsidyCase

목적: 매장 기준 지원금 진행 건을 관리한다.

주요 필드:

  • id
  • publicId
  • storeId
  • applicantUserId
  • programCode
  • status
  • eligibilityResult
  • policyVersionId
  • operatorAssigneeId
  • checklistVersionCode
  • submissionReadyAt
  • submittedAt
  • reviewedAt
  • rejectionReasonCode
  • eligibilitySnapshotJson
  • createdAt
  • updatedAt

인덱스:

  • @@index([storeId, status, createdAt])
  • @@index([applicantUserId, status, createdAt])
  • @@index([operatorAssigneeId, status, updatedAt])
  • partial unique index:
    • 같은 storeId + programCode 조합의 active case는 1개

SubsidyChecklistItem

목적: 정책 버전에 따라 생성된 체크리스트 항목을 저장한다.

주요 필드:

  • id
  • subsidyCaseId
  • itemCode
  • isRequired
  • status
  • checkedAt
  • checkedByUserId

SubsidyDocument

목적: 지원금 첨부 서류와 검토 상태를 관리한다.

주요 필드:

  • id
  • subsidyCaseId
  • documentTypeCode
  • storageKey
  • reviewStatus
  • uploadedByUserId
  • uploadedAt
  • reviewedAt
  • reviewedByUserId

5. 업체 도메인

Vendor

목적: 철거/인테리어 업체의 기본 프로필을 관리한다.

주요 필드:

  • id
  • publicId
  • ownerUserId
  • vendorType
  • businessName
  • contactName
  • contactPhoneNormalized
  • businessRegistrationNumber
  • serviceIntro
  • primaryRegionId
  • certificationStatus
  • latestCertificationId
  • deletedAt
  • createdAt
  • updatedAt

인덱스:

  • @@index([ownerUserId])
  • @@index([vendorType, certificationStatus, createdAt])

VendorCoverageRegion

목적: 업체 서비스 가능 지역을 다대다 조인으로 관리한다.

주요 필드:

  • id
  • vendorId
  • regionId
  • isPrimary
  • createdAt

인덱스:

  • @@unique([vendorId, regionId])
  • @@index([regionId, vendorId])

VendorCertification

목적: 업체 인증 이력을 append-only에 가깝게 유지한다.

주요 필드:

  • id
  • vendorId
  • status
  • requestedScopeCode
  • documentChecklistJson
  • appliedAt
  • reviewedAt
  • reviewedByUserId
  • reasonCode
  • validUntil
  • createdAt
  • updatedAt

6. 신뢰 인프라 도메인

Contract

목적: 매칭에서 생성된 계약의 현재 상태와 참여자를 관리한다.

주요 필드:

  • id
  • publicId
  • matchRequestId
  • storeId
  • contractType
  • status
  • createdByUserId
  • storeOwnerUserId
  • buyerUserId
  • vendorId
  • policyVersionId
  • templateCode
  • currentVersionId
  • escrowStatus
  • signedAt
  • effectiveAt
  • completedAt
  • cancelledAt
  • createdAt
  • updatedAt

인덱스:

  • @@unique([matchRequestId])
  • @@index([storeId, status, createdAt])
  • @@index([vendorId, status, createdAt])
  • @@index([buyerUserId, status, createdAt])

ContractVersion

목적: 계약 문서를 불변 버전으로 저장한다.

주요 필드:

  • id
  • contractId
  • versionNo
  • templateCode
  • templateVersion
  • documentStorageKey
  • documentSha256
  • renderedSnapshotJson
  • changeSummary
  • createdByUserId
  • createdAt

인덱스:

  • @@unique([contractId, versionNo])

SignatureEvidence

목적: 전자서명 또는 동의 증적을 저장한다.

주요 필드:

  • id
  • contractId
  • contractVersionId
  • signerRole
  • signerUserId
  • evidenceType
  • providerCode
  • providerEventId
  • signedAt
  • signatureHash
  • payloadJson
  • ipAddress
  • userAgent
  • createdAt

인덱스:

  • @@index([contractVersionId, signedAt])
  • 가능한 경우 @unique(providerEventId)

EscrowTransaction

목적: 결제/환불/정산/보류 관련 금전 이벤트를 immutable ledger 형태로 저장한다.

주요 필드:

  • id
  • contractId
  • transactionType
  • providerCode
  • providerTransactionId
  • status
  • amount
  • currencyCode
  • requestedByUserId
  • approvedByUserId
  • idempotencyKey
  • rawPayloadJson
  • occurredAt
  • createdAt

인덱스:

  • @unique(providerTransactionId)
  • @unique(idempotencyKey)
  • @@index([contractId, occurredAt])
  • @@index([status, occurredAt])

InspectionRecord

목적: 검수 요청과 검토 결과를 저장한다.

주요 필드:

  • id
  • contractId
  • inspectionType
  • status
  • submittedByUserId
  • submittedAt
  • reviewedByUserId
  • reviewedAt
  • reviewMemo
  • evidencePayloadJson
  • createdAt
  • updatedAt

DisputeCase

목적: 분쟁 접수, 조사, 중재, 종료 상태를 관리한다.

주요 필드:

  • id
  • contractId
  • openedByUserId
  • assignedOperatorId
  • status
  • reasonCode
  • description
  • resolutionCode
  • resolutionSummary
  • evidencePayloadJson
  • openedAt
  • resolvedAt
  • resolvedByUserId
  • createdAt
  • updatedAt

인덱스:

  • @@index([contractId, status, openedAt])
  • @@index([assignedOperatorId, status, openedAt])
  • partial unique index:
    • 계약당 active dispute는 1건

7. 운영/분석 도메인

PolicyVersion

목적: 정책 버전을 모델에 귀속시키기 위한 기준 테이블이다.

주요 필드:

  • id
  • policyType
  • versionCode
  • contentHash
  • effectiveFrom
  • isActive
  • createdAt

AuditLog

목적: 누가 무엇을 왜 바꿨는지를 운영 감사 기준으로 남긴다.

주요 필드:

  • id
  • resourceType
  • resourceId
  • actionType
  • actorUserId
  • actorRole
  • reasonCode
  • memo
  • beforeJson
  • afterJson
  • requestId
  • correlationId
  • ipHash
  • userAgent
  • createdAt

인덱스:

  • @@index([resourceType, resourceId, createdAt])
  • @@index([actorUserId, createdAt])
  • @@index([actionType, createdAt])

EventLog

목적: 도메인 이벤트를 append-only로 보관한다.

주요 필드:

  • id
  • aggregateType
  • aggregateId
  • eventName
  • eventVersion
  • eventKey
  • payloadJson
  • actorUserId
  • causationId
  • correlationId
  • piiLevel
  • occurredAt
  • recordedAt

인덱스:

  • @unique(eventKey)
  • @@index([aggregateType, aggregateId, occurredAt])
  • @@index([eventName, occurredAt])

OutboxEvent

목적: 외부 발행/재시도/실패 처리를 EventLog와 분리해 관리한다.

주요 필드:

  • id
  • aggregateType
  • aggregateId
  • eventName
  • payloadJson
  • publishStatus
  • availableAt
  • retryCount
  • lastError
  • createdAt

인덱스:

  • @@index([publishStatus, availableAt])

IdempotencyKey

목적: 웹훅과 중복 요청 방지를 위한 키 저장소다.

주요 필드:

  • id
  • scope
  • idempotencyKey
  • requestHash
  • responseHash
  • expiresAt
  • createdAt

인덱스:

  • @@unique([scope, idempotencyKey])

Enum 초안

고정 enum 후보

  • UserRole
    • CLOSING_OWNER
    • FOUNDER
    • VENDOR_MANAGER
    • OPS_MANAGER
    • SUBSIDY_OPERATOR
    • TRUST_OPERATOR
    • FINANCE_OPERATOR
    • SUPER_ADMIN
  • UserStatus
    • PENDING_VERIFICATION
    • ACTIVE
    • SUSPENDED
    • DEACTIVATED
  • StoreReviewStatus
    • DRAFT
    • SUBMITTED
    • REVIEWING
    • APPROVED
    • REJECTED
  • StorePublicationStatus
    • PRIVATE
    • RESTRICTED
    • PUBLISHED
    • UNPUBLISHED
  • StoreDealStatus
    • OPEN
    • MATCHING
    • RESERVED
    • CONTRACTED
    • CLOSED
    • CANCELLED
  • PhotoCategory
    • EXTERIOR
    • INTERIOR
    • KITCHEN
    • EQUIPMENT
    • FLOOR_PLAN
    • DOCUMENT
  • VisibilityScope
    • INTERNAL
    • MATCHED_ONLY
    • PUBLIC_SUMMARY
  • MatchType
    • ACQUISITION
    • DEMOLITION
    • INTERIOR
  • MatchRequestStatus
    • OPEN
    • REVIEWING
    • ACCEPTED
    • REJECTED
    • CONTRACTING
    • EXPIRED
    • CANCELLED
    • COMPLETED
  • SubsidyCaseStatus
    • DRAFT
    • ELIGIBILITY_CHECKED
    • DOCUMENTS_PENDING
    • REVIEWING
    • READY_TO_SUBMIT
    • SUBMITTED
    • APPROVED
    • REJECTED
    • CLOSED
  • EligibilityResult
    • UNKNOWN
    • ELIGIBLE
    • CONDITIONALLY_ELIGIBLE
    • NOT_ELIGIBLE
  • VendorType
    • DEMOLITION
    • INTERIOR
    • BOTH
  • VendorCertificationStatus
    • APPLIED
    • REVIEWING
    • APPROVED
    • REJECTED
    • SUSPENDED
    • EXPIRED
  • ContractType
    • ACQUISITION
    • DEMOLITION
    • INTERIOR
  • ContractStatus
    • DRAFT
    • GENERATED
    • SIGNING
    • SIGNED
    • ACTIVE
    • COMPLETED
    • CANCELLED
    • TERMINATED
  • EscrowStatus
    • NOT_STARTED
    • DEPOSIT_PENDING
    • HOLDING
    • RELEASE_REVIEW
    • RELEASED
    • REFUNDED
    • DISPUTED
  • EscrowTransactionType
    • DEPOSIT
    • RELEASE
    • REFUND
    • ADJUSTMENT
    • HOLD
  • InspectionStatus
    • REQUESTED
    • SUBMITTED
    • REVIEWING
    • APPROVED
    • REJECTED
  • DisputeStatus
    • OPEN
    • INVESTIGATING
    • MEDIATING
    • RESOLVED
    • CLOSED
  • RegionType
    • COUNTRY
    • SIDO
    • SIGUNGU
    • ADMIN_DONG
    • COMMERCIAL_AREA
    • BETA_CLUSTER

enum으로 두지 않을 값

아래는 자주 바뀌거나 운영 정책에 따라 변할 수 있으므로 enum보다 코드 값 또는 마스터 데이터로 둔다.

  • reasonCode
  • programCode
  • documentTypeCode
  • actionType
  • eventName
  • templateCode

마이그레이션 전략

기본 원칙

  • 운영 DB 변경은 항상 migration 파일로 관리한다.
  • 스키마 변경과 데이터 backfill은 분리한다.
  • 대용량 테이블 인덱스는 raw SQL로 CREATE INDEX CONCURRENTLY를 사용한다.
  • Prisma가 표현하기 어려운 partial unique index, check constraint, PostGIS index는 custom SQL migration으로 보강한다.

첫 스키마에서 넣을 custom SQL 후보

  1. 열린 매칭 요청 중복 방지 partial unique index
  2. 지원금 active case 중복 방지 partial unique index
  3. 계약당 active dispute 1건 제한 partial unique index
  4. amount >= 0, exclusive_area_sqm > 0 같은 check constraint
  5. 향후 locationPoint를 도입할 경우 GIST index

MVP에서 나중으로 미룰 모델

아래 모델은 개념만 유지하고 실제 첫 schema.prisma에서는 보류할 수 있다.

  • PriceEstimate
  • VendorQuote
  • StoreAsset
  • AlertSubscription
  • NotificationDeliveryLog
  • KpiSnapshot

바로 다음 단계

  1. 이 문서를 기준으로 실제 prisma/schema.prisma 초안을 생성한다.
  2. docs/master-data/beta-master-data.md와 FK 관계를 맞춘다.
  3. D001, D002, D003 정책 문서가 완성되면 nullable/required 필드를 다시 조정한다.