# Meeting Service 데이터베이스 스키마 분석 최종 보고서

**작성일**: 2025-10-28  
**분석 대상**: Meeting Service (feat/meeting-ai 브랜치)  
**분석 범위**: 마이그레이션 V1~V4, 엔티티 구조, 데이터 플로우

---

## Executive Summary

### 핵심 발견사항

1. **minutes 테이블에 content 필드가 없음**
   - 실제 회의록 내용은 `minutes_sections.content`에 저장
   - minutes 테이블은 메타데이터만 보유 (title, status, version 등)

2. **사용자별 회의록 완벽하게 지원 (V3)**
   - `minutes.user_id = NULL`: AI 통합 회의록
   - `minutes.user_id = 참석자ID`: 개인별 회의록
   - 인덱스: `idx_minutes_meeting_user` (meeting_id, user_id)

3. **AI 분석 결과 구조화 저장 (V3, V4)**
   - `agenda_sections`: 안건별 구조화된 요약
   - `ai_summaries`: AI 처리 결과 캐싱
   - `todos` (V4): 각 안건의 JSON으로 저장

4. **정규화 완료 (V2)**
   - `meetings.participants` (CSV) → `meeting_participants` (테이블)
   - 복합 PK: (meeting_id, user_id)

---

## 데이터베이스 구조 개요

### 테이블 분류

**핵심 테이블** (V1):
- `meetings`: 회의 기본 정보
- `minutes`: 회의록 메타데이터
- `minutes_sections`: 회의록 섹션 (실제 내용)

**참석자 관리** (V2):
- `meeting_participants`: 회의 참석자 정보

**AI 분석** (V3):
- `agenda_sections`: 안건별 AI 요약
- `ai_summaries`: AI 처리 결과 캐시
- `todos`: Todo 아이템 (expanded)

---

## 1. 핵심 테이블별 상세 분석

### 1.1 meetings (회의 기본 정보)

**구성**:
- PK: meeting_id (VARCHAR(50))
- 주요 필드: title, purpose, description
- 상태: SCHEDULED → IN_PROGRESS → COMPLETED
- 시간: scheduled_at, started_at, ended_at (V3)

**중요 변경**:
- V3에서 `ended_at` 추가
- 회의 정확한 종료 시간 기록

```sql
-- 조회 예시
SELECT * FROM meetings 
WHERE status = 'COMPLETED' 
  AND ended_at >= NOW() - INTERVAL '7 days'
ORDER BY ended_at DESC;
```

---

### 1.2 minutes (회의록 메타데이터)

**구성**:
```
minutes_id (PK)
├─ meeting_id (FK)
├─ user_id (V3) ← NULL: AI 통합 회의록 / NOT NULL: 개인 회의록
├─ title
├─ status (DRAFT, FINALIZED)
├─ version
├─ created_by, finalized_by
└─ created_at, finalized_at
```

**중요**: 
- **content 필드 없음** → minutes_sections에 저장
- 메타데이터만 관리 (생성자, 확정자, 버전 등)

**쿼리 패턴**:
```sql
-- AI 통합 회의록
SELECT * FROM minutes 
WHERE meeting_id = ? AND user_id IS NULL;

-- 특정 사용자의 회의록
SELECT * FROM minutes 
WHERE meeting_id = ? AND user_id = ?;

-- 복합 인덱스 활용: idx_minutes_meeting_user
```

---

### 1.3 minutes_sections (회의록 섹션 - 실제 내용)

**구성**:
```
section_id (PK)
├─ minutes_id (FK) ← 어느 회의록에 속하는가
├─ type (AGENDA, DISCUSSION, DECISION, ACTION_ITEM)
├─ title
├─ content ← ★ 실제 회의록 내용
├─ order
├─ verified (검증 완료)
├─ locked (수정 불가)
└─ locked_by
```

**핵심 특성**:
- **content**: 사용자가 작성한 실제 내용
- **locked**: finalize_minutes 호출시 잠금
- **verified**: 확정시 TRUE로 설정

**데이터 흐름**:
```
1. CreateMinutes → minutes_sections 초기 생성
2. UpdateMinutes → content 저장 (여러 번)
3. FinalizeMinutes → locked=TRUE, verified=TRUE
4. (locked 상태에서 수정 불가)
```

---

### 1.4 agenda_sections (AI 요약 - V3)

**구성**:
```
id (PK, UUID)
├─ minutes_id (FK) ← 통합 회의록만 (user_id=NULL)
├─ meeting_id (FK)
├─ agenda_number (1, 2, 3...)
├─ agenda_title
├─ ai_summary_short (1줄 요약)
├─ discussions (3-5문장 논의)
├─ decisions (JSON 배열)
├─ pending_items (JSON 배열)
├─ opinions (JSON 배열: {speaker, opinion})
└─ todos (JSON 배열 [V4])
```

