- 모노레포 구조 (Turborepo + pnpm): @relink/domain, @relink/shared, @relink/infrastructure, @relink/database, @relink/web - 도메인 레이어: 매장(store), 매칭(matching), 업체(vendor), 보조금(subsidy), 계약/에스크로(contract) TDD 완료 (158 단위 테스트) - 서비스 레이어: 전 도메인 서비스 함수 + 통합 테스트 (58 테스트) - 프론트엔드: Next.js 15 App Router, 13개 페이지 (사용자 6 + 관리자 7) - 인프라: PostgreSQL 16 + PostGIS, Prisma ORM, Docker Compose, AuditLog + OutboxEvent 패턴 - .env 파일 포함 (로컬 개발 기본값만 포함, 실제 시크릿 없음) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
16 KiB
Re:Link schema.prisma 설계 초안
목적
이 문서는 Re:Link 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코드 또는 마스터 테이블 사용
공통 컬럼
대부분의 트랜잭션성 모델은 아래 공통 컬럼을 가진다.
idpublicId또는 비즈니스 고유 키createdAtupdatedAt- 필요한 경우
deletedAt
append-only 성격의 모델은 updatedAt, deletedAt 없이 생성 시점만 가진다.
모델 설계
1. 사용자/권한
User
목적: 사용자 기본 계정과 로그인 주체를 관리한다.
주요 필드:
idpublicIdemailemailNormalizedphonephoneNormalizednameprimaryRolestatusemailVerifiedAtlastLoginAtcreatedAtupdatedAt
관계:
profiles UserProfile[]consents UserConsent[]stores Store[]vendors Vendor[]
인덱스:
@unique(emailNormalized)@@index([primaryRole, status])
UserProfile
목적: 역할별 추가 프로필 정보를 확장 가능하게 저장한다.
주요 필드:
iduserIdprofileTypebusinessRegistrationNumbermetadataJsoncreatedAtupdatedAt
UserConsent
목적: 개인정보, 정보 공개, 마케팅, 제3자 제공 동의를 버전 단위로 저장한다.
주요 필드:
iduserIdconsentTypepolicyVersionIdisGrantedgrantedAtrevokedAt
2. 매장 공급 도메인
Store
목적: 폐업자가 등록한 매장의 중심 aggregate다.
주요 필드:
idpublicIdownerUserIdlistingTitlepublicSummaryindustryLeafIdregionClusterIdregionLeafIdroadAddressdetailAddresslatitudelongitudereviewStatuspublicationStatusdealStatuspolicyVersionIdpublishedAtapprovedAtrejectedAtcreatedAtupdatedAt
핵심 설계:
StoreStatus단일 컬럼 대신reviewStatus,publicationStatus,dealStatus3축으로 분리한다.- 검색/필터에 쓰는 값은 JSON이 아니라 정식 컬럼으로 둔다.
- 주소 공개 정책 때문에 상세 주소는 API 응답 단계에서 권한 필터링한다.
인덱스:
@@index([ownerUserId, createdAt])@@index([reviewStatus, createdAt])@@index([regionClusterId, industryLeafId, publicationStatus, dealStatus, publishedAt])
StoreLease
목적: 임대/권리금 관련 정보를 1:1 확장 테이블로 저장한다.
주요 필드:
storeIddepositAmountmonthlyRentAmountmaintenanceFeeAmountpremiumAmounttransferableleaseExpiresAtremainingLeaseMonthscreatedAtupdatedAt
제약:
storeIdunique- 금액 필드는 모두
>= 0
StoreFacility
목적: F&B 필터링과 인수 판단에 필요한 설비 정보를 저장한다.
주요 필드:
storeIdexclusiveAreaSqmfloorLevelseatCounthasGashasDrainagehasDuctelectricCapacityKwkitchenEquipmentSummaryparkingCountrestroomTypecreatedAtupdatedAt
제약:
storeIduniqueexclusiveAreaSqm > 0
StoreLifecycle
목적: 폐업/인수/철거 일정 정보를 저장한다.
주요 필드:
storeIdclosingPlannedAttakeoverAvailableAtdemolitionPreferredAtvacateByAtisCurrentlyOperatingurgencyLevelcreatedAtupdatedAt
StorePhoto
목적: 매장 사진과 공개 범위를 저장한다.
주요 필드:
idstoreIdstorageKeyphotoCategoryvisibilityScopesortOrderuploadedByUserIdchecksumSha256widthheighttakenAtisRepresentativecreatedAt
인덱스:
@@index([storeId, sortOrder])
3. 매칭 도메인
MatchRequest
목적: 창업자/업체/운영자 추천 기반 매칭 요청을 관리한다.
주요 필드:
idpublicIdstoreIdmatchTyperequesterUserIdrequesterVendorIdsourceTypestatusoperatorAssigneeIdmessagedecisionReasonCodeacceptedAtrejectedAtclosedAtcreatedAtupdatedAt
핵심 설계:
ACQUISITION,DEMOLITION,INTERIOR를 한 테이블로 관리한다.matchType에 따라 필요한 FK가 달라지므로 raw SQLCHECK제약을 둔다.
인덱스:
@@index([storeId, status, createdAt])@@index([requesterUserId, status, createdAt])@@index([requesterVendorId, status, createdAt])- partial unique index:
- 열린 상태에서 동일 사용자/동일 매장/동일 요청 타입 중복 금지
4. 지원금 도메인
SubsidyCase
목적: 매장 기준 지원금 진행 건을 관리한다.
주요 필드:
idpublicIdstoreIdapplicantUserIdprogramCodestatuseligibilityResultpolicyVersionIdoperatorAssigneeIdchecklistVersionCodesubmissionReadyAtsubmittedAtreviewedAtrejectionReasonCodeeligibilitySnapshotJsoncreatedAtupdatedAt
인덱스:
@@index([storeId, status, createdAt])@@index([applicantUserId, status, createdAt])@@index([operatorAssigneeId, status, updatedAt])- partial unique index:
- 같은
storeId + programCode조합의 active case는 1개
- 같은
SubsidyChecklistItem
목적: 정책 버전에 따라 생성된 체크리스트 항목을 저장한다.
주요 필드:
idsubsidyCaseIditemCodeisRequiredstatuscheckedAtcheckedByUserId
SubsidyDocument
목적: 지원금 첨부 서류와 검토 상태를 관리한다.
주요 필드:
idsubsidyCaseIddocumentTypeCodestorageKeyreviewStatusuploadedByUserIduploadedAtreviewedAtreviewedByUserId
5. 업체 도메인
Vendor
목적: 철거/인테리어 업체의 기본 프로필을 관리한다.
주요 필드:
idpublicIdownerUserIdvendorTypebusinessNamecontactNamecontactPhoneNormalizedbusinessRegistrationNumberserviceIntroprimaryRegionIdcertificationStatuslatestCertificationIddeletedAtcreatedAtupdatedAt
인덱스:
@@index([ownerUserId])@@index([vendorType, certificationStatus, createdAt])
VendorCoverageRegion
목적: 업체 서비스 가능 지역을 다대다 조인으로 관리한다.
주요 필드:
idvendorIdregionIdisPrimarycreatedAt
인덱스:
@@unique([vendorId, regionId])@@index([regionId, vendorId])
VendorCertification
목적: 업체 인증 이력을 append-only에 가깝게 유지한다.
주요 필드:
idvendorIdstatusrequestedScopeCodedocumentChecklistJsonappliedAtreviewedAtreviewedByUserIdreasonCodevalidUntilcreatedAtupdatedAt
6. 신뢰 인프라 도메인
Contract
목적: 매칭에서 생성된 계약의 현재 상태와 참여자를 관리한다.
주요 필드:
idpublicIdmatchRequestIdstoreIdcontractTypestatuscreatedByUserIdstoreOwnerUserIdbuyerUserIdvendorIdpolicyVersionIdtemplateCodecurrentVersionIdescrowStatussignedAteffectiveAtcompletedAtcancelledAtcreatedAtupdatedAt
인덱스:
@@unique([matchRequestId])@@index([storeId, status, createdAt])@@index([vendorId, status, createdAt])@@index([buyerUserId, status, createdAt])
ContractVersion
목적: 계약 문서를 불변 버전으로 저장한다.
주요 필드:
idcontractIdversionNotemplateCodetemplateVersiondocumentStorageKeydocumentSha256renderedSnapshotJsonchangeSummarycreatedByUserIdcreatedAt
인덱스:
@@unique([contractId, versionNo])
SignatureEvidence
목적: 전자서명 또는 동의 증적을 저장한다.
주요 필드:
idcontractIdcontractVersionIdsignerRolesignerUserIdevidenceTypeproviderCodeproviderEventIdsignedAtsignatureHashpayloadJsonipAddressuserAgentcreatedAt
인덱스:
@@index([contractVersionId, signedAt])- 가능한 경우
@unique(providerEventId)
EscrowTransaction
목적: 결제/환불/정산/보류 관련 금전 이벤트를 immutable ledger 형태로 저장한다.
주요 필드:
idcontractIdtransactionTypeproviderCodeproviderTransactionIdstatusamountcurrencyCoderequestedByUserIdapprovedByUserIdidempotencyKeyrawPayloadJsonoccurredAtcreatedAt
인덱스:
@unique(providerTransactionId)@unique(idempotencyKey)@@index([contractId, occurredAt])@@index([status, occurredAt])
InspectionRecord
목적: 검수 요청과 검토 결과를 저장한다.
주요 필드:
idcontractIdinspectionTypestatussubmittedByUserIdsubmittedAtreviewedByUserIdreviewedAtreviewMemoevidencePayloadJsoncreatedAtupdatedAt
DisputeCase
목적: 분쟁 접수, 조사, 중재, 종료 상태를 관리한다.
주요 필드:
idcontractIdopenedByUserIdassignedOperatorIdstatusreasonCodedescriptionresolutionCoderesolutionSummaryevidencePayloadJsonopenedAtresolvedAtresolvedByUserIdcreatedAtupdatedAt
인덱스:
@@index([contractId, status, openedAt])@@index([assignedOperatorId, status, openedAt])- partial unique index:
- 계약당 active dispute는 1건
7. 운영/분석 도메인
PolicyVersion
목적: 정책 버전을 모델에 귀속시키기 위한 기준 테이블이다.
주요 필드:
idpolicyTypeversionCodecontentHasheffectiveFromisActivecreatedAt
AuditLog
목적: 누가 무엇을 왜 바꿨는지를 운영 감사 기준으로 남긴다.
주요 필드:
idresourceTyperesourceIdactionTypeactorUserIdactorRolereasonCodememobeforeJsonafterJsonrequestIdcorrelationIdipHashuserAgentcreatedAt
인덱스:
@@index([resourceType, resourceId, createdAt])@@index([actorUserId, createdAt])@@index([actionType, createdAt])
EventLog
목적: 도메인 이벤트를 append-only로 보관한다.
주요 필드:
idaggregateTypeaggregateIdeventNameeventVersioneventKeypayloadJsonactorUserIdcausationIdcorrelationIdpiiLeveloccurredAtrecordedAt
인덱스:
@unique(eventKey)@@index([aggregateType, aggregateId, occurredAt])@@index([eventName, occurredAt])
OutboxEvent
목적: 외부 발행/재시도/실패 처리를 EventLog와 분리해 관리한다.
주요 필드:
idaggregateTypeaggregateIdeventNamepayloadJsonpublishStatusavailableAtretryCountlastErrorcreatedAt
인덱스:
@@index([publishStatus, availableAt])
IdempotencyKey
목적: 웹훅과 중복 요청 방지를 위한 키 저장소다.
주요 필드:
idscopeidempotencyKeyrequestHashresponseHashexpiresAtcreatedAt
인덱스:
@@unique([scope, idempotencyKey])
Enum 초안
고정 enum 후보
UserRoleCLOSING_OWNERFOUNDERVENDOR_MANAGEROPS_MANAGERSUBSIDY_OPERATORTRUST_OPERATORFINANCE_OPERATORSUPER_ADMIN
UserStatusPENDING_VERIFICATIONACTIVESUSPENDEDDEACTIVATED
StoreReviewStatusDRAFTSUBMITTEDREVIEWINGAPPROVEDREJECTED
StorePublicationStatusPRIVATERESTRICTEDPUBLISHEDUNPUBLISHED
StoreDealStatusOPENMATCHINGRESERVEDCONTRACTEDCLOSEDCANCELLED
PhotoCategoryEXTERIORINTERIORKITCHENEQUIPMENTFLOOR_PLANDOCUMENT
VisibilityScopeINTERNALMATCHED_ONLYPUBLIC_SUMMARY
MatchTypeACQUISITIONDEMOLITIONINTERIOR
MatchRequestStatusOPENREVIEWINGACCEPTEDREJECTEDCONTRACTINGEXPIREDCANCELLEDCOMPLETED
SubsidyCaseStatusDRAFTELIGIBILITY_CHECKEDDOCUMENTS_PENDINGREVIEWINGREADY_TO_SUBMITSUBMITTEDAPPROVEDREJECTEDCLOSED
EligibilityResultUNKNOWNELIGIBLECONDITIONALLY_ELIGIBLENOT_ELIGIBLE
VendorTypeDEMOLITIONINTERIORBOTH
VendorCertificationStatusAPPLIEDREVIEWINGAPPROVEDREJECTEDSUSPENDEDEXPIRED
ContractTypeACQUISITIONDEMOLITIONINTERIOR
ContractStatusDRAFTGENERATEDSIGNINGSIGNEDACTIVECOMPLETEDCANCELLEDTERMINATED
EscrowStatusNOT_STARTEDDEPOSIT_PENDINGHOLDINGRELEASE_REVIEWRELEASEDREFUNDEDDISPUTED
EscrowTransactionTypeDEPOSITRELEASEREFUNDADJUSTMENTHOLD
InspectionStatusREQUESTEDSUBMITTEDREVIEWINGAPPROVEDREJECTED
DisputeStatusOPENINVESTIGATINGMEDIATINGRESOLVEDCLOSED
RegionTypeCOUNTRYSIDOSIGUNGUADMIN_DONGCOMMERCIAL_AREABETA_CLUSTER
enum으로 두지 않을 값
아래는 자주 바뀌거나 운영 정책에 따라 변할 수 있으므로 enum보다 코드 값 또는 마스터 데이터로 둔다.
reasonCodeprogramCodedocumentTypeCodeactionTypeeventNametemplateCode
마이그레이션 전략
기본 원칙
- 운영 DB 변경은 항상 migration 파일로 관리한다.
- 스키마 변경과 데이터 backfill은 분리한다.
- 대용량 테이블 인덱스는 raw SQL로
CREATE INDEX CONCURRENTLY를 사용한다. - Prisma가 표현하기 어려운 partial unique index, check constraint, PostGIS index는 custom SQL migration으로 보강한다.
첫 스키마에서 넣을 custom SQL 후보
- 열린 매칭 요청 중복 방지 partial unique index
- 지원금 active case 중복 방지 partial unique index
- 계약당 active dispute 1건 제한 partial unique index
amount >= 0,exclusive_area_sqm > 0같은 check constraint- 향후
locationPoint를 도입할 경우 GIST index
MVP에서 나중으로 미룰 모델
아래 모델은 개념만 유지하고 실제 첫 schema.prisma에서는 보류할 수 있다.
PriceEstimateVendorQuoteStoreAssetAlertSubscriptionNotificationDeliveryLogKpiSnapshot
바로 다음 단계
- 이 문서를 기준으로 실제
prisma/schema.prisma초안을 생성한다. docs/master-data/beta-master-data.md와 FK 관계를 맞춘다.D001,D002,D003정책 문서가 완성되면 nullable/required 필드를 다시 조정한다.