DB 스키마 v1
1. 설계 원칙
- DB는 PostgreSQL을 기준으로 설계하고, Prisma 모델로 옮기기 쉬운 테이블 단위 구조를 사용한다.
- 본사관리자 계정과 부동산 사무소 사용자는 인증 영역과 테이블을 분리한다.
- 사무소 업무 데이터는 반드시
officeId를 가진다. - 일반 업무 사용자의 데이터 범위는
writerUserId,managerUserId,createdByUserId,assigneeUserId등 담당자 필드로 제한한다. - 모든 주요 테이블은
id,createdAt,updatedAt을 가진다. - 업무 데이터는 삭제보다
status,deletedAt기반 비활성/소프트 삭제를 우선한다. - 개인정보 접근, 계약 PDF 생성, 과금 계산처럼 민감하거나 감사가 필요한 작업은 별도 로그를 남긴다.
2. 공통 타입
2.1 ID와 시간
id: UUID, primary keycreatedAt: timestamptz, default nowupdatedAt: timestamptz, default nowdeletedAt: 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 | 이름 |
| 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_usersoffices 1:N contractsoffices 1:N customersoffices 1:N listingsoffices 1:N office_billing_policiesoffices 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 | 이름 |
| 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)whereofficeId is not null - unique
(code)whereofficeId 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)whereeffectiveTo 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 >= includedUserLimitbaseMonthlyFee >= 0,extraUserFee >= 0- 같은 사무소의 현재 정책은 1개만 허용한다.
인덱스:
- index
idx_office_billing_policies_office_effective(officeId, effectiveFrom desc) - unique partial
office_billing_policies_current_key(officeId)whereeffectiveTo 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 | 대표 연락처 |
| 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 | 생성/수정 일시 |
제약:
ownerOfficeId는listing.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)wherestatus 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는 PrismaJson으로 매핑한다. - 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를 추가한다.