# 회의종료 기능 DB 스키마 설계 ## 📋 개요 회의 종료 시 참석자별 회의록을 통합하고 AI 요약 및 Todo 자동 추출을 지원하기 위한 데이터베이스 스키마 **마이그레이션 버전**: V3__add_meeting_end_support.sql --- ## 🗄️ 테이블 구조 ### 1. minutes (확장) **설명**: 참석자별 회의록 및 AI 통합 회의록 저장 #### 새로 추가된 컬럼 | 컬럼명 | 타입 | 제약조건 | 설명 | |--------|------|----------|------| | user_id | VARCHAR(100) | NULL | 작성자 사용자 ID (참석자별 회의록인 경우) | | is_consolidated | BOOLEAN | DEFAULT FALSE | AI 통합 회의록 여부 | | consolidated_by | VARCHAR(255) | DEFAULT 'AI' | 통합 처리자 | | section_type | VARCHAR(50) | DEFAULT 'PARTICIPANT' | 회의록 섹션 타입 | #### 인덱스 - `idx_minutes_meeting_user`: (meeting_id, user_id) - `idx_minutes_consolidated`: (is_consolidated) - `idx_minutes_section_type`: (section_type) #### 사용 패턴 ```sql -- 참석자별 회의록 조회 SELECT * FROM minutes WHERE meeting_id = 'meeting-123' AND is_consolidated = false AND section_type = 'PARTICIPANT'; -- AI 통합 회의록 조회 SELECT * FROM minutes WHERE meeting_id = 'meeting-123' AND is_consolidated = true AND section_type = 'CONSOLIDATED'; ``` --- ### 2. agenda_sections (신규) **설명**: 안건별 AI 요약 결과 저장 #### 컬럼 구조 | 컬럼명 | 타입 | 제약조건 | 설명 | |--------|------|----------|------| | id | VARCHAR(36) | PRIMARY KEY | 섹션 고유 ID | | minutes_id | VARCHAR(36) | NOT NULL, FK | 회의록 ID (통합 회의록) | | meeting_id | VARCHAR(50) | NOT NULL, FK | 회의 ID | | agenda_number | INT | NOT NULL | 안건 번호 | | agenda_title | VARCHAR(200) | NOT NULL | 안건 제목 | | ai_summary_short | TEXT | NULL | AI 생성 짧은 요약 (1줄) | | discussions | TEXT | NULL | 논의 사항 (3-5문장) | | decisions | JSON | NULL | 결정 사항 배열 | | pending_items | JSON | NULL | 보류 사항 배열 | | opinions | JSON | NULL | 참석자별 의견 | | created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 생성 시간 | | updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 수정 시간 | #### 외래키 - `fk_agenda_sections_minutes`: minutes(id) ON DELETE CASCADE - `fk_agenda_sections_meeting`: meetings(meeting_id) ON DELETE CASCADE #### 인덱스 - `idx_sections_meeting`: (meeting_id) - `idx_sections_agenda`: (meeting_id, agenda_number) - `idx_sections_minutes`: (minutes_id) #### JSON 데이터 구조 **decisions** (결정 사항): ```json [ "타겟 고객: 20-30대 직장인", "UI/UX 개선을 최우선 과제로 설정", "예산: 5억원" ] ``` **pending_items** (보류 사항): ```json [ "세부 일정 확정은 다음 회의에서 논의", "추가 예산 검토 필요" ] ``` **opinions** (참석자별 의견): ```json [ { "speaker": "김민준", "opinion": "타겟 고객층을 명확히 설정하여 마케팅 전략 수립 필요" }, { "speaker": "박서연", "opinion": "UI/UX 개선에 AI 기술 적용 검토" } ] ``` #### 사용 패턴 ```sql -- 안건별 섹션 조회 SELECT * FROM agenda_sections WHERE meeting_id = 'meeting-123' ORDER BY agenda_number; -- 특정 안건 조회 SELECT * FROM agenda_sections WHERE meeting_id = 'meeting-123' AND agenda_number = 1; -- JSON 필드 쿼리 (PostgreSQL) SELECT agenda_title, decisions, jsonb_array_length(decisions::jsonb) as decision_count FROM agenda_sections WHERE meeting_id = 'meeting-123'; ``` --- ### 3. ai_summaries (신규) **설명**: AI 요약 결과 캐싱 및 성능 최적화 #### 컬럼 구조 | 컬럼명 | 타입 | 제약조건 | 설명 | |--------|------|----------|------| | id | VARCHAR(36) | PRIMARY KEY | 요약 결과 고유 ID | | meeting_id | VARCHAR(50) | NOT NULL, FK | 회의 ID | | summary_type | VARCHAR(50) | NOT NULL | 요약 타입 | | source_minutes_ids | JSON | NOT NULL | 통합에 사용된 회의록 ID 배열 | | result | JSON | NOT NULL | AI 응답 전체 결과 | | processing_time_ms | INT | NULL | AI 처리 시간 (밀리초) | | model_version | VARCHAR(50) | DEFAULT 'claude-3.5-sonnet' | AI 모델 버전 | | keywords | JSON | NULL | 주요 키워드 배열 | | statistics | JSON | NULL | 통계 정보 | | created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 생성 시간 | #### summary_type 값 - `CONSOLIDATED`: 통합 회의록 요약 - `TODO_EXTRACTION`: Todo 자동 추출 #### 외래키 - `fk_ai_summaries_meeting`: meetings(meeting_id) ON DELETE CASCADE #### 인덱스 - `idx_summaries_meeting`: (meeting_id) - `idx_summaries_type`: (meeting_id, summary_type) - `idx_summaries_created`: (created_at) #### JSON 데이터 구조 **source_minutes_ids**: ```json [ "minutes-uuid-1", "minutes-uuid-2", "minutes-uuid-3" ] ``` **result** (CONSOLIDATED 타입): ```json { "agendaSections": [ { "agendaNumber": 1, "aiSummaryShort": "타겟 고객을 20-30대로 설정...", "discussions": "신제품의 주요 타겟 고객층...", "decisions": ["타겟 고객: 20-30대", "UI/UX 개선"], "pendingItems": [], "opinions": [ {"speaker": "김민준", "opinion": "..."} ] } ] } ``` **result** (TODO_EXTRACTION 타입): ```json { "todos": [ { "content": "시장 조사 보고서 작성", "assignee": "김민준", "dueDate": "2025-11-01", "priority": "HIGH", "sectionReference": "안건 1", "confidence": 0.92 } ] } ``` **keywords**: ```json [ "신제품기획", "예산편성", "일정조율", "시장조사", "UI/UX" ] ``` **statistics**: ```json { "participantsCount": 4, "durationMinutes": 90, "agendasCount": 3, "todosCount": 5 } ``` #### 사용 패턴 ```sql -- 캐시된 통합 요약 조회 SELECT * FROM ai_summaries WHERE meeting_id = 'meeting-123' AND summary_type = 'CONSOLIDATED' ORDER BY created_at DESC LIMIT 1; -- 성능 통계 조회 SELECT summary_type, AVG(processing_time_ms) as avg_time, MAX(processing_time_ms) as max_time FROM ai_summaries GROUP BY summary_type; -- JSON 필드 쿼리 SELECT meeting_id, keywords, statistics->>'participantsCount' as participants, statistics->>'todosCount' as todos FROM ai_summaries WHERE summary_type = 'CONSOLIDATED'; ``` --- ### 4. todos (확장) **설명**: AI 자동 추출 정보 추가 #### 새로 추가된 컬럼 | 컬럼명 | 타입 | 제약조건 | 설명 | |--------|------|----------|------| | extracted_by | VARCHAR(50) | DEFAULT 'AI' | Todo 추출 방법 | | section_reference | VARCHAR(200) | NULL | 관련 회의록 섹션 참조 | | extraction_confidence | DECIMAL(3,2) | DEFAULT 0.00 | AI 추출 신뢰도 점수 | #### extracted_by 값 - `AI`: AI 자동 추출 - `MANUAL`: 사용자 수동 작성 #### 인덱스 - `idx_todos_extracted`: (extracted_by) - `idx_todos_meeting`: (meeting_id) #### 사용 패턴 ```sql -- AI 추출 Todo 조회 SELECT * FROM todos WHERE meeting_id = 'meeting-123' AND extracted_by = 'AI' ORDER BY extraction_confidence DESC; -- 신뢰도 높은 Todo 조회 SELECT * FROM todos WHERE meeting_id = 'meeting-123' AND extracted_by = 'AI' AND extraction_confidence >= 0.80; -- 안건별 Todo 조회 SELECT * FROM todos WHERE meeting_id = 'meeting-123' AND section_reference = '안건 1'; ``` --- ## 🔄 데이터 플로우 ### 회의 종료 시 데이터 저장 순서 ``` 1. 참석자별 회의록 저장 (minutes) ├─ user_id: 각 참석자 ID ├─ is_consolidated: false └─ section_type: 'PARTICIPANT' 2. AI Service 호출 → 통합 요약 생성 3. 통합 회의록 저장 (minutes) ├─ is_consolidated: true ├─ section_type: 'CONSOLIDATED' └─ consolidated_by: 'AI' 4. 안건별 섹션 저장 (agenda_sections) ├─ meeting_id: 회의 ID ├─ minutes_id: 통합 회의록 ID └─ AI 요약 결과 (discussions, decisions, opinions 등) 5. AI 요약 결과 캐싱 (ai_summaries) ├─ summary_type: 'CONSOLIDATED' ├─ source_minutes_ids: 참석자 회의록 ID 배열 └─ result: 전체 AI 응답 (JSON) 6. Todo 자동 추출 및 저장 (todos) ├─ extracted_by: 'AI' ├─ section_reference: 관련 안건 └─ extraction_confidence: 신뢰도 점수 7. 통계 정보 캐싱 (ai_summaries) ├─ keywords: 주요 키워드 배열 └─ statistics: 참석자 수, 안건 수, Todo 수 등 ``` --- ## 📊 ERD (Entity Relationship Diagram) ``` meetings ├─ 1:N → minutes (참석자별 회의록) │ ├─ user_id (참석자) │ └─ is_consolidated = false │ ├─ 1:1 → minutes (통합 회의록) │ └─ is_consolidated = true │ ├─ 1:N → agenda_sections (안건별 섹션) │ ├─ FK: minutes_id (통합 회의록) │ └─ FK: meeting_id │ ├─ 1:N → ai_summaries (AI 요약 캐시) │ ├─ summary_type: CONSOLIDATED | TODO_EXTRACTION │ └─ source_minutes_ids (JSON) │ └─ 1:N → todos (할일) ├─ extracted_by: AI | MANUAL ├─ section_reference └─ extraction_confidence ``` --- ## 🔍 주요 쿼리 예시 ### 1. 회의 종료 후 전체 데이터 조회 ```sql -- 회의 기본 정보 + 통계 SELECT m.meeting_id, m.title, m.status, s.statistics->>'participantsCount' as participants, s.statistics->>'durationMinutes' as duration, s.statistics->>'agendasCount' as agendas, s.statistics->>'todosCount' as todos, s.keywords FROM meetings m LEFT JOIN ai_summaries s ON m.meeting_id = s.meeting_id AND s.summary_type = 'CONSOLIDATED' WHERE m.meeting_id = 'meeting-123'; -- 안건별 섹션 + Todo SELECT a.agenda_number, a.agenda_title, a.ai_summary_short, a.discussions, a.decisions, a.pending_items, json_agg( json_build_object( 'content', t.content, 'assignee', t.assignee, 'dueDate', t.due_date, 'priority', t.priority, 'confidence', t.extraction_confidence ) ) as todos FROM agenda_sections a LEFT JOIN todos t ON a.meeting_id = t.meeting_id AND t.section_reference = CONCAT('안건 ', a.agenda_number) WHERE a.meeting_id = 'meeting-123' GROUP BY a.id, a.agenda_number, a.agenda_title, a.ai_summary_short, a.discussions, a.decisions, a.pending_items ORDER BY a.agenda_number; ``` ### 2. 참석자별 회의록 vs AI 통합 회의록 비교 ```sql SELECT 'PARTICIPANT' as type, user_id, content, created_at FROM minutes WHERE meeting_id = 'meeting-123' AND is_consolidated = false UNION ALL SELECT 'CONSOLIDATED' as type, 'AI' as user_id, content, created_at FROM minutes WHERE meeting_id = 'meeting-123' AND is_consolidated = true ORDER BY type, created_at; ``` ### 3. AI 성능 모니터링 ```sql -- AI 처리 시간 통계 SELECT summary_type, COUNT(*) as total_count, AVG(processing_time_ms) as avg_time_ms, MIN(processing_time_ms) as min_time_ms, MAX(processing_time_ms) as max_time_ms, PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY processing_time_ms) as p95_time_ms FROM ai_summaries WHERE created_at >= CURRENT_DATE - INTERVAL '7 days' GROUP BY summary_type; -- Todo 추출 정확도 (신뢰도 분포) SELECT CASE WHEN extraction_confidence >= 0.90 THEN 'HIGH (>=0.90)' WHEN extraction_confidence >= 0.70 THEN 'MEDIUM (0.70-0.89)' ELSE 'LOW (<0.70)' END as confidence_level, COUNT(*) as count FROM todos WHERE extracted_by = 'AI' AND created_at >= CURRENT_DATE - INTERVAL '7 days' GROUP BY confidence_level ORDER BY confidence_level DESC; ``` --- ## 🚀 마이그레이션 실행 방법 ### 1. 로컬 환경 ```bash # Flyway 마이그레이션 (Spring Boot) ./gradlew flywayMigrate # 또는 직접 SQL 실행 psql -h localhost -U postgres -d hgzero -f meeting/src/main/resources/db/migration/V3__add_meeting_end_support.sql ``` ### 2. Docker Compose 환경 ```bash # 컨테이너 재시작 (자동 마이그레이션) docker-compose down docker-compose up -d meeting-service # 마이그레이션 로그 확인 docker-compose logs -f meeting-service | grep "Flyway" ``` ### 3. 프로덕션 환경 ```bash # 1. 백업 pg_dump -h prod-db-host -U postgres -d hgzero > backup_$(date +%Y%m%d_%H%M%S).sql # 2. 마이그레이션 실행 psql -h prod-db-host -U postgres -d hgzero -f V3__add_meeting_end_support.sql # 3. 검증 psql -h prod-db-host -U postgres -d hgzero -c "\d+ agenda_sections" psql -h prod-db-host -U postgres -d hgzero -c "\d+ ai_summaries" ``` --- ## ✅ 검증 체크리스트 ### 마이그레이션 후 검증 - [ ] `minutes` 테이블에 새 컬럼 추가 확인 - [ ] `agenda_sections` 테이블 생성 확인 - [ ] `ai_summaries` 테이블 생성 확인 - [ ] `todos` 테이블 확장 확인 - [ ] 모든 인덱스 생성 확인 - [ ] 외래키 제약조건 확인 - [ ] 트리거 생성 확인 (updated_at) ### 성능 검증 - [ ] 참석자별 회의록 조회 성능 (인덱스 활용) - [ ] 안건별 섹션 조회 성능 - [ ] AI 캐시 조회 성능 (<100ms) - [ ] JSON 필드 쿼리 성능 --- ## 📝 참고 사항 ### JSON 필드 쿼리 (PostgreSQL) ```sql -- JSON 배열 길이 SELECT jsonb_array_length(decisions::jsonb) FROM agenda_sections; -- JSON 필드 추출 SELECT keywords->0 as first_keyword, statistics->>'todosCount' as todo_count FROM ai_summaries; -- JSON 배열 펼치기 SELECT agenda_title, jsonb_array_elements_text(decisions::jsonb) as decision FROM agenda_sections; ``` ### 성능 최적화 팁 1. **캐싱 활용**: ai_summaries 테이블을 우선 조회하여 불필요한 AI 호출 방지 2. **인덱스 활용**: meeting_id + summary_type 복합 인덱스로 빠른 조회 3. **JSON 필드**: PostgreSQL의 jsonb 타입 사용 권장 (인덱스 지원) 4. **파티셔닝**: 대용량 데이터의 경우 created_at 기준 파티셔닝 고려