**V4 추가 사항**:
- `todos` JSON 필드 추가
- 안건별 추출된 Todo 저장

```json
{
  "title": "시장 조사 보고서 작성",
  "assignee": "김민준",
  "dueDate": "2025-02-15",
  "description": "20-30대 타겟 시장 조사",
  "priority": "HIGH"
}
```

**중요**:
- **통합 회의록만 분석** (user_id=NULL인 것)
- 참석자별 회의록(user_id NOT NULL)은 AI 분석 대상 아님
- minutes_id로 통합 회의록 참조

---

### 1.5 minutes_sections vs agenda_sections

| 항목 | minutes_sections | agenda_sections |
|------|-----------------|-----------------|
| **용도** | 사용자 작성 | AI 요약 |
| **모든 회의록** | ✓ 통합 + 개인 | ✗ 통합만 |
| **구조** | 순차적 섹션 | 안건별 구조화 |
| **내용 저장** | content (TEXT) | JSON 필드들 |
| **관계** | 1:N (minutes과) | N:1 (minutes과) |
| **목적** | 기록 | 분석/요약 |

**생성 흐름**:
```
회의 시작
  ↓
minutes 생성 (통합 + 개인)
  ↓
minutes_sections 생성 (4개 그룹)
  ↓
사용자 작성 중...
  ↓
회의 종료 → FinalizeMinutes
  ↓
minutes_sections locked
  ↓
AI 분석 (비동기)
  ↓
agenda_sections 생성 (통합 회의록 기반)
```

---

### 1.6 ai_summaries (AI 처리 결과 - V3)

**구성**:
```
id (PK, UUID)
├─ meeting_id (FK)
├─ summary_type (CONSOLIDATED, TODO_EXTRACTION)
├─ source_minutes_ids (JSON: 사용된 회의록 ID 배열)
├─ result (JSON: AI 응답 전체)
├─ processing_time_ms (처리 시간)
├─ model_version (claude-3.5-sonnet)
├─ keywords (JSON: 키워드 배열)
└─ statistics (JSON: {participants, agendas, todos})
```

**용도**:
- AI 처리 결과 캐싱
- 재처리 필요시 참조
- 성능 통계 기록

---

### 1.7 todos (Todo 아이템)

**기본 구조**:
```
todo_id (PK)
├─ meeting_id (FK)
├─ minutes_id (FK)
├─ title
├─ description
├─ assignee_id
├─ due_date
├─ status (PENDING, COMPLETED)
├─ priority (HIGH, MEDIUM, LOW)
└─ completed_at
```

**V3 추가 필드**:
```
├─ extracted_by (AI / MANUAL) ← AI 자동 추출 vs 수동
├─ section_reference (안건 참조)
└─ extraction_confidence (0.00~1.00) ← AI 신뢰도
```

**저장 전략**:
1. `agenda_sections.todos` (JSON): 간단한 Todo, 기본 저장 위치
2. `todos` 테이블: 상세 관리 필요시만 추가 저장

---

### 1.8 meeting_participants (참석자 관리 - V2)

**구성**:
```
PK: (meeting_id, user_id)
├─ invitation_status (PENDING, ACCEPTED, DECLINED)
├─ attended (BOOLEAN)
└─ created_at, updated_at
```

**V2 개선**:
- 이전: meetings.participants (CSV 문자열)
- 현재: 별도 테이블 (정규화)
- 복합 PK로 중복 방지

---

## 2. 회의록 작성 플로우 (전체)

### 단계별 데이터 변화

```
PHASE 1: 회의 준비
═════════════════════════════════════════════
1. CreateMeeting
   → INSERT meetings (status='SCHEDULED')
   → INSERT meeting_participants (5명)

PHASE 2: 회의 진행
═════════════════════════════════════════════
2. StartMeeting
   → UPDATE meetings SET status='IN_PROGRESS'

3. CreateMinutes (회의 중)
   → INSERT minutes (user_id=NULL) × 1 (통합)
   → INSERT minutes (user_id=user_id) × 5 (개인)
   → INSERT minutes_sections (초기 생성)

4. UpdateMinutes (여러 번)
   → UPDATE minutes_sections SET content='...'

PHASE 3: 회의 종료
═════════════════════════════════════════════
5. EndMeeting
   → UPDATE meetings SET 
       status='COMPLETED', 
       ended_at=NOW() [V3]

PHASE 4: 회의록 최종화
═════════════════════════════════════════════
6. FinalizeMinutes
   → UPDATE minutes SET 
       status='FINALIZED'
   → UPDATE minutes_sections SET 
       locked=TRUE, 
       verified=TRUE

PHASE 5: AI 분석 (비동기)
═════════════════════════════════════════════
7. MinutesAnalysisEventConsumer
   → Read minutes (user_id=NULL)
   → Read minutes_sections
   → Call AI Service
   → INSERT agenda_sections [V3]
   → INSERT ai_summaries [V3]
   → INSERT todos [V3 확장]
```

