상세 설계 문서

DB 스키마

DB 스키마 v1

1. 설계 원칙

  • DB는 PostgreSQL을 기준으로 설계하고, Prisma 모델로 옮기기 쉬운 테이블 단위 구조를 사용한다.
  • 본사관리자 계정과 부동산 사무소 사용자는 인증 영역과 테이블을 분리한다.
  • 사무소 업무 데이터는 반드시 officeId를 가진다.
  • 일반 업무 사용자의 데이터 범위는 writerUserId, managerUserId, createdByUserId, assigneeUserId 등 담당자 필드로 제한한다.
  • 모든 주요 테이블은 id, createdAt, updatedAt을 가진다.
  • 업무 데이터는 삭제보다 status, deletedAt 기반 비활성/소프트 삭제를 우선한다.
  • 개인정보 접근, 계약 PDF 생성, 과금 계산처럼 민감하거나 감사가 필요한 작업은 별도 로그를 남긴다.

2. 공통 타입

2.1 ID와 시간

  • id: UUID, primary key
  • createdAt: timestamptz, default now
  • updatedAt: timestamptz, default now
  • deletedAt: timestamptz nullable

2.2 주요 enum

HeadquartersRole = SUPER_ADMIN | ADMIN | BILLING_MANAGER | SUPPORT
HeadquartersUserStatus = ACTIVE | INACTIVE

OfficeStatus = ACTIVE | SUSPENDED | CLOSED
OfficeUserRole = OWNER_ADMIN | AGENT | ASSISTANT
OfficeUserStatus = INVITED | ACTIVE | INACTIVE | SUSPENDED

PermissionScope = SYSTEM | OFFICE
FeatureCode = EXPIRY_KAKAO_ALERT | CUSTOMER_POPUP | LISTING_EXCHANGE | ADVANCED_PDF | BULK_EXCEL_UPLOAD | EXTERNAL_DOCUMENT_LINK

ContractType = SALE | LEASE | COMMERCIAL | LAND | OTHER
ContractStatus = DRAFT | ACTIVE | COMPLETED | CANCELED | ARCHIVED
ContractPartyType = SELLER | BUYER | LESSOR | LESSEE | AGENT | CO_AGENT | OTHER

CustomerStatus = ACTIVE | LEAD | IN_PROGRESS | CONTRACTED | INACTIVE | ARCHIVED
ListingType = APARTMENT | OFFICETEL | HOUSE | COMMERCIAL | LAND | OTHER
ListingDealType = SALE | JEONSE | MONTHLY_RENT | SHORT_TERM | OTHER
ListingVisibility = PRIVATE | OFFICE | EXCHANGE
ListingStatus = DRAFT | ACTIVE | RESERVED | CONTRACTED | HIDDEN | ARCHIVED
ListingExchangeStatus = REQUESTED | APPROVED | REJECTED | REVOKED | EXPIRED

NotificationStatus = SCHEDULED | SENT | FAILED | CANCELED
PopupCampaignStatus = DRAFT | ACTIVE | PAUSED | ENDED
BillingMonthStatus = DRAFT | CONFIRMED | CANCELED
AuditAction = READ | CREATE | UPDATE | DELETE | EXPORT | PRINT | LOGIN | PERMISSION_CHANGE | BILLING_CHANGE

3. 본사 영역

3.1 headquarters_users

본사관리자 계정이다. 부동산 사무소 사용자인 office_users와 분리한다.

필드 타입 필수 설명
id uuid Y PK
name varchar(100) Y 이름
email varchar(255) Y 로그인 이메일, unique
phone varchar(30) N 연락처
passwordHash varchar(255) Y 비밀번호 해시
role HeadquartersRole Y 본사 역할
status HeadquartersUserStatus Y 상태
lastLoginAt timestamptz N 마지막 로그인
createdAt, updatedAt timestamptz Y 생성/수정 일시

관계:

  • headquarters_users.id는 사무소 생성, 과금 정책 변경, 기능 권한 변경, 감사 로그의 행위자 FK로 사용한다.

인덱스:

  • unique headquarters_users_email_key(email)
  • index idx_headquarters_users_status(status)

