데이터 모델
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
| 컬렉션 | owner | staff | 비고 |
|---|---|---|---|
| tenants | 읽기/수정 | 읽기 | 삭제 불가 (Admin SDK) |
| cases | 읽기/쓰기 | 읽기/쓰기 | |
| hearings, recoveries, evidence, logs | 읽기/쓰기 | 읽기/쓰기 | 사건 하위 |
| comments | 읽기/쓰기 | 읽기/쓰기 | 내부 노트 (의뢰인 비공개) |
| documents | 읽기/쓰기 | 읽기/쓰기 | 테넌트 레벨 |
| members | CRUD | 읽기 | 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)로 조회.
| 항목 | Free | Pro |
|---|---|---|
| 사건 한도 | 20건 | 무제한 |
| AI 서류 생성 | 월 3건 | 무제한 |
| AI 브리핑 | 월 1회 | 무제한 |
| 구성원 | 3명 | 무제한 |
| 레거시 아카이브 | 불가 (0건) | 무제한 |
- 무제한 =
UNLIMITED = 999_999센티널 값 (Firestore에 Infinity 저장 불가) freePeriodEndDate가 미래이면 Free 플랜에도 Pro 한도 적용- 가격: Pro 월 300,000원 (부가세 별도)
AI 기능 플래그
config/appMetadata 문서의 features에서 관리.
| 플래그 | 기본값 | 설명 |
|---|---|---|
features.ai.enabled | true | AI 전체 활성화 |
features.ai.docGeneration | true | AI 서류 생성 |
features.ai.briefing | true | 대시보드 브리핑 |
features.ai.ragSearch | true | RAG 검색 |
features.ai.strategyReport | false | 전략 보고서 (단계적 배포) |
features.ocr.enabled | true | OCR 파이프라인 |
계획된 변경사항
의뢰인 포털 재설계 (구현 완료)
- ✅
portalInvites→portalTokens로 대체 (사건 단위 토큰) - ✅
messages→cases/{caseId}/messages로 이동 (사건별 대화) - ✅
clientId필드 제거 (Firebase Auth UID 불필요) - ✅
UserRole에서"client"제거 → iron-session 포털 세션으로 전환 - ✅ 카카오/네이버 OAuth 본인확인 + OTP 폴백
- 상세: 포털 재설계 문서, OAuth 설정 가이드