본문으로 건너뛰기

Pack 1 데이터 모델

기반 ADR: 0003 §4 데이터 모델 · 0002 §5 사무소 자산 · 0005 §7 Vector Search

전체 구조

Pack 1 은 기존 tenants/{tid}/cases/{caseId} 하위에 5개 신규 서브컬렉션 + 기존 legacyDocuments 연계 + auditLog 확장.

Mermaid ER 다이어그램

Firestore 컬렉션 경로

tenants/{tid}/
├── cases/{caseId}/
│ ├── loans/{loanId}
│ ├── repayments/{repaymentId}
│ ├── interestRates/{rateId}
│ ├── settlements/{settlementId}
│ ├── scenarios/{scenarioId}
│ ├── aiDrafts/{draftId}
│ │ └── revisions/{revisionId}
│ └── signEvents/{eventId}

├── legacyDocuments/{docId}
│ └── referencedBy/{draftId}

├── auditLog/{logId}
│ ├── /ai/{logId} ← AI 호출
│ ├── /rag/{logId} ← RAG 쿼리
│ ├── /docSign/{logId} ← 변호사 서명 이벤트
│ ├── /winrate/{logId} ← 승소 통계 조회 (3년 보존)
│ └── /redaction/{logId} ← 비식별화

├── axMetrics ← 단일 문서 (집계)
├── assistCounters/ai-assists ← 단일 문서 (weight 합산)
└── consentHistory/{historyId} ← 의뢰인·변호사 동의 이력

스키마 정의

Loan (대여)

cases/{caseId}/loans/{loanId}

interface Loan {
id: string;
principal: number; // 정수 원 (KRW)
lentAt: Timestamp; // KST
dueAt: Timestamp | null; // 이행기
method: "A" | "B"; // 개별 충당 / 잔액 정산 (Phase 1 B 활성)
note?: string;
evidenceIds?: string[]; // 차용증·계약서 증거 ref

// 확장 슬롯 (Phase 0 예약, Phase 1+ 활용)
coObligors?: string[]; // 연대채무자 user refs
guarantors?: string[]; // 보증인 user refs
assignedFrom?: string; // 채권양도 양도인
assignedAt?: Timestamp;

// 메타
createdAt: Timestamp;
createdBy: string; // user uid
updatedAt: Timestamp;
updatedBy: string;
}

Repayment (변제)

cases/{caseId}/repayments/{repaymentId}

interface Repayment {
id: string;
loanId?: string; // 특정 채권 지정 (없으면 법정충당 자동)
amount: number; // 정수 원
paidAt: Timestamp; // KST
appropriationOverride?: "principal" | "interest" | null; // 지정충당 (민법 476조)
source?: "계좌" | "현금" | "상계" | "공탁" | "대물변제";
evidenceIds?: string[];
statuteOfLimitationEffect?: boolean; // 시효 중단 플래그 (민법 168조 승인)

// CSV 파싱 소스 추적
importSource?: {
bankId: string;
sourceHash: string; // (거래일+금액+적요) 해시
parsedAt: Timestamp;
classification: "repayment" | "unclassified";
feeAmount?: number;
};

// 메타
createdAt: Timestamp;
createdBy: string;
updatedAt: Timestamp;
}

InterestRate (이자율 이력)

cases/{caseId}/interestRates/{rateId}

interface InterestRate {
id: string;
loanId?: string | null; // null = 모든 채권 공통
rate: number; // basis points (정수) e.g. 1200 = 12%
basis: "agreed" | "statutory" | "delay";
effectiveFrom: Timestamp;
effectiveTo: Timestamp | null;
compounding: "simple" | "monthly" | "annual";

createdAt: Timestamp;
createdBy: string;
}

SettlementSnapshot (정산 스냅샷)

cases/{caseId}/settlements/{settlementId}

interface SettlementSnapshot {
id: string;
name: string; // "소장 첨부본 2026-02-10"
asOf: Timestamp; // 기산일
method: "A" | "B";
scenarioId?: string; // 시나리오 A/B 분기 (Phase 1)
parentSnapshotId?: string; // 판결 기준 재계산 이어받기 (Phase 2)

// 계산 결과 (불변)
totalPrincipal: number;
totalInterest: number;
totalDelay: number;
totalRepaid: number;
outstanding: number;
excess?: number; // 과납액
breakdown: StepRow[]; // 충당표

// 엔진 버전 (과거 재현 보장)
engineVersion: string; // "v1.0.0"

// 동결
frozen: boolean; // true = 수정 불가
frozenAt: Timestamp;
frozenBy: string;

createdAt: Timestamp;
createdBy: string;
}