4. 사무소와 사용자

4.1 offices

부동산 사무소 tenant의 루트 테이블이다.

필드 타입 필수 설명
id uuid Y PK
name varchar(150) Y 사무소명
businessNumber varchar(30) N 사업자등록번호
brokerageRegistrationNo varchar(80) N 중개사무소 등록번호
ownerName varchar(100) N 대표자명
phone varchar(30) N 대표 전화
address1 varchar(255) N 주소
address2 varchar(255) N 상세 주소
postalCode varchar(20) N 우편번호
status OfficeStatus Y 사무소 상태
createdByHeadquartersUserId uuid Y 생성한 본사관리자
memo text N 내부 메모
createdAt, updatedAt, deletedAt timestamptz Y/N 생성/수정/삭제 일시

관계:

  • offices 1:N office_users
  • offices 1:N contracts
  • offices 1:N customers
  • offices 1:N listings
  • offices 1:N office_billing_policies
  • offices 1:N office_features

인덱스:

  • unique partial offices_business_number_key(businessNumber) where businessNumber is not null
  • index idx_offices_status(status)
  • index idx_offices_name(name)

4.2 office_users

부동산 사무소에 소속된 사용자 계정이다.

필드 타입 필수 설명
id uuid Y PK
officeId uuid Y 소속 사무소 FK
name varchar(100) Y 이름
email varchar(255) Y 로그인 이메일
phone varchar(30) N 연락처
passwordHash varchar(255) Y 비밀번호 해시
role OfficeUserRole Y OWNER_ADMIN, AGENT, ASSISTANT
status OfficeUserStatus Y 초대/활성/비활성/정지
billable boolean Y 과금 포함 여부, default true
billingExemptReason varchar(255) N 과금 제외 사유
permissionGroupId uuid N 권한 그룹 FK
invitedByUserId uuid N 초대한 사무소 사용자
invitedByHeadquartersUserId uuid N 본사 초대/생성자
lastLoginAt timestamptz N 마지막 로그인
createdAt, updatedAt, deletedAt timestamptz Y/N 생성/수정/삭제 일시

제약:

  • unique (officeId, email)
  • role = OWNER_ADMIN 사용자는 사무소별 최소 1명 유지를 애플리케이션 또는 트랜잭션으로 보장한다.
  • billable = false는 비활성/초대 미가입/특별 면제 등 명시 사유가 있어야 한다.

인덱스:

  • index idx_office_users_office_role_status(officeId, role, status)
  • index idx_office_users_office_billable_status(officeId, billable, status)
  • index idx_office_users_permission_group(permissionGroupId)

4.3 permission_groups

사무소별 사용자 권한 그룹이다. MVP에서는 JSON 권한을 사용하고, 기능 확정 후 별도 permission 테이블로 분리할 수 있다.

필드 타입 필수 설명
id uuid Y PK
officeId uuid N 사무소 FK. null이면 시스템 기본 그룹
code varchar(80) Y OWNER_ADMIN_DEFAULT, AGENT_DEFAULT 등
name varchar(100) Y 권한 그룹명
description text N 설명
scope PermissionScope Y SYSTEM 또는 OFFICE
permissionsJson jsonb Y 기능별 권한
createdByUserId uuid N 생성한 사무소 사용자
createdByHeadquartersUserId uuid N 생성한 본사관리자
createdAt, updatedAt, deletedAt timestamptz Y/N 생성/수정/삭제 일시

권장 permissionsJson 예시:

{
  "contracts": { "read": "own", "create": true, "update": "own", "delete": false, "export": false },
  "customers": { "read": "own", "create": true, "update": "own", "delete": false, "export": false },
  "listings": { "read": "own", "create": true, "update": "own", "delete": false, "exchange": false },
  "users": { "read": false, "create": false, "update": false },
  "billing": { "read": false },
  "features": { "manage": false }
}

인덱스:

  • unique (officeId, code) where officeId is not null
  • unique (code) where officeId is null
  • index idx_permission_groups_scope(scope)

5. 기능 권한과 과금

5.1 features

기능 권한 및 추가 과금 기준의 마스터 테이블이다.

