diff --git a/docs/DB-Schema-회의종료.md b/docs/DB-Schema-회의종료.md new file mode 100644 index 0000000..3a38e8d --- /dev/null +++ b/docs/DB-Schema-회의종료.md @@ -0,0 +1,538 @@ +# 회의종료 기능 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 기준 파티셔닝 고려 diff --git a/docs/ERD-회의종료.puml b/docs/ERD-회의종료.puml new file mode 100644 index 0000000..ded2dd6 --- /dev/null +++ b/docs/ERD-회의종료.puml @@ -0,0 +1,151 @@ +@startuml ERD-회의종료 +!theme mono + +' ======================================== +' 회의종료 기능 ERD +' ======================================== + +!define TABLE(name,desc) class name as "desc" << (T,#FFAAAA) >> +!define PRIMARY_KEY(x) PK: x +!define FOREIGN_KEY(x) FK: x +!define NOT_NULL(x) x +!define UNIQUE(x) x + +' 기존 테이블 +TABLE(meetings, "meetings\n회의") { + PRIMARY_KEY(meeting_id: VARCHAR(50)) + -- + title: VARCHAR(200) + start_time: TIMESTAMP + end_time: TIMESTAMP + status: VARCHAR(20) + created_at: TIMESTAMP + updated_at: TIMESTAMP +} + +TABLE(meeting_participants, "meeting_participants\n회의 참석자") { + PRIMARY_KEY(meeting_id, user_id) + -- + FOREIGN_KEY(meeting_id: VARCHAR(50)) + FOREIGN_KEY(user_id: VARCHAR(100)) + invitation_status: VARCHAR(20) + attended: BOOLEAN + created_at: TIMESTAMP +} + +' 확장된 minutes 테이블 +TABLE(minutes, "minutes\n회의록") { + PRIMARY_KEY(id: VARCHAR(36)) + -- + FOREIGN_KEY(meeting_id: VARCHAR(50)) + content: TEXT + NOT_NULL(user_id: VARCHAR(100)) + NOT_NULL(is_consolidated: BOOLEAN) + consolidated_by: VARCHAR(255) + section_type: VARCHAR(50) + created_at: TIMESTAMP + updated_at: TIMESTAMP +} + +' 신규 테이블: agenda_sections +TABLE(agenda_sections, "agenda_sections\n안건별 섹션") { + PRIMARY_KEY(id: VARCHAR(36)) + -- + FOREIGN_KEY(minutes_id: VARCHAR(36)) + FOREIGN_KEY(meeting_id: VARCHAR(50)) + NOT_NULL(agenda_number: INT) + NOT_NULL(agenda_title: VARCHAR(200)) + ai_summary_short: TEXT + discussions: TEXT + decisions: JSON + pending_items: JSON + opinions: JSON + created_at: TIMESTAMP + updated_at: TIMESTAMP +} + +' 신규 테이블: ai_summaries +TABLE(ai_summaries, "ai_summaries\nAI 요약 캐시") { + PRIMARY_KEY(id: VARCHAR(36)) + -- + FOREIGN_KEY(meeting_id: VARCHAR(50)) + NOT_NULL(summary_type: VARCHAR(50)) + NOT_NULL(source_minutes_ids: JSON) + NOT_NULL(result: JSON) + processing_time_ms: INT + model_version: VARCHAR(50) + keywords: JSON + statistics: JSON + created_at: TIMESTAMP +} + +' 확장된 todos 테이블 +TABLE(todos, "todos\n할일") { + PRIMARY_KEY(id: VARCHAR(36)) + -- + FOREIGN_KEY(meeting_id: VARCHAR(50)) + content: TEXT + assignee: VARCHAR(100) + due_date: DATE + priority: VARCHAR(20) + status: VARCHAR(20) + NOT_NULL(extracted_by: VARCHAR(50)) + section_reference: VARCHAR(200) + extraction_confidence: DECIMAL(3,2) + created_at: TIMESTAMP + updated_at: TIMESTAMP +} + +' 관계 정의 +meetings "1" --> "N" minutes : "has" +meetings "1" --> "N" meeting_participants : "has" +meetings "1" --> "N" agenda_sections : "has" +meetings "1" --> "N" ai_summaries : "has" +meetings "1" --> "N" todos : "has" + +minutes "1" --> "N" agenda_sections : "contains" +minutes "1" ..> "1" meeting_participants : "written by\n(user_id)" + +' 노트 추가 +note right of minutes + **참석자별 회의록** + - is_consolidated = false + - section_type = 'PARTICIPANT' + - user_id: 작성자 + + **AI 통합 회의록** + - is_consolidated = true + - section_type = 'CONSOLIDATED' + - user_id: NULL +end note + +note right of agenda_sections + **JSON 필드 구조** + - decisions: ["결정1", "결정2"] + - pending_items: ["보류1"] + - opinions: [ + {"speaker": "김민준", "opinion": "..."} + ] +end note + +note right of ai_summaries + **summary_type** + - CONSOLIDATED: 통합 요약 + - TODO_EXTRACTION: Todo 추출 + + **캐싱 전략** + - 재조회 시 DB 우선 조회 + - 처리 시간 <0.5초 +end note + +note right of todos + **extracted_by** + - AI: AI 자동 추출 + - MANUAL: 사용자 수동 작성 + + **extraction_confidence** + - 0.00 ~ 1.00 + - 0.80 이상: 신뢰도 높음 +end note + +@enduml diff --git a/docs/setup-eventhub-policy.md b/docs/setup-eventhub-policy.md new file mode 100644 index 0000000..f285238 --- /dev/null +++ b/docs/setup-eventhub-policy.md @@ -0,0 +1,107 @@ +# Event Hub 정책 설정 가이드 + +## 📌 개요 +Azure Event Hub 사용을 위한 공유 액세스 정책(SAS Policy) 설정 방법 + +## 🔧 설정 단계 + +### 1. Azure Portal 접속 +- Event Hub Namespace: `hgzero-eventhub-ns` +- 리소스 그룹: `rg-digitalgarage-02` + +### 2. 정책 생성 + +#### (1) STT 서비스용 Send 정책 +``` +정책 이름: stt-send-policy +권한: ✓ Send +용도: STT 서비스가 음성 인식 결과를 Event Hub에 발행 +``` + +**생성 방법**: +1. `hgzero-eventhub-ns` → 공유 액세스 정책 +2. `+ 추가` 클릭 +3. 정책 이름: `stt-send-policy` +4. **Send** 체크 +5. **만들기** 클릭 +6. 생성된 정책 클릭 → **연결 문자열-기본 키** 복사 + +#### (2) AI 서비스용 Listen 정책 +``` +정책 이름: ai-listen-policy +권한: ✓ Listen +용도: AI 서비스가 Event Hub에서 이벤트를 구독 +``` + +**생성 방법**: +1. 공유 액세스 정책 → `+ 추가` +2. 정책 이름: `ai-listen-policy` +3. **Listen** 체크 +4. **만들기** → 연결 문자열 복사 + +#### (3) 관리용 Manage 정책 +``` +정책 이름: hgzero-manage-policy +권한: ✓ Manage (Send + Listen 포함) +용도: 개발, 테스트, 관리 작업 +``` + +**생성 방법**: +1. 공유 액세스 정책 → `+ 추가` +2. 정책 이름: `hgzero-manage-policy` +3. **Manage** 체크 (자동으로 Send/Listen 포함) +4. **만들기** → 연결 문자열 복사 + +### 3. 연결 문자열 형식 +``` +Endpoint=sb://hgzero-eventhub-ns.servicebus.windows.net/; +SharedAccessKeyName=stt-send-policy; +SharedAccessKey=; +EntityPath=hgzero-eventhub-name +``` + +### 4. IntelliJ 실행 프로파일 설정 + +#### STT 서비스 (`.run/SttServiceApplication.run.xml`) +```xml + +``` + +#### AI 서비스 (`.run/AiServiceApplication.run.xml`) +```xml + +``` + +## ✅ 검증 방법 + +### 연결 테스트 +```bash +# STT 서비스 시작 후 로그 확인 +# 성공 메시지 예시: +[INFO] EventHubProducerClient - Connected to Event Hub: hgzero-eventhub-name +``` + +### 권한 검증 +- **Send 정책**: 이벤트 발행만 가능 +- **Listen 정책**: 이벤트 구독만 가능 +- **Manage 정책**: 모든 작업 가능 + +## 🔐 보안 권장사항 + +1. **최소 권한 원칙**: 각 서비스는 필요한 권한만 부여 +2. **키 회전**: 정기적으로 액세스 키 재생성 +3. **환경 분리**: Dev/Prod 환경별 별도 정책 사용 +4. **연결 문자열 보호**: Git에 커밋하지 않음 (환경 변수 사용) + +## 📚 참고 문서 +- [Azure Event Hub SAS 인증](https://learn.microsoft.com/azure/event-hubs/authenticate-shared-access-signature) diff --git a/docs/회의종료-개발계획.md b/docs/회의종료-개발계획.md new file mode 100644 index 0000000..abda3e7 --- /dev/null +++ b/docs/회의종료-개발계획.md @@ -0,0 +1,360 @@ +# 회의종료 기능 개발 계획 + +## 📋 개요 +회의가 종료되면 모든 참석자의 회의록을 수집하여 Claude AI가 통합 요약하고 Todo를 자동 추출하는 기능 + +## 🎯 핵심 기능 +1. **참석자별 회의록 통합**: 모든 참석자가 작성한 회의록을 DB에서 조회 +2. **AI 통합 요약**: Claude AI가 안건별로 요약 및 구조화 +3. **Todo 자동 추출**: AI가 회의록에서 액션 아이템 자동 추출 +4. **통계 생성**: 참석자 수, 회의 시간, 안건 수, Todo 수 통계 + +--- + +## 🗄️ 데이터베이스 스키마 설계 + +### 1. minutes (회의록 테이블) - 확장 +```sql +-- 기존 테이블 확장 +ALTER TABLE minutes ADD COLUMN user_id VARCHAR(50); -- 작성자 ID +ALTER TABLE minutes ADD COLUMN is_consolidated BOOLEAN DEFAULT FALSE; -- 통합 회의록 여부 +ALTER TABLE minutes ADD COLUMN consolidated_by VARCHAR(255); -- 통합 처리자 (AI) + +-- 인덱스 추가 +CREATE INDEX idx_minutes_meeting_user ON minutes(meeting_id, user_id); +CREATE INDEX idx_minutes_consolidated ON minutes(is_consolidated); +``` + +### 2. agenda_sections (안건별 섹션 테이블) - 신규 +```sql +CREATE TABLE agenda_sections ( + id VARCHAR(36) PRIMARY KEY, + minutes_id VARCHAR(36) NOT NULL, + meeting_id VARCHAR(36) NOT NULL, + agenda_number INT NOT NULL, -- 안건 번호 (1, 2, 3...) + agenda_title VARCHAR(200) NOT NULL, -- 안건 제목 + + -- AI 요약 결과 + ai_summary_short TEXT, -- 짧은 요약 (1줄) + discussion TEXT, -- 논의 사항 + decisions JSON, -- 결정 사항 배열 + pending_items JSON, -- 보류 사항 배열 + opinions JSON, -- 참석자별 의견 배열 + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + FOREIGN KEY (minutes_id) REFERENCES minutes(id) ON DELETE CASCADE, + FOREIGN KEY (meeting_id) REFERENCES meetings(id) ON DELETE CASCADE, + + INDEX idx_sections_meeting (meeting_id), + INDEX idx_sections_agenda (meeting_id, agenda_number) +); +``` + +### 3. ai_summaries (AI 요약 결과 캐시) - 신규 +```sql +CREATE TABLE ai_summaries ( + id VARCHAR(36) PRIMARY KEY, + meeting_id VARCHAR(36) NOT NULL, + summary_type VARCHAR(50) NOT NULL, -- 'CONSOLIDATED', 'TODO_EXTRACTION' + + -- 입력 정보 + source_minutes_ids JSON NOT NULL, -- 통합에 사용된 회의록 ID 배열 + + -- AI 처리 결과 + result JSON NOT NULL, -- AI 응답 전체 결과 + processing_time_ms INT, -- 처리 시간 (밀리초) + model_version VARCHAR(50), -- 사용한 AI 모델 버전 + + -- 통계 + keywords JSON, -- 주요 키워드 배열 + statistics JSON, -- 통계 정보 (참석자 수, 안건 수 등) + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + FOREIGN KEY (meeting_id) REFERENCES meetings(id) ON DELETE CASCADE, + + INDEX idx_summaries_meeting (meeting_id), + INDEX idx_summaries_type (meeting_id, summary_type) +); +``` + +### 4. todos (Todo 테이블) - 확장 +```sql +-- 기존 테이블 확장 +ALTER TABLE todos ADD COLUMN extracted_by VARCHAR(50) DEFAULT 'AI'; -- 'AI' 또는 'MANUAL' +ALTER TABLE todos ADD COLUMN section_reference VARCHAR(200); -- 관련 안건 참조 +ALTER TABLE todos ADD COLUMN extraction_confidence DECIMAL(3,2); -- AI 추출 신뢰도 (0.00~1.00) + +-- 인덱스 추가 +CREATE INDEX idx_todos_extracted ON todos(extracted_by); +CREATE INDEX idx_todos_meeting ON todos(meeting_id); +``` + +--- + +## 🔌 API 설계 + +### Meeting Service API + +#### 1. 참석자별 회의록 조회 +```yaml +GET /meetings/{meetingId}/minutes/by-participants +Response: + participantMinutes: + - userId: "user1" + userName: "김민준" + minutesId: "uuid" + content: "회의록 내용..." + status: "DRAFT" + - userId: "user2" + userName: "박서연" + ... +``` + +#### 2. 안건별 섹션 조회 +```yaml +GET /meetings/{meetingId}/agenda-sections +Response: + sections: + - agendaNumber: 1 + agendaTitle: "신제품 기획 방향성" + aiSummaryShort: "타겟 고객을 20-30대로 설정..." + discussions: "..." + decisions: [...] + todos: [...] +``` + +#### 3. 회의 통계 조회 +```yaml +GET /meetings/{meetingId}/statistics +Response: + participantsCount: 4 + durationMinutes: 90 + agendasCount: 3 + todosCount: 5 + keywords: ["신제품기획", "예산편성", ...] +``` + +### AI Service API + +#### 1. 통합 회의록 요약 API (신규) +```yaml +POST /transcripts/consolidate +Request: + meetingId: "uuid" + participantMinutes: + - userId: "user1" + content: "회의록 내용..." + - userId: "user2" + content: "회의록 내용..." + agendas: + - number: 1 + title: "신제품 기획 방향성" + +Response: + consolidatedMinutesId: "uuid" + agendaSections: + - agendaNumber: 1 + aiSummaryShort: "타겟 고객을 20-30대로 설정..." + discussions: "..." + decisions: [...] + pendingItems: [...] + keywords: ["신제품기획", "예산편성"] + processingTimeMs: 3500 +``` + +#### 2. Todo 자동 추출 API (기존) +```yaml +POST /todos/extract +Request: + meetingId: "uuid" + minutesContent: "통합된 회의록 전체 내용..." + +Response: + todos: + - content: "시장 조사 보고서 작성" + assignee: "김민준" + dueDate: "2025-11-01" + priority: "HIGH" + sectionReference: "안건 1" + confidence: 0.92 +``` + +--- + +## 🤖 Claude AI 프롬프트 설계 + +### 1. 통합 회의록 요약 프롬프트 +``` +당신은 회의록 통합 전문가입니다. + +입력: +- 회의 제목: {meetingTitle} +- 안건 목록: {agendas} +- 참석자별 회의록: + * {userName1}: {content1} + * {userName2}: {content2} + ... + +작업: +각 안건별로 다음을 생성하세요: +1. 짧은 요약 (1줄, 20자 이내) +2. 논의 사항 (핵심 내용 3-5문장) +3. 결정 사항 (배열 형태) +4. 보류 사항 (배열 형태) +5. 참석자별 의견 (speaker, opinion) + +출력 형식 (JSON): +{ + "agendaSections": [ + { + "agendaNumber": 1, + "aiSummaryShort": "...", + "discussions": "...", + "decisions": ["결정1", "결정2"], + "pendingItems": ["보류1"], + "opinions": [ + {"speaker": "김민준", "opinion": "..."} + ] + } + ], + "keywords": ["키워드1", "키워드2", ...] +} +``` + +### 2. Todo 자동 추출 프롬프트 +``` +당신은 Todo 추출 전문가입니다. + +입력: +- 회의록 전체 내용: {minutesContent} +- 참석자 목록: {participants} + +작업: +회의록에서 액션 아이템(Todo)을 추출하세요. +- "~하기로 함", "~가 작성", "~까지 완료" 등의 패턴 탐지 +- 담당자와 마감일 식별 +- 우선순위 판단 (HIGH/MEDIUM/LOW) + +출력 형식 (JSON): +{ + "todos": [ + { + "content": "시장 조사 보고서 작성", + "assignee": "김민준", + "dueDate": "2025-11-01", + "priority": "HIGH", + "sectionReference": "안건 1", + "confidence": 0.92 + } + ] +} +``` + +--- + +## 🔄 통합 플로우 + +### 회의 종료 처리 시퀀스 +``` +1. 사용자가 "회의 종료" 버튼 클릭 + ↓ +2. Meeting Service: 회의 상태를 'ENDED'로 변경 + ↓ +3. Meeting Service: 모든 참석자의 회의록 조회 (GET /meetings/{meetingId}/minutes/by-participants) + ↓ +4. AI Service 호출: 통합 요약 요청 (POST /transcripts/consolidate) + ↓ +5. Claude AI: 안건별 요약 및 구조화 + ↓ +6. AI Service: agenda_sections 테이블에 저장 + ↓ +7. AI Service 호출: Todo 자동 추출 (POST /todos/extract) + ↓ +8. Claude AI: Todo 추출 및 담당자 식별 + ↓ +9. Meeting Service: todos 테이블에 저장 + ↓ +10. Meeting Service: ai_summaries 테이블에 캐시 저장 + ↓ +11. 프론트엔드: 07-회의종료.html 화면 렌더링 +``` + +--- + +## 📊 성능 고려사항 + +### 1. 처리 시간 목표 +- AI 통합 요약: 3-5초 이내 +- Todo 추출: 2-3초 이내 +- 전체 회의 종료 처리: 10초 이내 + +### 2. 최적화 방안 +- **병렬 처리**: 통합 요약과 Todo 추출을 병렬로 실행 +- **캐싱**: ai_summaries 테이블에 결과 캐싱 (재조회 시 0.5초 이내) +- **비동기 처리**: 회의 종료 후 백그라운드에서 AI 처리 +- **진행 상태 표시**: WebSocket으로 실시간 진행률 전달 + +### 3. 대용량 처리 +- 10명 이상 참석자: 회의록을 청크 단위로 분할 처리 +- 긴 회의 (2시간 이상): 안건별 병렬 처리 + +--- + +## 🧪 테스트 시나리오 + +### 1. 단위 테스트 +- [ ] 참석자별 회의록 조회 API 테스트 +- [ ] AI 통합 요약 API 테스트 +- [ ] Todo 자동 추출 API 테스트 +- [ ] 안건별 섹션 저장 및 조회 테스트 + +### 2. 통합 테스트 +- [ ] 회의 종료 → AI 요약 → Todo 추출 전체 플로우 +- [ ] 다수 참석자 (10명) 회의록 통합 테스트 +- [ ] 긴 회의록 (5000자 이상) 처리 테스트 + +### 3. 성능 테스트 +- [ ] AI 요약 응답 시간 측정 (목표: 5초 이내) +- [ ] 동시 다발적 회의 종료 처리 (10개 동시) +- [ ] 대용량 회의록 (10000자 이상) 처리 시간 + +--- + +## 📅 개발 일정 (예상) + +### Phase 1: DB 및 기본 API (3일) +- Day 1: DB 스키마 설계 및 마이그레이션 +- Day 2: Meeting Service API 개발 +- Day 3: 단위 테스트 및 API 검증 + +### Phase 2: AI 통합 (4일) +- Day 1-2: Claude AI 프롬프트 설계 및 테스트 +- Day 3: AI Service API 개발 +- Day 4: 통합 테스트 + +### Phase 3: 최적화 및 배포 (2일) +- Day 1: 성능 최적화 및 캐싱 +- Day 2: 프론트엔드 연동 테스트 및 배포 + +**총 예상 기간: 9일** + +--- + +## 🚀 배포 체크리스트 +- [ ] DB 마이그레이션 스크립트 준비 +- [ ] API 명세서 업데이트 +- [ ] AI 프롬프트 버전 관리 +- [ ] 성능 모니터링 설정 +- [ ] 에러 로깅 및 알림 설정 +- [ ] 백업 및 롤백 계획 수립 + +--- + +## 📝 참고 문서 +- [유저스토리](../design/userstory.md) +- [AI Service API 명세](../design/backend/api/ai-service-api.yaml) +- [Meeting Service API 명세](../design/backend/api/meeting-service-api.yaml) +- [07-회의종료.html 프로토타입](../design/uiux/prototype/07-회의종료.html) diff --git a/meeting/src/main/resources/db/migration/V3__add_meeting_end_support.sql b/meeting/src/main/resources/db/migration/V3__add_meeting_end_support.sql new file mode 100644 index 0000000..8198ac0 --- /dev/null +++ b/meeting/src/main/resources/db/migration/V3__add_meeting_end_support.sql @@ -0,0 +1,192 @@ +-- ======================================== +-- V3: 회의종료 기능을 위한 스키마 확장 +-- ======================================== +-- 작성일: 2025-10-28 +-- 설명: 참석자별 회의록, 안건별 섹션, AI 요약 결과 캐싱, Todo 자동 추출 지원 + +-- ======================================== +-- 1. minutes 테이블 확장 +-- ======================================== +-- 참석자별 회의록 지원 (user_id로 구분) +-- user_id가 NULL이면 AI 통합 회의록, NOT NULL이면 참석자별 회의록 + +ALTER TABLE minutes +ADD COLUMN IF NOT EXISTS user_id VARCHAR(100); + +-- 인덱스 추가 +CREATE INDEX IF NOT EXISTS idx_minutes_meeting_user ON minutes(meeting_id, user_id); + +-- 코멘트 추가 +COMMENT ON COLUMN minutes.user_id IS '작성자 사용자 ID (NULL: AI 통합 회의록, NOT NULL: 참석자별 회의록)'; + +-- ======================================== +-- 2. agenda_sections 테이블 생성 +-- ======================================== +-- 안건별 AI 요약 결과 저장 + +CREATE TABLE IF NOT EXISTS agenda_sections ( + id VARCHAR(36) PRIMARY KEY, + minutes_id VARCHAR(36) NOT NULL, + meeting_id VARCHAR(50) NOT NULL, + + -- 안건 정보 + agenda_number INT NOT NULL, + agenda_title VARCHAR(200) NOT NULL, + + -- AI 요약 결과 + ai_summary_short TEXT, + discussions TEXT, + decisions JSON, + pending_items JSON, + opinions JSON, + + -- 메타데이터 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- 외래키 + CONSTRAINT fk_agenda_sections_minutes + FOREIGN KEY (minutes_id) REFERENCES minutes(id) + ON DELETE CASCADE, + CONSTRAINT fk_agenda_sections_meeting + FOREIGN KEY (meeting_id) REFERENCES meetings(meeting_id) + ON DELETE CASCADE +); + +-- 인덱스 생성 +CREATE INDEX IF NOT EXISTS idx_sections_meeting ON agenda_sections(meeting_id); +CREATE INDEX IF NOT EXISTS idx_sections_agenda ON agenda_sections(meeting_id, agenda_number); +CREATE INDEX IF NOT EXISTS idx_sections_minutes ON agenda_sections(minutes_id); + +-- 코멘트 추가 +COMMENT ON TABLE agenda_sections IS '안건별 회의록 섹션 - AI 요약 결과 저장'; +COMMENT ON COLUMN agenda_sections.id IS '섹션 고유 ID'; +COMMENT ON COLUMN agenda_sections.minutes_id IS '회의록 ID (통합 회의록 참조)'; +COMMENT ON COLUMN agenda_sections.meeting_id IS '회의 ID'; +COMMENT ON COLUMN agenda_sections.agenda_number IS '안건 번호 (1, 2, 3...)'; +COMMENT ON COLUMN agenda_sections.agenda_title IS '안건 제목'; +COMMENT ON COLUMN agenda_sections.ai_summary_short IS 'AI 생성 짧은 요약 (1줄, 20자 이내)'; +COMMENT ON COLUMN agenda_sections.discussions IS '논의 사항 (핵심 내용 3-5문장)'; +COMMENT ON COLUMN agenda_sections.decisions IS '결정 사항 배열 (JSON)'; +COMMENT ON COLUMN agenda_sections.pending_items IS '보류 사항 배열 (JSON)'; +COMMENT ON COLUMN agenda_sections.opinions IS '참석자별 의견 (JSON: [{speaker, opinion}])'; + +-- ======================================== +-- 3. ai_summaries 테이블 생성 +-- ======================================== +-- AI 요약 결과 캐싱 및 성능 최적화 + +CREATE TABLE IF NOT EXISTS ai_summaries ( + id VARCHAR(36) PRIMARY KEY, + meeting_id VARCHAR(50) NOT NULL, + summary_type VARCHAR(50) NOT NULL, + + -- 입력 정보 + source_minutes_ids JSON NOT NULL, + + -- AI 처리 결과 + result JSON NOT NULL, + processing_time_ms INT, + model_version VARCHAR(50) DEFAULT 'claude-3.5-sonnet', + + -- 통계 정보 + keywords JSON, + statistics JSON, + + -- 메타데이터 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- 외래키 + CONSTRAINT fk_ai_summaries_meeting + FOREIGN KEY (meeting_id) REFERENCES meetings(meeting_id) + ON DELETE CASCADE +); + +-- 인덱스 생성 +CREATE INDEX IF NOT EXISTS idx_summaries_meeting ON ai_summaries(meeting_id); +CREATE INDEX IF NOT EXISTS idx_summaries_type ON ai_summaries(meeting_id, summary_type); +CREATE INDEX IF NOT EXISTS idx_summaries_created ON ai_summaries(created_at); + +-- 코멘트 추가 +COMMENT ON TABLE ai_summaries IS 'AI 요약 결과 캐시 테이블'; +COMMENT ON COLUMN ai_summaries.id IS '요약 결과 고유 ID'; +COMMENT ON COLUMN ai_summaries.meeting_id IS '회의 ID'; +COMMENT ON COLUMN ai_summaries.summary_type IS '요약 타입 (CONSOLIDATED: 통합 요약, TODO_EXTRACTION: Todo 추출)'; +COMMENT ON COLUMN ai_summaries.source_minutes_ids IS '통합에 사용된 회의록 ID 배열 (JSON)'; +COMMENT ON COLUMN ai_summaries.result IS 'AI 응답 전체 결과 (JSON)'; +COMMENT ON COLUMN ai_summaries.processing_time_ms IS 'AI 처리 시간 (밀리초)'; +COMMENT ON COLUMN ai_summaries.model_version IS '사용한 AI 모델 버전'; +COMMENT ON COLUMN ai_summaries.keywords IS '주요 키워드 배열 (JSON)'; +COMMENT ON COLUMN ai_summaries.statistics IS '통계 정보 (참석자 수, 안건 수 등, JSON)'; + +-- ======================================== +-- 4. todos 테이블 확장 +-- ======================================== +-- AI 자동 추출 정보 추가 + +ALTER TABLE todos +ADD COLUMN IF NOT EXISTS extracted_by VARCHAR(50) DEFAULT 'AI', +ADD COLUMN IF NOT EXISTS section_reference VARCHAR(200), +ADD COLUMN IF NOT EXISTS extraction_confidence DECIMAL(3,2) DEFAULT 0.00; + +-- 인덱스 추가 +CREATE INDEX IF NOT EXISTS idx_todos_extracted ON todos(extracted_by); +CREATE INDEX IF NOT EXISTS idx_todos_meeting ON todos(meeting_id); + +-- 코멘트 추가 +COMMENT ON COLUMN todos.extracted_by IS 'Todo 추출 방법 (AI: AI 자동 추출, MANUAL: 사용자 수동 작성)'; +COMMENT ON COLUMN todos.section_reference IS '관련 회의록 섹션 참조 (예: "안건 1", "결정사항 #3")'; +COMMENT ON COLUMN todos.extraction_confidence IS 'AI 추출 신뢰도 점수 (0.00~1.00)'; + +-- ======================================== +-- 5. 제약조건 및 트리거 추가 +-- ======================================== + +-- updated_at 자동 업데이트 함수 (PostgreSQL) +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- agenda_sections 테이블에 updated_at 트리거 추가 +DROP TRIGGER IF EXISTS update_agenda_sections_updated_at ON agenda_sections; +CREATE TRIGGER update_agenda_sections_updated_at + BEFORE UPDATE ON agenda_sections + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- ======================================== +-- 6. 샘플 데이터 (개발 환경용) +-- ======================================== +-- 실제 운영 환경에서는 주석 처리 + +-- 샘플 회의록 (참석자별) +-- INSERT INTO minutes (id, meeting_id, user_id, content, created_at) +-- VALUES +-- ('sample-minutes-1', 'sample-meeting-1', 'user1@example.com', '회의록 내용 1...', CURRENT_TIMESTAMP), +-- ('sample-minutes-2', 'sample-meeting-1', 'user2@example.com', '회의록 내용 2...', CURRENT_TIMESTAMP); + +-- 샘플 통합 회의록 (user_id가 NULL) +-- INSERT INTO minutes (id, meeting_id, user_id, content, created_at) +-- VALUES +-- ('sample-minutes-consolidated', 'sample-meeting-1', NULL, 'AI 통합 회의록...', CURRENT_TIMESTAMP); + +-- 샘플 안건 섹션 +-- INSERT INTO agenda_sections (id, minutes_id, meeting_id, agenda_number, agenda_title, ai_summary_short, discussions, decisions, pending_items, opinions) +-- VALUES +-- ('sample-section-1', 'sample-minutes-consolidated', 'sample-meeting-1', 1, '신제품 기획 방향성', +-- '타겟 고객을 20-30대로 설정...', +-- '신제품의 주요 타겟 고객층을 20-30대 직장인으로 설정하고...', +-- '["타겟 고객: 20-30대 직장인", "UI/UX 개선을 최우선 과제로 설정"]'::json, +-- '[]'::json, +-- '[{"speaker": "김민준", "opinion": "타겟 고객층을 명확히 설정하여 마케팅 전략 수립 필요"}]'::json); + +-- ======================================== +-- 7. 권한 설정 (필요시) +-- ======================================== +-- GRANT SELECT, INSERT, UPDATE, DELETE ON agenda_sections TO meeting_service_user; +-- GRANT SELECT, INSERT, UPDATE, DELETE ON ai_summaries TO meeting_service_user; +-- GRANT SELECT, INSERT, UPDATE, DELETE ON ai_summaries TO ai_service_user;