# 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