필드 타입 필수 설명
id uuid Y PK
code FeatureCode Y 기능 코드, unique
name varchar(100) Y 기능명
description text N 설명
defaultMonthlyFee integer Y 기본 월 과금액, 원 단위
defaultEnabled boolean Y 기본 제공 여부
status varchar(30) Y ACTIVE, INACTIVE
createdAt, updatedAt timestamptz Y 생성/수정 일시

인덱스:

  • unique features_code_key(code)
  • index idx_features_status(status)

5.2 office_features

사무소별 기능 사용 가능 여부와 월 과금액을 저장한다.

필드 타입 필수 설명
id uuid Y PK
officeId uuid Y 사무소 FK
featureId uuid Y 기능 FK
enabled boolean Y 사용 가능 여부
monthlyFee integer Y 사무소별 적용 금액
effectiveFrom date Y 적용 시작일
effectiveTo date N 적용 종료일
memo text N 상담/예외 메모
changedByHeadquartersUserId uuid Y 변경한 본사관리자
createdAt, updatedAt timestamptz Y 생성/수정 일시

제약:

  • 같은 officeId + featureId에서 effectiveTo is null인 현재 정책은 1개만 허용한다.

인덱스:

  • index idx_office_features_office_enabled(officeId, enabled)
  • index idx_office_features_feature(featureId)
  • unique partial office_features_current_key(officeId, featureId) where effectiveTo is null

5.3 office_billing_policies

사무소별 기본 과금, 등록 가능 인원, 무료 인원, 초과 인원 단가를 이력형으로 저장한다.

필드 타입 필수 설명
id uuid Y PK
officeId uuid Y 사무소 FK
baseMonthlyFee integer Y 기본 월 이용료
maxUserLimit integer Y 등록 가능 총 인원
includedUserLimit integer Y 무료/기본 포함 인원
extraUserFee integer Y 초과 인원 1명당 월 과금
billingEnabled boolean Y 실제 과금 여부
effectiveFrom date Y 적용 시작일
effectiveTo date N 적용 종료일
memo text N 정책 메모
changedByHeadquartersUserId uuid Y 변경한 본사관리자
createdAt, updatedAt timestamptz Y 생성/수정 일시

제약:

  • maxUserLimit >= includedUserLimit
  • baseMonthlyFee >= 0, extraUserFee >= 0
  • 같은 사무소의 현재 정책은 1개만 허용한다.

인덱스:

  • index idx_office_billing_policies_office_effective(officeId, effectiveFrom desc)
  • unique partial office_billing_policies_current_key(officeId) where effectiveTo is null

5.4 billing_monthly_usages

월별 예상 또는 확정 과금 스냅샷이다. 실제 결제 자동화 전에도 본사 화면의 예상 과금 계산에 사용한다.

필드 타입 필수 설명
id uuid Y PK
officeId uuid Y 사무소 FK
billingMonth char(7) Y YYYY-MM
baseFee integer Y 기본 요금
activeUserCount integer Y 활성 사용자 수
billableUserCount integer Y 과금 대상 사용자 수
includedUserLimit integer Y 포함 인원
extraUserCount integer Y 초과 인원 수
extraUserAmount integer Y 초과 인원 금액
featureAmount integer Y 추가 기능 금액
totalAmount integer Y 합계
billingEnabled boolean Y 실제 과금 여부
status BillingMonthStatus Y 초안/확정/취소
calculatedAt timestamptz Y 계산 시각
confirmedAt timestamptz N 확정 시각
confirmedByHeadquartersUserId uuid N 확정한 본사관리자
snapshotJson jsonb Y 계산 근거 스냅샷
createdAt, updatedAt timestamptz Y 생성/수정 일시

인덱스:

  • unique billing_monthly_usages_office_month_key(officeId, billingMonth)
  • index idx_billing_monthly_usages_month_status(billingMonth, status)

6. 계약

6.1 contracts

계약관리 목록의 기준 테이블이다.

