본문으로 건너뛰기

데이터 모델

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-assists ← AI 어시스트 통합 월간 카운터 (문서 생성·사무실 기억 분석·전략 보고서 등 weight 0/1/3 차감)
├── dailyBriefings/{YYYY-MM-DD} ← 대시보드 브리핑 공유 캐시 (하루 1회 Gemini, #83)
├── docgenEvents/{eventId} ← 서류 생성 텔레메트리 (ADR 0012 S1, 성공·실패·kill 3 경로)
├── relatedMemoriesEvents/{eventId} ← RelatedMemoriesPanel 텔레메트리 (ADR 0012 S1 B, 3 이벤트 타입)
├── docxTemplates/{kind}.docx ← tenant custom 서식 템플릿 (Storage, owner 업로드)
├── 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 포함)
├── generatedDocs/{caseId}/{kind}-{uuid}.docx ← AI 생성 .docx (ADR 0012 S1, Signed URL 15분)
├── docxTemplates/{kind}.docx ← tenant custom 서식 템플릿 (owner 업로드)
└── public/docxTemplates/{kind}.docx ← 공용 기본 서식 (운영팀 관리)

핵심 엔티티

Tenant (사무소)

tenants/{tenantId}
├── name: string — 사무소명
├── ownerUid: string — 생성자 UID
├── ownerName?: string — 대표 이름
├── phone?: string — 연락처
├── email?: string — 이메일
├── address?: string — 주소
├── licenseType?: string — "법무사" | "변호사"
├── licenseNumber?: string
├── logoUrl?: string — 로고 이미지 URL
├── plan: UserPlan — "registered" 단일 tier (ADR 0002 전 사용자 무료, tier 차등 없음)
├── planOverrides?: { assistLimit?: number } — 운영팀 재량 한도 override (어뷰징 방어 한정)
├── aiBanned?: boolean — AI 호출 즉시 차단 플래그 (어뷰징 방어, audit log 필수)
├── stats?: — KPI 사전 집계 (aggregateCaseStats Cloud Function이 자동 갱신)
│ ├── totalCases: number
│ ├── activeCases: number
│ ├── totalAmount: number
│ ├── recoveredAmount: number
│ ├── statusDistribution: { 소제기, 진행중, 판결대기, 집행중, 종결: number }
│ └── updatedAt: Timestamp
├── createdAt: Timestamp
└── updatedAt?: Timestamp

ADR 0002 (전 사용자 무료) 후 제거된 유료 tier 필드: planExpiresAt · planSource · promoCode · promoExpiresAt · subscription · subscriptionRequest 는 타입(apps/web/types/tenant.ts)에서 삭제됨. 레거시 Firestore 문서에 잔존할 수 있으나 런타임은 참조하지 않는다 (Phase 1 후속 마이그레이션에서 필드 자체 제거 예정). 신규 기획·코드에서 "유료 전환" · "Pro tier" 개념 사용 금지.

Case (사건)

tenants/{tenantId}/cases/{caseId}
├── caseNumber: string — 내부 사건번호 (YYMM-NNN)
├── courtCaseNumber?: string — 법원 사건번호
├── status: string — "소제기" | "진행중" | "판결대기" | "집행중" | "종결"
├── recoveryType: string — 14종 중 1 (아래 Pack 매핑 표 참조). 기본 "debt"
├── clientName: string — 의뢰인 이름
├── clientPhone?: string — 의뢰인 전화번호
├── clientEmail?: string — 의뢰인 이메일
├── opponentName: string — 상대방
├── principal: number — 원금
├── totalAmount: number — 청구 총액 (NON_DEBT 8종은 0 — calcLoanAmounts 우회)
├── interestRate?: number — 이자율
├── assignee?: string — 담당자
├── court?: string — 관할 법원
├── createdAt: Timestamp
└── updatedAt?: Timestamp

RecoveryType — Pack 1~5 14종 매핑

recoveryType 은 사건의 도메인을 결정하고, 금액 계산 분기 + wizard UI 분기 + AI 프롬프트 컨텍스트 의 SSoT.

