mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 07:56:24 +00:00
## 변경 내용 - minutes 테이블에 user_id 컬럼 추가 (참석자별 회의록 지원) * user_id IS NULL: AI 통합 회의록 * user_id IS NOT NULL: 참석자별 회의록 - agenda_sections 테이블 생성 (안건별 AI 요약 저장) * agenda_number, agenda_title * ai_summary_short, discussions, decisions (JSON) * pending_items (JSON), opinions (JSON) - ai_summaries 테이블 생성 (AI 결과 캐싱) * summary_type: CONSOLIDATED, TODO_EXTRACTION * keywords, statistics (JSON) * processing_time_ms (성능 모니터링) - todos 테이블 확장 (AI 추출 정보) * extracted_by: AI, MANUAL * section_reference: 관련 안건 참조 * extraction_confidence: 0.00~1.00 ## 문서 - DB-Schema-회의종료.md: 상세 스키마 문서 - ERD-회의종료.puml: ERD 다이어그램 - 회의종료-개발계획.md: 전체 개발 계획 ## 설계 개선 - is_consolidated 컬럼 제거 (user_id로 구분 가능) - 중복 정보 제거로 데이터 일관성 향상
539 lines
14 KiB
Markdown
539 lines
14 KiB
Markdown
# 회의종료 기능 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 기준 파티셔닝 고려
|