필드 타입 필수 설명
id uuid Y PK
officeId uuid Y 사무소 FK
contractNo varchar(80) Y 사무소별 계약번호
writerUserId uuid Y 최초 작성자
managerUserId uuid Y 주 담당자
listingId uuid N 연결 매물
type ContractType Y 계약서 종류
status ContractStatus Y 상태
title varchar(200) N 표시용 제목
contractDate date N 계약일
startDate date N 임대차 시작일 등
endDate date N 만기일
balanceDueDate date N 잔금일
amountJson jsonb N 보증금/매매가/월세 등 금액 구조
latestVersionNo integer Y 최신 버전 번호
templateCode varchar(80) Y 계약서 템플릿 코드
templateVersion integer Y 저장 당시 템플릿 버전
createdAt, updatedAt, deletedAt timestamptz Y/N 생성/수정/삭제 일시

제약:

  • unique (officeId, contractNo)
  • writerUserId, managerUserId, listingId는 같은 officeId에 속해야 한다.

인덱스:

  • index idx_contracts_office_status_date(officeId, status, contractDate desc)
  • index idx_contracts_office_manager(officeId, managerUserId, updatedAt desc)
  • index idx_contracts_office_writer(officeId, writerUserId, updatedAt desc)
  • index idx_contracts_office_end_date(officeId, endDate)
  • index idx_contracts_office_balance_due_date(officeId, balanceDueDate)
  • index idx_contracts_listing(listingId)

6.2 contract_versions

계약 입력값과 특약사항의 버전 이력이다. PDF 재생성은 이 테이블의 특정 버전 데이터를 사용한다.

필드 타입 필수 설명
id uuid Y PK
contractId uuid Y 계약 FK
versionNo integer Y 버전 번호
dataJson jsonb Y 계약서 구조화 입력값
specialTerms text N 특약사항
changeMemo text N 변경 사유
createdByUserId uuid Y 생성한 사무소 사용자
createdAt timestamptz Y 생성 일시

인덱스:

  • unique contract_versions_contract_version_key(contractId, versionNo)
  • index idx_contract_versions_contract_created(contractId, createdAt desc)

6.3 contract_parties

계약 당사자와 고객관리 데이터 연결 테이블이다.

필드 타입 필수 설명
id uuid Y PK
contractId uuid Y 계약 FK
customerId uuid N 고객 FK
partyType ContractPartyType Y 매도인/매수인/임대인/임차인 등
name varchar(100) Y 성명
phone varchar(30) N 연락처
address varchar(500) N 주소
identityRef varchar(255) N 암호화 주민번호/사업자번호 참조값
sortOrder integer Y 표시 순서
createdAt, updatedAt timestamptz Y 생성/수정 일시

제약:

  • customerId가 있으면 계약의 officeId와 고객의 officeId가 같아야 한다.

인덱스:

  • index idx_contract_parties_contract(contractId, sortOrder)
  • index idx_contract_parties_customer(customerId)
  • index idx_contract_parties_phone(phone)

7. 고객

7.1 customer_groups

사무소별 고객 그룹이다.

필드 타입 필수 설명
id uuid Y PK
officeId uuid Y 사무소 FK
name varchar(100) Y 그룹명
description text N 설명
createdAt, updatedAt, deletedAt timestamptz Y/N 생성/수정/삭제 일시

인덱스:

  • unique (officeId, name)

7.2 customers

고객관리의 기준 테이블이다.

필드 타입 필수 설명
id uuid Y PK
officeId uuid Y 사무소 FK
managerUserId uuid Y 담당자 FK
groupId uuid N 고객 그룹 FK
name varchar(100) Y 고객명
phone varchar(30) N 대표 연락처
email varchar(255) N 이메일
address1 varchar(255) N 주소
address2 varchar(255) N 상세 주소
grade varchar(50) N 고객 등급
requestType varchar(80) N 의뢰 유형
status CustomerStatus Y 업무 상태
memo text N 요약 메모
sourceContractId uuid N 계약 작성 중 자동 생성된 경우 원 계약
createdByUserId uuid Y 등록자
createdAt, updatedAt, deletedAt timestamptz Y/N 생성/수정/삭제 일시

제약:

  • managerUserId, groupId, sourceContractId는 같은 officeId에 속해야 한다.