---

## 3. 사용자별 회의록 구조

### 데이터 분리 방식

**1개 회의 (참석자 5명)**:

```
meetings: 1개
  ├─ meeting_id = 'meeting-001'
  └─ status = COMPLETED

meeting_participants: 5개
  ├─ (meeting-001, user1@example.com)
  ├─ (meeting-001, user2@example.com)
  ├─ (meeting-001, user3@example.com)
  ├─ (meeting-001, user4@example.com)
  └─ (meeting-001, user5@example.com)

minutes: 6개 [V3]
  ├─ (id=consol-1, meeting_id=meeting-001, user_id=NULL)
  │   → 통합 회의록 (AI 분석 대상)
  ├─ (id=user1-min, meeting_id=meeting-001, user_id=user1@example.com)
  │   → 사용자1 개인 회의록
  ├─ (id=user2-min, meeting_id=meeting-001, user_id=user2@example.com)
  │   → 사용자2 개인 회의록
  ├─ ... (user3, user4, user5)
  └─

minutes_sections: 수십 개 (6개 회의록 × N개 섹션)
  ├─ Group 1: consol-1의 섹션들 (AI 작성)
  ├─ Group 2: user1-min의 섹션들 (사용자1 작성)
  ├─ Group 3: user2-min의 섹션들 (사용자2 작성)
  └─ ... (user3, user4, user5)

agenda_sections: 5개 [V3]
  ├─ (id=ag-1, minutes_id=consol-1, agenda_number=1)
  ├─ (id=ag-2, minutes_id=consol-1, agenda_number=2)
  └─ ... (3, 4, 5)
```

**핵심**:
- 참석자별 회의록은 minutes.user_id로 구분
- 인덱스 활용: `idx_minutes_meeting_user`
- AI 분석: 통합 회의록만 (user_id=NULL)

---

## 4. 마이그레이션 변경사항 요약

### V2 (2025-10-27)

```sql
-- meeting_participants 테이블 생성
CREATE TABLE meeting_participants (
  meeting_id, user_id (복합 PK),
  invitation_status, attended
)

-- 데이터 마이그레이션
SELECT TRIM(participant) FROM meetings.participants (CSV)
  → INSERT INTO meeting_participants

-- meetings.participants 컬럼 삭제
ALTER TABLE meetings DROP COLUMN participants
```

**영향**: 정규화 완료, 중복 데이터 제거

---

### V3 (2025-10-28)

#### 3-1. minutes 테이블 확장
```sql
ALTER TABLE minutes ADD COLUMN user_id VARCHAR(100);
CREATE INDEX idx_minutes_meeting_user ON minutes(meeting_id, user_id);
```

**의미**: 사용자별 회의록 지원

---

#### 3-2. agenda_sections 테이블 신규
```sql
CREATE TABLE agenda_sections (
  id, minutes_id, meeting_id,
  agenda_number, agenda_title,
  ai_summary_short, discussions,
  decisions (JSON),
  pending_items (JSON),
  opinions (JSON)
)
```

**의미**: AI 요약을 구조화된 형식으로 저장

---

#### 3-3. ai_summaries 테이블 신규
```sql
CREATE TABLE ai_summaries (
  id, meeting_id, summary_type,
  source_minutes_ids (JSON),
  result (JSON),
  processing_time_ms,
  model_version,
  keywords (JSON),
  statistics (JSON)
)
```

**의미**: AI 처리 결과 캐싱

---

#### 3-4. todos 테이블 확장
```sql
ALTER TABLE todos ADD COLUMN extracted_by VARCHAR(50) DEFAULT 'AI';
ALTER TABLE todos ADD COLUMN section_reference VARCHAR(200);
ALTER TABLE todos ADD COLUMN extraction_confidence DECIMAL(3,2);
```

**의미**: AI 자동 추출 추적

---

### V4 (2025-10-28)

```sql
ALTER TABLE agenda_sections ADD COLUMN todos JSON;
```

**의미**: 안건별 Todo를 JSON으로 저장

---

## 5. 성능 최적화

### 현재 인덱스