interface StepRow {
date: Timestamp;
type: "loan" | "repayment" | "interest_accrual";
amount: number;
balanceAfter: {
principal: number;
interest: number;
delay: number;
};
description: string;
formulaText: string; // 자연어 수식 ("원금 50,000,000 × 연 5% × 92일/365일")
loanRef?: string;
repaymentRef?: string;
rateRef?: string;
}

Scenario (시나리오 A/B)

cases/{caseId}/scenarios/{scenarioId} — Phase 1 활성

interface Scenario {
id: string;
name: string; // "시나리오 A — 의뢰인 주장"
perspective: "plaintiff" | "defendant" | "custom";
method: "A" | "B";
repaymentOverrides?: Array<{ // 변제 항목별 차이 (diff 저장으로 노이즈 최소화)
repaymentId: string;
amount?: number;
paidAt?: Timestamp;
isExcluded?: boolean;
}>;
rateOverrides?: Array<{...}>;

createdAt: Timestamp;
createdBy: string;
}

DocumentDraft (AI 서류 초안)

cases/{caseId}/aiDrafts/{draftId}

interface DocumentDraft {
id: string;
docType: "demandLetter" | "paymentOrder" | "complaint" | "response" | "brief";
weight: 1 | 2 | 3;
modelVersion: string; // "gemini-2.0-flash-2026-04"
promptVersion: string; // "v3"

// 생성문
originalText: string; // AI 원본
finalText: string; // 변호사 제출본
citations: Citation[];

// diff (P15 품질 회귀 탐지용)
diff?: {
levenshtein: number;
editRatio: number; // 0~1
sectionEdits: Record<string, number>;
fullReplace: boolean;
citationChanges: {
added: string[];
removed: string[]; // AI hallucination 신호
modified: string[];
};
};

// RAG 참조
referencedLegacyDocIds: string[];
referencedPublicCaseIds: string[];
referencedStatuteIds: string[];

// 실재성 검증 (3단 방어)
unverifiedCitations: string[]; // 1·2단계 실패 목록
signedAt?: Timestamp; // 3단계 서명 완료
signedBy?: string;
released: boolean; // 외부 송출 허용 상태

experimentId?: string; // A/B 테스트
consentToTraining: boolean; // Tier 3 진입 시 활용 가능 (기본 false)

createdAt: Timestamp;
createdBy: string;
updatedAt: Timestamp;
}

interface Citation {
start: number; // text 내 span 시작
end: number;
refId: string; // "case:2023da12345" | "statute:civil-477" | "memory:abc123"
verified: boolean;
source: "대법원 종합법률정보" | "국가법령정보" | "사무소 기억" | "법고을";
url?: string;
verifiedAt?: Timestamp;
}

LegacyDocument (사무소 기억)

tenants/{tid}/legacyDocuments/{docId} — 기존 컬렉션 확장

interface LegacyDocument {
id: string;
meta: {
caseType: "loan" | "construction" | "subrogation" | ...;
year: number;
clientId?: string; // 내부 ID (비식별화)
handlers: Array<{
name: string;
role: "owner" | "associate" | "staff";
active: boolean; // 퇴직 여부
nameDisclosureConsent: boolean; // ADR 0002 §6
}>;
strategies: string[]; // 전략 키워드
outcome: "won" | "lost" | "settled" | "withdrawn" | "pending";
};
content: {
summary: string;
fullTextPath: string; // Cloud Storage 원본
maskedTextPath: string; // 비식별화본
embeddingVersion: string; // ADR 0005 §2
piiTier: 1 | 2 | 3; // 강도 레벨
};
case_history_metadata: { // 세대 간 지식 이전 (ADR 0002 §6)
handlers: string[];
strategies: string[];
year: number;
outcome: string;
};
referencedBy: string[]; // 역참조: 이 기억을 참고한 신규 서류 draftIds
createdAt: Timestamp;
updatedAt: Timestamp;
}

AxMetrics (단일 집계 문서)