인덱스:

  • index idx_customers_office_manager_status(officeId, managerUserId, status)
  • index idx_customers_office_name(officeId, name)
  • index idx_customers_office_phone(officeId, phone)
  • index idx_customers_group(groupId)

7.3 customer_memos

고객별 상담/업무 메모 이력이다.

필드 타입 필수 설명
id uuid Y PK
customerId uuid Y 고객 FK
officeId uuid Y 사무소 FK, 조회 필터 최적화용
body text Y 메모 내용
createdByUserId uuid Y 작성자
createdAt, updatedAt, deletedAt timestamptz Y/N 생성/수정/삭제 일시

인덱스:

  • index idx_customer_memos_customer_created(customerId, createdAt desc)
  • index idx_customer_memos_office_created(officeId, createdAt desc)

8. 매물

8.1 listings

매물관리의 기준 테이블이다.

필드 타입 필수 설명
id uuid Y PK
officeId uuid Y 등록 사무소 FK
managerUserId uuid Y 담당자 FK
createdByUserId uuid Y 등록자 FK
type ListingType Y 매물 유형
dealType ListingDealType Y 거래 유형
status ListingStatus Y 상태
visibility ListingVisibility Y 비공개/사무소공개/공동중개공개
exchangeEnabled boolean Y 공동중개 가능 여부
title varchar(200) N 표시용 제목
address1 varchar(255) Y 주소
address2 varchar(255) N 상세 주소
legalDongCode varchar(20) N 법정동 코드
priceJson jsonb Y 매매가/보증금/월세 등
areaJson jsonb N 전용/공급/대지 면적 등
detailJson jsonb N 층/방/욕실/입주가능일 등
memo text N 내부 메모
publicMemo text N 공개 설명
createdAt, updatedAt, deletedAt timestamptz Y/N 생성/수정/삭제 일시

제약:

  • managerUserId, createdByUserId는 같은 officeId에 속해야 한다.
  • visibility = EXCHANGE이면 exchangeEnabled = true여야 한다.

인덱스:

  • index idx_listings_office_manager_status(officeId, managerUserId, status)
  • index idx_listings_office_visibility_status(officeId, visibility, status)
  • index idx_listings_exchange_status(visibility, exchangeEnabled, status)
  • index idx_listings_address(officeId, address1)
  • GIN index idx_listings_price_json(priceJson) 필요 시 추가

8.2 listing_exchanges

공동중개/매물 교환 공개 범위를 확장하기 위한 준비 테이블이다.

필드 타입 필수 설명
id uuid Y PK
listingId uuid Y 매물 FK
ownerOfficeId uuid Y 매물 소유 사무소
sharedToOfficeId uuid N 특정 공유 대상 사무소. null이면 전체 공개 후보
status ListingExchangeStatus Y 요청/승인/거절/철회/만료
requestedByUserId uuid N 요청자
approvedByUserId uuid N 승인자
requestedAt timestamptz N 요청 일시
approvedAt timestamptz N 승인 일시
expiresAt timestamptz N 만료 일시
memo text N 메모
createdAt, updatedAt timestamptz Y 생성/수정 일시

제약:

  • ownerOfficeIdlisting.officeId와 같아야 한다.
  • sharedToOfficeId가 null이면 전체 공개 정책으로 해석한다.

인덱스:

  • index idx_listing_exchanges_listing_status(listingId, status)
  • index idx_listing_exchanges_shared_office_status(sharedToOfficeId, status)
  • unique partial listing_exchanges_target_key(listingId, sharedToOfficeId) where status in ('REQUESTED', 'APPROVED')

9. 알림과 팝업

9.1 notifications

계약 만료 알림 등 발송 예약/이력이다. 실제 카카오 발송은 2차라도 스키마는 준비한다.

필드 타입 필수 설명
id uuid Y PK
officeId uuid Y 사무소 FK
contractId uuid N 계약 FK
customerId uuid N 고객 FK
recipientType varchar(50) Y CUSTOMER, MANAGER 등
recipientPhone varchar(30) Y 수신 전화번호
messageTemplateCode varchar(80) N 템플릿 코드
payloadJson jsonb N 발송 데이터
remindAt timestamptz Y 발송 예정 시각
status NotificationStatus Y 예정/성공/실패/취소
sentAt timestamptz N 발송 시각
failureReason text N 실패 사유
createdAt, updatedAt timestamptz Y 생성/수정 일시