```
meetings:
  ├─ PK: meeting_id

minutes:
  ├─ PK: id
  └─ idx_minutes_meeting_user (meeting_id, user_id) [V3]

minutes_sections:
  ├─ PK: id
  └─ (minutes_id로 FK 지원)

agenda_sections: [V3]
  ├─ PK: id
  ├─ idx_sections_meeting (meeting_id)
  ├─ idx_sections_agenda (meeting_id, agenda_number)
  └─ idx_sections_minutes (minutes_id)

ai_summaries: [V3]
  ├─ PK: id
  ├─ idx_summaries_meeting (meeting_id)
  ├─ idx_summaries_type (meeting_id, summary_type)
  └─ idx_summaries_created (created_at)

todos:
  ├─ PK: todo_id
  ├─ idx_todos_extracted (extracted_by) [V3]
  └─ idx_todos_meeting (meeting_id) [V3]

meeting_participants: [V2]
  ├─ PK: (meeting_id, user_id)
  ├─ idx_user_id (user_id)
  └─ idx_invitation_status (invitation_status)
```

### 추천 추가 인덱스

```sql
-- 자주 조회하는 패턴
CREATE INDEX idx_minutes_status_created 
  ON minutes(status, created_at DESC);

CREATE INDEX idx_agenda_meeting_created 
  ON agenda_sections(meeting_id, created_at DESC);

CREATE INDEX idx_todos_meeting_assignee 
  ON todos(meeting_id, assignee_id);
```

---

## 6. 핵심 질문 답변

### Q1: minutes 테이블에 content 필드가 있는가?
**A**: **없음**
- minutes: 메타데이터만 (title, status, version 등)
- 실제 내용: minutes_sections.content

### Q2: minutes_section과 agenda_sections의 차이점?
**A**:
| 항목 | minutes_sections | agenda_sections |
|------|-----------------|-----------------|
| 목적 | 사용자 작성 | AI 요약 |
| 모든 회의록 | O | X (통합만) |
| 내용 저장 | content (TEXT) | JSON |

### Q3: 사용자별 회의록 저장 방식?
**A**: 
- minutes.user_id로 구분
- NULL: AI 통합회의록
- NOT NULL: 개인별 회의록
- 인덱스: idx_minutes_meeting_user

### Q4: V3, V4 주요 변경?
**A**:
- V3: user_id, agenda_sections, ai_summaries, todos 확장
- V4: agenda_sections.todos JSON 추가

---

## 7. 개발 시 주의사항

### Do's ✓
- minutes_sections.content에 실제 내용 저장
- AI 분석시 user_id=NULL인 minutes만 처리
- agenda_sections.todos와 todos 테이블 동시 저장 (필요시)
- 복합 인덱스 활용 (meeting_id, user_id)

### Don'ts ✗
- minutes 테이블에 content 저장 (없음)
- 참석자별 회의록(user_id NOT NULL)을 AI 분석 (통합만)
- agenda_sections를 모든 minutes에 생성 (통합만)
- 인덱스 무시한 풀 스캔

---

## 8. 파일 위치 및 참조

**마이그레이션 파일**:
- `/Users/jominseo/HGZero/meeting/src/main/resources/db/migration/V2__*.sql`
- `/Users/jominseo/HGZero/meeting/src/main/resources/db/migration/V3__*.sql`
- `/Users/jominseo/HGZero/meeting/src/main/resources/db/migration/V4__*.sql`

**엔티티**:
- `MeetingEntity`, `MinutesEntity`, `MinutesSectionEntity`
- `AgendaSectionEntity`, `TodoEntity`, `MeetingParticipantEntity`

**서비스**:
- `MinutesService`, `MinutesSectionService`
- `MinutesAnalysisEventConsumer` (비동기)

---

## 9. 결론

### 핵심 설계 원칙
1. **메타데이터 vs 내용 분리**: minutes (메타) vs minutes_sections (내용)
2. **사용자별 격리**: user_id 컬럼으로 개인 회의록 관리
3. **AI 결과 구조화**: JSON으로 유연성과 성능 확보
4. **정규화 완료**: 참석자 정보 테이블화

### 검증 사항
- V3, V4 마이그레이션 정상 적용
- 모든 인덱스 생성됨
- 관계 설정 정상 (FK, 1:N)

### 다음 단계
- 성능 모니터링 (쿼리 실행 계획)
- 추가 인덱스 검토
- AI 분석 결과 검증
- 참석자별 회의록 사용성 테스트

---

**문서 정보**:
- 작성자: Database Architecture Analysis
- 대상 서비스: Meeting Service (AI 통합 회의록)
- 최종 버전: 2025-10-28