tenants/{tid}/axMetrics — ADR 0004 §5

interface AxMetrics {
currentLevel: 1 | 2 | 3 | 4 | 5;
kpis: {
aiCalls30d: number;
memoryDepth: number; // legacyDocuments count
pack1Completions: number; // 완주 사건 수
editRatioMedian: number; // P15 품질 신호
citationPrecision: number; // 최근 100건 기준
};
nextLevelGap: {
condition: string; // 예: "월 AI 50 필요 (현재 42)"
progressPct: number;
};
lastComputedAt: Timestamp;
computedBy: "system" | string; // Cloud Function or 수동
}

AssistCounter (weight 합산)

tenants/{tid}/assistCounters/ai-assists — ADR 0005 §2

interface AssistCounter {
periodStart: Timestamp; // 월 시작
monthlyWeight: number; // 이번 달 소진 합
dailyWeight: number; // 오늘 소진
limit: {
monthly: 200; // Chair 확정
daily: 40; // soft cap
};
lastResetAt: Timestamp;

// 분산 카운터 10 shard (ADR 0005 §11 P17)
shards: Array<{
shardId: number;
weight: number;
}>;
}

AuditLog (5종 서브컬렉션)

tenants/{tid}/auditLog/{type}/{logId} — ADR 0005 §1 블록 12 · ADR 0006 §4-5 §10

// /ai/{logId} — AI 호출 기록 (2년 보존)
interface AiAuditLog {
id: string;
feature: string; // "docGeneration" | "settlementParse" | ...
weight: 0 | 1 | 2 | 3;
model: string;
promptHash: string; // SHA-256
responseHash: string;
inputTokens: number;
outputTokens: number;
cost: number; // USD
latencyMs: number;
cacheHit: boolean;
refunded: boolean;
refundReason?: "parse_fail" | "timeout" | "upstream_5xx" | "empty_json";
embeddingVersion?: string;
retrievedDocIds?: string[]; // RAG 결과
userId: string;
timestamp: Timestamp;
}

// /rag/{logId} — RAG 쿼리 (90일 보존, 개보법 §39의14)
interface RagAuditLog {
id: string;
caseId: string;
queryText: string; // 원질의 (비식별화 후)
queryEmbedding: "hash_only"; // 실제 벡터 저장 안 함
retrievedDocIds: string[];
filters: Record<string, any>;
resultCount: number;
userId: string;
timestamp: Timestamp;
}

// /docSign/{logId} — 변호사 서명 (5년 보존, 변호사법 §15)
interface DocSignAuditLog {
id: string;
draftId: string;
signerId: string;
signedAt: Timestamp;
citationsVerified: boolean;
statutesVerified: boolean;
partiesVerified: boolean;
unverifiedCitations: string[];
ipAddress: string;
userAgent: string;
}

// /winrate/{logId} — 승소 통계 조회 (3년 보존, 변호사법 §109 형사책임)
interface WinrateAuditLog {
id: string;
caseId: string;
similarCaseIds: string[];
stats: {
total: number;
won: number;
lost: number;
settled: number;
};
displayedToUserId: string;
timestamp: Timestamp;
}

// /redaction/{logId} — 비식별화 (3년 보존, 개보법 §21)
interface RedactionAuditLog {
id: string;
documentId: string;
strength: "aggressive" | "moderate" | "minimal";
maskedFields: string[];
reidentificationRisk: "low" | "medium" | "high";
kAnonymity: number;
manualReviewBy?: string;
reviewedAt?: Timestamp;
timestamp: Timestamp;
}

ConsentHistory (동의 이력)

tenants/{tid}/consentHistory/{historyId} — ADR 0002 §8 수임약정서 조항

interface ConsentHistory {
id: string;
subject: {
type: "client" | "attorney";
id: string;
caseId?: string;
};
scope: {
tier1: boolean; // 사무소 내 참조
tier2: boolean; // 자사 AI 개선 (DPIA 후)
tier3: boolean; // 사무소 간 공동 학습 (1년 유예)
aiTraining: boolean; // 파인튜닝 사용
disclosureConsent: boolean; // 이름 노출 (퇴직 변호사)
};
contractVersion: string; // 수임약정서 버전
signedAt: Timestamp;
withdrawnAt?: Timestamp; // 철회 시점
evidenceId: string; // 전자 서명 증빙
}

