본문으로 건너뛰기

데이터 모델

Firestore 기반 멀티테넌트 데이터 모델. 경로 기반 격리 (Path-based isolation).

핵심 원칙

모든 데이터는 tenants/{tenantId} 하위에 격리됩니다. 테넌트 간 데이터 교차 접근은 불가능합니다.

엔터티 관계도

전체 구조

Firestore
├── config/
│ └── appMetadata ← 글로벌 설정 (플랜 한도, 기능 플래그, 점검 모드)

├── promoCodes/{code} ← 프로모션 코드 (루트 레벨)

└── tenants/{tenantId}/ ← 테넌트 (사무소) 단위 격리
├── members/{userId} ← 구성원 (owner, staff)
├── activityLogs/{logId} ← 활동 감사 로그
├── notifications/{notifId} ← 인앱 알림
├── documents/{docId} ← AI 생성/업로드 서류
├── sentDocuments/{sentId} ← 발송된 서류
├── messages/{msgId} ← 의뢰인-사무소 메시지 (레거시, case-level로 이동 중)
├── fees/{feeId} ← 수임료 청구서
├── payments/{orderId} ← 결제 내역
├── portalTokens/{token} ← 포털 인증 토큰 (portalInvites 대체)
├── portalInvites/{token} ← 포털 초대 토큰 (레거시, 삭제 예정)
├── legacyDocuments/{docId} ← 종결 사건 OCR 아카이브 (Pro)
├── counters/ai-usage ← AI 월간 사용량 카운터
├── meta/caseCounters ← 사건번호 월별 채번 카운터

└── cases/{caseId}/ ← 사건
├── hearings/{id} ← 기일
├── recoveries/{id} ← 변제 내역
├── evidence/{id} ← 증거자료
├── comments/{id} ← 내부 노트 (의뢰인 비공개)
├── strategyReports/{id} ← AI 전략 보고서
├── messages/{id} ← 의뢰인↔변호사 사건별 메시지
└── logs/{id} ← 사건 로그

Cloud Storage 경로

tenants/{tenantId}/
├── evidence/{caseId}/{fileName} ← 증거파일
├── documents/{docId}.html ← 서류 HTML
├── documents/{fileName} ← 업로드 서류
└── legacy-ocr/{legacyDocId}/original.txt ← OCR 원본 (PII 포함)

핵심 엔티티

Tenant (사무소)

tenants/{tenantId}
├── name: string — 사무소명
├── ownerUid: string — 생성자 UID
├── ownerName?: string — 대표 이름
├── phone?: string — 연락처
├── email?: string — 이메일
├── address?: string — 주소
├── licenseType?: string — "법무사" | "변호사"
├── licenseNumber?: string
├── logoUrl?: string — 로고 이미지 URL
├── plan: string — "free" | "pro"
├── planExpiresAt?: Timestamp
├── createdAt: Timestamp
└── updatedAt?: Timestamp

Case (사건)

tenants/{tenantId}/cases/{caseId}
├── caseNumber: string — 내부 사건번호 (YYMM-NNN)
├── courtCaseNumber?: string — 법원 사건번호
├── status: string — "소제기" | "진행중" | "판결대기" | "집행중" | "종결"
├── clientName: string — 의뢰인 이름
├── clientPhone?: string — 의뢰인 전화번호
├── clientEmail?: string — 의뢰인 이메일
├── opponentName: string — 상대방
├── principal: number — 원금
├── totalAmount: number — 청구 총액
├── interestRate?: number — 이자율
├── assignee?: string — 담당자
├── court?: string — 관할 법원
├── createdAt: Timestamp
└── updatedAt?: Timestamp

Member (구성원)

tenants/{tenantId}/members/{userId}
├── tenantId: string
├── name: string
├── email: string
├── role: string — "owner" | "staff"
├── status: string — "active" | "pending" | "inactive"
├── joinedAt: Timestamp
└── lastActiveAt?: Timestamp

Document (서류)

tenants/{tenantId}/documents/{docId}
├── name: string — 서류명
├── type: string — "지급명령" | "소장" | "내용증명" | "강제집행신청서" | ...
├── caseId: string — 연결된 사건 ID
├── content?: string — HTML 본문
├── storageUrl?: string — Storage 파일 URL
├── storagePath?: string — Storage 경로
├── status: string — "생성중" | "완료" | "발송됨"
├── aiGenerated: boolean — AI 생성 여부
├── createdAt: Timestamp
└── updatedAt?: Timestamp

접근 제어

Firestore Security Rules

컬렉션ownerstaff비고
tenants읽기/수정읽기삭제 불가 (Admin SDK)
cases읽기/쓰기읽기/쓰기
hearings, recoveries, evidence, logs읽기/쓰기읽기/쓰기사건 하위
comments읽기/쓰기읽기/쓰기내부 노트 (의뢰인 비공개)
documents읽기/쓰기읽기/쓰기테넌트 레벨
membersCRUD읽기owner만 멤버 추가/수정/삭제
notifications읽기 + readBy 업데이트읽기 + readBy 업데이트생성/삭제는 Admin SDK
fees, payments읽기/쓰기읽기/쓰기의뢰인 비공개
legacyDocuments읽기읽기쓰기는 Cloud Functions
portalTokens읽기읽기생성/수정은 Admin SDK
portalInvites읽기읽기레거시, 삭제 예정
promoCodes불가불가Admin SDK 전용
config/appMetadata읽기읽기쓰기는 Admin SDK (콘솔)

Server Action 권한 체크

  • getServerSession() — 토큰 서명 검증 + Custom Claims 확인
  • requireStaffSession() — owner/staff만 허용
  • session.role === "owner" — 설정, 멤버 관리, 플랜 변경

플랜별 한도

config/appMetadata 문서의 planLimits에서 관리. 서버에서 getEffectivePlanLimits(plan, metadata)로 조회.

항목FreePro
사건 한도20건무제한
AI 서류 생성월 3건무제한
AI 브리핑월 1회무제한
구성원3명무제한
레거시 아카이브불가 (0건)무제한
  • 무제한 = UNLIMITED = 999_999 센티널 값 (Firestore에 Infinity 저장 불가)
  • freePeriodEndDate가 미래이면 Free 플랜에도 Pro 한도 적용
  • 가격: Pro 월 300,000원 (부가세 별도)

AI 기능 플래그

config/appMetadata 문서의 features에서 관리.

플래그기본값설명
features.ai.enabledtrueAI 전체 활성화
features.ai.docGenerationtrueAI 서류 생성
features.ai.briefingtrue대시보드 브리핑
features.ai.ragSearchtrueRAG 검색
features.ai.strategyReportfalse전략 보고서 (단계적 배포)
features.ocr.enabledtrueOCR 파이프라인

계획된 변경사항

의뢰인 포털 재설계 (구현 완료)

  • portalInvitesportalTokens로 대체 (사건 단위 토큰)
  • messagescases/{caseId}/messages로 이동 (사건별 대화)
  • clientId 필드 제거 (Firebase Auth UID 불필요)
  • UserRole에서 "client" 제거 → iron-session 포털 세션으로 전환
  • ✅ 카카오/네이버 OAuth 본인확인 + OTP 폴백
  • 상세: 포털 재설계 문서, OAuth 설정 가이드