인덱스:

  • index idx_notifications_office_status_remind(officeId, status, remindAt)
  • index idx_notifications_contract(contractId)
  • index idx_notifications_customer(customerId)

9.2 popup_campaigns

고객 예약 팝업 캠페인이다.

필드 타입 필수 설명
id uuid Y PK
officeId uuid Y 사무소 FK
title varchar(150) Y 제목
message text Y 내용
targetRuleJson jsonb Y 고객/그룹/조건 대상 규칙
showAt timestamptz N 단일 노출 시각
repeatRuleJson jsonb N 반복 규칙
status PopupCampaignStatus Y 상태
createdByUserId uuid Y 생성자
createdAt, updatedAt, deletedAt timestamptz Y/N 생성/수정/삭제 일시

인덱스:

  • index idx_popup_campaigns_office_status(officeId, status)
  • GIN index idx_popup_campaigns_target_rule(targetRuleJson)

9.3 popup_deliveries

사용자별 팝업 노출/확인/닫기 이력이다.

필드 타입 필수 설명
id uuid Y PK
campaignId uuid Y 캠페인 FK
officeId uuid Y 사무소 FK
customerId uuid N 대상 고객
userId uuid Y 팝업을 본 사용자
shownAt timestamptz N 노출 시각
clickedAt timestamptz N 클릭 시각
dismissedAt timestamptz N 닫은 시각
createdAt timestamptz Y 생성 일시

인덱스:

  • index idx_popup_deliveries_user_created(userId, createdAt desc)
  • index idx_popup_deliveries_campaign(campaignId)
  • unique popup_deliveries_campaign_user_customer_key(campaignId, userId, customerId)

10. 감사 로그

10.1 audit_logs

개인정보, 권한, 과금, 데이터 내보내기 작업을 추적한다.

필드 타입 필수 설명
id uuid Y PK
actorType varchar(30) Y HEADQUARTERS_USER 또는 OFFICE_USER
headquartersUserId uuid N 본사 행위자
officeUserId uuid N 사무소 행위자
officeId uuid N 대상 사무소
action AuditAction Y 행위
resourceType varchar(80) Y CONTRACT, CUSTOMER 등
resourceId uuid N 대상 ID
ipAddress inet N IP
userAgent text N User-Agent
metadataJson jsonb N 추가 정보
createdAt timestamptz Y 생성 일시

인덱스:

  • index idx_audit_logs_office_created(officeId, createdAt desc)
  • index idx_audit_logs_actor_created(actorType, headquartersUserId, officeUserId, createdAt desc)
  • index idx_audit_logs_resource(resourceType, resourceId)

11. 핵심 관계 요약

headquarters_users
  ├─ offices.createdByHeadquartersUserId
  ├─ office_billing_policies.changedByHeadquartersUserId
  └─ office_features.changedByHeadquartersUserId

offices
  ├─ office_users
  ├─ permission_groups
  ├─ office_billing_policies
  ├─ office_features ─ features
  ├─ contracts ─ contract_versions
  │             └─ contract_parties ─ customers
  ├─ customers ─ customer_groups
  ├─ listings ─ listing_exchanges
  ├─ notifications
  ├─ popup_campaigns ─ popup_deliveries
  └─ billing_monthly_usages

12. Prisma 변환 메모

  • PostgreSQL jsonb는 Prisma Json으로 매핑한다.
  • PostgreSQL inet은 Prisma에서 String으로 시작하고, 필요 시 raw SQL migration으로 타입을 조정한다.
  • partial unique index, GIN index, enum 변경은 Prisma schema만으로 표현이 제한될 수 있으므로 migration SQL에 별도 작성한다.
  • 같은 사무소 소속 검증은 DB FK만으로 완전 보장하기 어렵다. 애플리케이션 서비스 레이어에서 officeId 일치 검사를 공통 함수로 강제한다.
  • deletedAt is null 조건이 자주 붙는 목록 조회는 향후 partial index를 추가한다.