Vector Search 인덱스

단일 인덱스 (ADR 0005 §1 블록 5):

Index: legal-rag-index (768 dim, cosine similarity)

restricts:
- namespace: tenantId (required)
- namespace: docType (memory | publicCase | statute | template)
- namespace: caseType
- namespace: year
- namespace: outcome
- namespace: embeddingVersion

3-part 분리 임베딩 (Phase 1 A/B 검증 후):

  • 1 문서 → 3 벡터 (요지·판결이유·주문)
  • 쿼리 시 3-part 합산 or 부분 rerank

서버 액션 시그니처 (대표)

위치: apps/web/app/(workspace)/cases/[caseId]/_actions/settlement-actions.ts

// 계산·스냅샷
export async function calculateSettlement(caseId, asOf?, method?): Promise<ActionResult<SettlementResult>>;
export async function createSettlementSnapshot(caseId, params): Promise<ActionResult<{ snapshotId }>>;
export async function getSettlementSnapshot(caseId, snapshotId): Promise<ActionResult<SettlementSnapshot>>;

// 대여·변제·이자율 CRUD
export async function upsertLoan(...): Promise<ActionResult>;
export async function upsertRepayment(...): Promise<ActionResult>;
export async function upsertInterestRate(...): Promise<ActionResult>;

// 청구취지
export async function generateClaimClause(snapshotId, preset): Promise<ActionResult<{ text }>>;

// CSV 파싱
export async function parseBankCsv(bankId, content): Promise<ActionResult<ParseResult>>;
export async function classifyRepaymentsByAi(caseId, rows): Promise<ActionResult<TransactionRow[]>>;

// 서류 자동 생성
export async function generateLegalDocument(caseId, params): Promise<ActionResult<DocumentDraft>>;
export async function signAndReleaseDocument(draftId, checklist): Promise<ActionResult<{ released }>>;

// 실재성 검증
export async function verifyCaseNumbers(citations): Promise<Array<{ citation, verified, url? }>>;
export async function verifyStatutes(citations): Promise<Array<{ citation, verified, status }>>;

// 승소 통계 (weight 0, AI 없음)
export async function computeSimilarCaseStats(caseId, ragResults): Promise<SimilarCaseStats>;

모든 mutating Server Action 은 requireCaseOwnership({ mutating: true }) 사용.

에러 코드

type Pack1ErrorCode =
| "SETTLEMENT_INPUT_INVALID"
| "SETTLEMENT_RATE_MISSING"
| "SETTLEMENT_SNAPSHOT_STALE"
| "CITATION_UNVERIFIED"
| "DOC_NOT_SIGNED"
| "COMPLIANCE_FILTER_BANNED" // 수치 금지 린트
| "TENANT_FILTER_MISSING" // assertTenantFilter
| "EMBEDDING_VERSION_MISMATCH" // silent corruption 방어
| "BANK_ADAPTER_UNSUPPORTED"
| "CSV_PARSE_ERROR"
| "ASSIST_LIMIT_EXCEEDED" // 사무소당 200/월
| "ASSIST_DAILY_CAP"; // 40/일

인덱스 (복합 쿼리 최적화)

cases/{cid}/loans → composite (caseId asc, lentAt desc)
cases/{cid}/repayments → composite (caseId asc, paidAt desc)
cases/{cid}/settlements → composite (caseId asc, asOf desc, frozen asc)
cases/{cid}/aiDrafts → composite (caseId asc, createdAt desc, released asc)
legacyDocuments → composite (tenantId asc, caseType asc, year desc, outcome asc)
auditLog/ai → composite (tenantId asc, timestamp desc, feature asc)

보관 기간 (ADR 0005 · 0006 정합)

컬렉션보관 기간근거
cases/*영구 (수임 종결 후 7년, 변호사법 §15)법적 보존
legacyDocuments영구 (수임 종결 후 7년)업무 유산
settlements영구 (수임 종결 후 7년)법적 증거
aiDrafts2년 (AI 감사)AI 호출 이력
auditLog/ai2년AI 기본법 권고
auditLog/rag90일개보법 §39의14
auditLog/docSign5년변호사법 §15
auditLog/winrate3년변호사법 §109 형사책임
auditLog/redaction3년개보법 §21
consentHistory영구 (철회 후 5년)증빙

관련 문서