PackrecoveryType한글명채권 계산ADR
Pack 1 — 채권 회수debt대여금calcLoanAmounts0003·0010
construction공사대금0010
subrogation구상금0010
agreement약정금0010
lease-deposit임대차보증금 반환0010
assigned양수금0010
Pack 2 — 가사divorce이혼❌ NON_DEBT0022
Pack 3 — 부동산real-estate-eviction명도청구❌ NON_DEBT0023
rent-arrears차임 청구❌ NON_DEBT0023
title-transfer이전등기청구❌ NON_DEBT0023
Pack 4 — 상속inheritance-share상속분 청구❌ NON_DEBT0026
inheritance-division유산분할❌ NON_DEBT0026
Pack 5 — 계약unjust-enrichment부당이득❌ NON_DEBT0027
contract-damages계약 위반 손해배상❌ NON_DEBT0027
  • 채권 계산 (✅) = packages/business-logic/src/cases/loan-amounts.tscalcLoanAmounts (이자·지연손해금) 적용. Pack 1 6종만.
  • NON_DEBT (❌) = 8종. packages/business-logic/src/cases/create.tsNON_DEBT_RECOVERY_TYPES Set 으로 분기. totalAmount=0, interestAmount=0, delayDamage=0 으로 저장하고 도메인별 별도 필드 (혼인기간·차임·상속분·계약위반사유 등) 사용.
  • 타입 정의 SSoT = apps/web/app/(workspace)/cases/[caseId]/_lib/recovery-input-schemas.tsRECOVERY_TYPES 배열 + 파생 RecoveryType union. 추가·변경 시 본 표 + NON_DEBT_RECOVERY_TYPES Set + 해당 Pack 의 ADR 동시 갱신.

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" — 설정, 멤버 관리, 플랜 변경

한도 (단일 tier)

config/appMetadata.planLimits.registered 에서 관리. 서버에서 getEffectivePlan(plan, _unused, metadata) 또는 getEffectivePlanForSession(session) 로 조회.

ADR 0002 (전 사용자 무료, 2026-04-19) 이후 플랜 tier 차별 · 트라이얼 · grace 개념 소멸. 모든 tenant 는 동일한 운영 안정성 한도 (어뷰징 방어) 만 공유한다.

항목한도
활성 사건200건
AI 월간 호출60회 (단일 카운터, weight 1·3·0 합산)
구성원15명
레거시 문서 DB100건
  • 한도 초과 시 PLAN_LIMIT_EXCEEDED 에러 반환 (트라이얼·grace 차단은 제거)
  • 한도 값은 Firestore 에서만 변경 가능 — 코드 DEFAULT_METADATA.planLimits.registered 는 fallback 용
  • ADR 0002 (2026-04-19) 이후 3-tier·트라이얼·PLAN_READ_ONLY 차단 설계는 삭제. 과거 설계는 git history 참조.

AI 기능 플래그

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

플래그기본값설명
features.ai.enabledtrueAI 전체 활성화
features.ai.docGenerationtrueAI 서류 생성
features.ai.briefingtrue대시보드 브리핑
features.ai.ragSearchtrueRAG 사무실 기억 검색
features.ai.strategyReportfalse전략 보고서 (단계적 배포)

계획된 변경사항

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

  • portalInvitesportalTokens로 대체 (사건 단위 토큰)
  • messagescases/{caseId}/messages로 이동 (사건별 대화)
  • clientId 필드 제거 (Firebase Auth UID 불필요)
  • UserRole에서 "client" 제거 → iron-session 포털 세션으로 전환
  • ✅ 4자리 접속 코드 단일 경로 본인확인 (변호사가 링크·코드를 직접 전달)
  • ✅ 접속 코드 오입력 누적 시 토큰 자동 revoke + 3경로 대응 (인앱 알림, /activity 감사 로그, 사건 상세 PortalLockoutBanner)
  • NotificationType / ActivityTypeportal_access_code_locked 신설 (actorId: "system" 관례)
  • 상세: 포털 재설계 문서