# Meeting Service 데이터베이스 스키마 전체 분석 ## 1. 마이그레이션 파일 현황 ### 마이그레이션 체인 ``` V1 (초기) → V2 (회의 참석자) → V3 (회의종료) → V4 (todos) ``` ### 각 마이그레이션 내용 - **V1**: 초기 스키마 (meetings, minutes, minutes_sections 등 - JPA로 자동 생성) - **V2**: `meeting_participants` 테이블 분리 (2025-10-27) - **V3**: 회의종료 기능 지원 (2025-10-28) - **주요 변경** - **V4**: `agenda_sections` 테이블에 `todos` 컬럼 추가 (2025-10-28) --- ## 2. 핵심 테이블 구조 분석 ### 2.1 meetings 테이블 **용도**: 회의 기본 정보 저장 | 컬럼명 | 타입 | 설명 | 용도 | |--------|------|------|------| | meeting_id | VARCHAR(50) | PK | 회의 고유 식별자 | | title | VARCHAR(200) | NOT NULL | 회의 제목 | | purpose | VARCHAR(500) | | 회의 목적 | | description | TEXT | | 상세 설명 | | scheduled_at | TIMESTAMP | NOT NULL | 예정된 시간 | | started_at | TIMESTAMP | | 실제 시작 시간 | | ended_at | TIMESTAMP | | **V3 추가**: 실제 종료 시간 | | status | VARCHAR(20) | NOT NULL | 상태: SCHEDULED, IN_PROGRESS, COMPLETED | | organizer_id | VARCHAR(50) | NOT NULL | 회의 주최자 | | created_at | TIMESTAMP | | 생성 시간 | | updated_at | TIMESTAMP | | 수정 시간 | **관계**: - 1:N with `meeting_participants` (V2에서 분리) - 1:N with `minutes` --- ### 2.2 minutes 테이블 **용도**: 회의록 기본 정보 + 사용자별 회의록 구분 | 컬럼명 | 타입 | 설명 | 용도 | |--------|------|------|------| | id/minutes_id | VARCHAR(50) | PK | 회의록 고유 식별자 | | meeting_id | VARCHAR(50) | FK | 해당 회의 ID | | user_id | VARCHAR(100) | **V3 추가** | NULL: AI 통합 회의록 / NOT NULL: 참석자별 회의록 | | title | VARCHAR(200) | NOT NULL | 회의록 제목 | | status | VARCHAR(20) | NOT NULL | DRAFT, FINALIZED | | version | INT | NOT NULL | 버전 관리 | | created_by | VARCHAR(50) | NOT NULL | 작성자 | | finalized_by | VARCHAR(50) | | 확정자 | | finalized_at | TIMESTAMP | | 확정 시간 | | created_at | TIMESTAMP | | 생성 시간 | | updated_at | TIMESTAMP | | 수정 시간 | **중요**: `minutes` 테이블에는 `content` 컬럼이 **없음** - 실제 회의록 내용은 `minutes_sections`의 `content`에 저장됨 - minutes는 메타데이터만 저장 **인덱스 (V3)**: `idx_minutes_meeting_user` on (meeting_id, user_id) **관계**: - N:1 with `meetings` - 1:N with `minutes_sections` - 1:N with `agenda_sections` (V3 추가) --- ### 2.3 minutes_sections 테이블 **용도**: 회의록 섹션별 상세 내용 | 컬럼명 | 타입 | 설명 | |--------|------|------| | id | VARCHAR(50) | PK | | minutes_id | VARCHAR(50) | FK to minutes | | type | VARCHAR(50) | AGENDA, DISCUSSION, DECISION, ACTION_ITEM | | title | VARCHAR(200) | 섹션 제목 | | **content** | TEXT | **섹션 상세 내용 저장** | | order | INT | 섹션 순서 | | verified | BOOLEAN | 검증 완료 여부 | | locked | BOOLEAN | 잠금 여부 | | locked_by | VARCHAR(50) | 잠금 사용자 | **중요 사항**: - 회의록 실제 내용은 여기에 저장됨 - `minutes`와 N:1 관계 (1개 회의록에 다중 섹션) - 사용자별 회의록도 각각 섹션을 가짐 --- ### 2.4 agenda_sections 테이블 (V3 신규) **용도**: 안건별 AI 요약 결과 저장 (구조화된 형식) | 컬럼명 | 타입 | 설명 | 포함 데이터 | |--------|------|------|-----------| | id | VARCHAR(36) | PK | UUID | | minutes_id | VARCHAR(36) | FK | 통합 회의록 참조 | | meeting_id | VARCHAR(50) | FK | 회의 ID | | agenda_number | INT | | 안건 번호 (1, 2, 3...) | | agenda_title | VARCHAR(200) | | 안건 제목 | | ai_summary_short | TEXT | | 짧은 요약 (1줄, 20자 이내) | | discussions | TEXT | | 논의 사항 (3-5문장) | | decisions | JSON | | 결정 사항 배열 | | pending_items | JSON | | 보류 사항 배열 | | opinions | JSON | | 참석자별 의견: [{speaker, opinion}] | | **todos** | JSON | **V4 추가** | 추출된 Todo: [{title, assignee, dueDate, description, priority}] | **V4 추가 구조** (todos JSON): ```json [ { "title": "시장 조사 보고서 작성", "assignee": "김민준", "dueDate": "2025-02-15", "description": "20-30대 타겟 시장 조사", "priority": "HIGH" } ] ``` **인덱스**: - `idx_sections_meeting` on meeting_id - `idx_sections_agenda` on (meeting_id, agenda_number) - `idx_sections_minutes` on minutes_id **관계**: - N:1 with `minutes` (통합 회의록만 참조) - N:1 with `meetings` --- ### 2.5 minutes_section vs agenda_sections 차이점 | 특성 | minutes_sections | agenda_sections | |------|------------------|-----------------| | **용도** | 회의록 작성용 | AI 요약 결과 저장용 | | **구조** | 순차적 섹션 (type: AGENDA, DISCUSSION, DECISION) | 안건별 구조화된 데이터 | | **내용 저장** | content (TEXT) | 구조화된 필드 + JSON | | **소유 관계** | 모든 회의록 (사용자별 포함) | 통합 회의록만 (user_id=NULL) | | **목적** | 사용자 작성 | AI 자동 생성 | | **JSON 필드** | 없음 | decisions, pending_items, opinions, todos | **생성 흐름**: ``` 회의 종료 → 통합 회의록 (minutes, user_id=NULL) → minutes_sections 생성 (사용자가 내용 작성) → AI 분석 → agenda_sections 생성 (AI 요약 결과 저장) 동시에: → 참석자별 회의록 (minutes, user_id NOT NULL) → 참석자별 minutes_sections 생성 ``` --- ### 2.6 ai_summaries 테이블 (V3 신규) **용도**: AI 요약 결과 캐싱 | 컬럼명 | 타입 | 설명 | |--------|------|------| | id | VARCHAR(36) | PK | | meeting_id | VARCHAR(50) | FK | | summary_type | VARCHAR(50) | CONSOLIDATED (통합 요약) / TODO_EXTRACTION (Todo 추출) | | source_minutes_ids | JSON | 통합에 사용된 회의록 ID 배열 | | result | JSON | **AI 응답 전체 결과** | | processing_time_ms | INT | AI 처리 시간 | | model_version | VARCHAR(50) | 사용 모델 (claude-3.5-sonnet) | | keywords | JSON | 주요 키워드 배열 | | statistics | JSON | 통계 (참석자 수, 안건 수 등) | --- ### 2.7 todos 테이블 **용도**: Todo 아이템 저장 | 컬럼명 | 타입 | 설명 | |--------|------|------| | todo_id | VARCHAR(50) | PK | | minutes_id | VARCHAR(50) | FK | 관련 회의록 | | meeting_id | VARCHAR(50) | FK | 회의 ID | | title | VARCHAR(200) | 제목 | | description | TEXT | 상세 설명 | | assignee_id | VARCHAR(50) | 담당자 | | due_date | DATE | 마감일 | | status | VARCHAR(20) | PENDING, COMPLETED | | priority | VARCHAR(20) | HIGH, MEDIUM, LOW | | completed_at | TIMESTAMP | 완료 시간 | **V3에서 추가된 컬럼**: ```sql extracted_by VARCHAR(50) -- AI 또는 MANUAL section_reference VARCHAR(200) -- 관련 회의록 섹션 참조 extraction_confidence DECIMAL(3,2) -- AI 신뢰도 (0.00~1.00) ``` --- ### 2.8 meeting_participants 테이블 (V2 신규) **용도**: 회의 참석자 정보 분리 | 컬럼명 | 타입 | 설명 | |--------|------|------| | meeting_id | VARCHAR(50) | PK1, FK | | user_id | VARCHAR(100) | PK2 | | invitation_status | VARCHAR(20) | PENDING, ACCEPTED, DECLINED | | attended | BOOLEAN | 참석 여부 | | created_at | TIMESTAMP | | | updated_at | TIMESTAMP | | **변경 배경 (V2)**: - 이전: meetings.participants (CSV 문자열) - 현재: meeting_participants (별도 테이블, 정규화) --- ## 3. 회의록 작성 플로우에서의 테이블 사용 ### 3.1 회의 시작 (StartMeeting) ``` meetings 테이블 UPDATE └─ status: SCHEDULED → IN_PROGRESS └─ started_at 기록 ``` ### 3.2 회의 종료 (EndMeeting) ``` meetings 테이블 UPDATE ├─ status: IN_PROGRESS → COMPLETED └─ ended_at 기록 (V3 신규) ↓ minutes 테이블 생성 (AI 통합 회의록) ├─ user_id = NULL ├─ status = DRAFT └─ 각 참석자별 회의록도 동시 생성 └─ user_id = 참석자ID ↓ minutes_sections 테이블 초기 생성 ├─ 통합 회의록용 섹션 └─ 각 참석자별 섹션 ``` ### 3.3 회의록 작성 (CreateMinutes / UpdateMinutes) ``` minutes 테이블 UPDATE ├─ title 작성 └─ status 유지 (DRAFT) ↓ minutes_sections 테이블 INSERT/UPDATE ├─ type: AGENDA, DISCUSSION, DECISION 등 ├─ title: 섹션 제목 ├─ content: 실제 회의록 내용 ← **여기에 사용자가 입력한 내용 저장** └─ order: 순서 사용자가 작성한 내용 저장 경로: minutes_sections.content (TEXT 컬럼) ``` ### 3.4 AI 분석 (FinializeMinutes + AI Processing) ``` minutes 테이블 UPDATE ├─ status: DRAFT → FINALIZED └─ finalized_at 기록 ↓ agenda_sections 테이블 INSERT ├─ minutesId = 통합 회의록 ID (user_id=NULL) ├─ AI 요약: aiSummaryShort, discussions ├─ 구조화된 데이터: decisions, pendingItems, opinions (JSON) └─ todos (V4): AI 추출 Todo (JSON) ↓ ai_summaries 테이블 INSERT ├─ summary_type: CONSOLIDATED ├─ result: AI 응답 전체 결과 └─ keywords, statistics ↓ todos 테이블 INSERT (선택) ├─ 간단한 Todo는 agenda_sections.todos에만 저장 └─ 상세 관리 필요한 경우 별도 테이블 저장 ``` --- ## 4. 사용자별 회의록 저장 구조 ### 4.1 회의 종료 시 자동 생성 ``` 1개의 회의 → 여러 회의록 ├─ AI 통합 회의록 (minutes.user_id = NULL) │ ├─ minutes_sections (AI/시스템이 생성) │ └─ agenda_sections (AI 분석 결과) │ └─ 각 참석자별 회의록 (minutes.user_id = 참석자ID) ├─ User1의 회의록 (minutes.user_id = 'user1@example.com') │ └─ minutes_sections (User1이 작성) │ ├─ User2의 회의록 (minutes.user_id = 'user2@example.com') │ └─ minutes_sections (User2이 작성) │ └─ ... ``` ### 4.2 minutes 테이블 쿼리 예시 ```sql -- 특정 회의의 AI 통합 회의록 SELECT * FROM minutes WHERE meeting_id = 'meeting-001' AND user_id IS NULL; -- 특정 회의의 참석자별 회의록 SELECT * FROM minutes WHERE meeting_id = 'meeting-001' AND user_id IS NOT NULL; -- 특정 사용자의 회의록 SELECT * FROM minutes WHERE user_id = 'user1@example.com'; -- 참석자별로 회의록 조회 (복합 인덱스 활용) SELECT * FROM minutes WHERE meeting_id = 'meeting-001' AND user_id = 'user1@example.com'; ``` --- ## 5. V3 마이그레이션의 주요 변경사항 ### 5.1 minutes 테이블 확장 ```sql 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); ``` **영향**: - 기존 회의록: `user_id = NULL` (AI 통합 회의록) - 새 회의록: `user_id = 참석자ID` (참석자별) - 쿼리 성능: 복합 인덱스로 빠른 검색 ### 5.2 agenda_sections 테이블 신규 생성 - AI 요약을 구조화된 형식으로 저장 - JSON 필드로 결정사항, 보류사항, 의견, Todo 저장 - minutes_id로 통합 회의록과 연결 ### 5.3 ai_summaries 테이블 신규 생성 - AI 처리 결과 캐싱 - 처리 시간, 모델 버전 기록 - 재처리 필요 시 참조 가능 ### 5.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) DEFAULT 0.00; ``` **목적**: - AI 자동 추출 vs 수동 작성 구분 - Todo의 출처 추적 - AI 신뢰도 관리 --- ## 6. V4 마이그레이션의 변경사항 ### 6.1 agenda_sections 테이블에 todos 컬럼 추가 ```sql ALTER TABLE agenda_sections ADD COLUMN IF NOT EXISTS todos JSON; ``` **구조**: ```json { "title": "시장 조사 보고서 작성", "assignee": "김민준", "dueDate": "2025-02-15", "description": "20-30대 타겟 시장 조사", "priority": "HIGH" } ``` **저장 경로**: - **안건별 요약의 Todo**: `agenda_sections.todos` (JSON) - **개별 Todo 관리**: `todos` 테이블 (필요시) --- ## 7. 데이터 정규화 현황 ### 7.1 정규화 수행 (V2) ``` meetings (이전): participants: "user1@example.com,user2@example.com" ↓ 정규화 (V2 마이그레이션) meetings_participants (별도 테이블): [meeting_id, user_id] (복합 PK) invitation_status attended ``` ### 7.2 JSON 필드 사용 (V3, V4) - `decisions`, `pending_items`, `opinions`, `todos` (agenda_sections) - `keywords`, `statistics` (ai_summaries) - `source_minutes_ids` (ai_summaries) **사용 이유**: - 변동적인 구조 데이터 - AI 응답의 유연한 저장 - 쿼리 패턴이 검색보다 전체 조회 --- ## 8. 핵심 질문 답변 ### Q1: minutes 테이블에 content 필드가 있는가? **A**: **없음**. 회의록 실제 내용은 `minutes_sections.content`에 저장됨. ### Q2: minutes_section과 agenda_sections의 차이점? | 항목 | minutes_sections | agenda_sections | |------|-----------------|-----------------| | 목적 | 사용자 작성 | AI 요약 | | 모든 회의록 | O | X (통합만) | | 구조 | 순차적 | 안건별 | | 내용 저장 | content (TEXT) | JSON | ### Q3: 사용자별 회의록을 저장할 적절한 구조는? **A**: - `minutes` 테이블: `user_id` 컬럼으로 구분 - `minutes_sections`: 각 회의록의 섹션 - 인덱스: `idx_minutes_meeting_user` (meeting_id, user_id) ### Q4: V3, V4 주요 변경사항은? - **V3**: user_id 추가, agenda_sections 신규, ai_summaries 신규, todos 확장 - **V4**: agenda_sections.todos JSON 필드 추가 --- ## 9. 데이터베이스 구조도 (PlantUML) ```plantuml @startuml !theme mono entity "meetings" as meetings { * meeting_id: VARCHAR(50) -- title: VARCHAR(200) status: VARCHAR(20) organizer_id: VARCHAR(50) started_at: TIMESTAMP ended_at: TIMESTAMP [V3] created_at: TIMESTAMP updated_at: TIMESTAMP } entity "meeting_participants" as participants { * meeting_id: VARCHAR(50) [FK] * user_id: VARCHAR(100) -- invitation_status: VARCHAR(20) attended: BOOLEAN } entity "minutes" as minutes { * id: VARCHAR(50) -- meeting_id: VARCHAR(50) [FK] user_id: VARCHAR(100) [V3] title: VARCHAR(200) status: VARCHAR(20) created_by: VARCHAR(50) finalized_at: TIMESTAMP } entity "minutes_sections" as sections { * id: VARCHAR(50) -- minutes_id: VARCHAR(50) [FK] type: VARCHAR(50) title: VARCHAR(200) content: TEXT locked: BOOLEAN } entity "agenda_sections" as agenda { * id: VARCHAR(36) -- minutes_id: VARCHAR(36) [FK, 통합회의록만] meeting_id: VARCHAR(50) [FK] agenda_number: INT agenda_title: VARCHAR(200) ai_summary_short: TEXT discussions: TEXT decisions: JSON opinions: JSON todos: JSON [V4] } entity "ai_summaries" as summaries { * id: VARCHAR(36) -- meeting_id: VARCHAR(50) [FK] summary_type: VARCHAR(50) result: JSON keywords: JSON statistics: JSON } entity "todos" as todos { * todo_id: VARCHAR(50) -- meeting_id: VARCHAR(50) [FK] minutes_id: VARCHAR(50) [FK] title: VARCHAR(200) assignee_id: VARCHAR(50) status: VARCHAR(20) extracted_by: VARCHAR(50) [V3] } meetings ||--o{ participants: "1:N" meetings ||--o{ minutes: "1:N" meetings ||--o{ agenda: "1:N" meetings ||--o{ todos: "1:N" minutes ||--o{ sections: "1:N" minutes ||--o{ agenda: "1:N" meetings ||--o{ summaries: "1:N" @enduml ``` --- ## 10. 회의록 작성 전체 플로우 ``` ┌─────────────────────────────────────────────────────┐ │ 1. 회의 시작 (StartMeeting) │ │ ├─ meetings.status = IN_PROGRESS │ │ └─ meetings.started_at 기록 │ └─────────────────┬───────────────────────────────────┘ │ ┌─────────────────▼───────────────────────────────────┐ │ 2. 회의 진행 중 (회의록 작성) │ │ ├─ CreateMinutes: minutes 생성 (user_id=NULL 통합) │ │ ├─ CreateMinutes: 참석자별 minutes 생성 │ │ ├─ UpdateMinutes: minutes_sections 작성 │ │ │ └─ content에 회의 내용 저장 │ │ └─ SaveMinutes: draft 상태 유지 │ └─────────────────┬───────────────────────────────────┘ │ ┌─────────────────▼───────────────────────────────────┐ │ 3. 회의 종료 (EndMeeting) │ │ ├─ meetings.status = COMPLETED │ │ ├─ meetings.ended_at = NOW() [V3] │ │ └─ 회의 기본 정보 확정 │ └─────────────────┬───────────────────────────────────┘ │ ┌─────────────────▼───────────────────────────────────┐ │ 4. 회의록 최종화 (FinalizeMinutes) │ │ ├─ minutes.status = FINALIZED │ │ ├─ minutes.finalized_by = 확정자 │ │ ├─ minutes.finalized_at = NOW() │ │ └─ minutes_sections 내용 확정 (locked) │ └─────────────────┬───────────────────────────────────┘ │ ┌─────────────────▼───────────────────────────────────┐ │ 5. AI 분석 처리 (MinutesAnalysisEventConsumer) │ │ ├─ 통합 회의록 분석 (user_id=NULL) │ │ │ │ │ ├─ agenda_sections INSERT [V3] │ │ │ ├─ minutes_id = 통합 회의록 ID │ │ │ ├─ ai_summary_short, discussions │ │ │ ├─ decisions, pending_items, opinions (JSON) │ │ │ └─ todos (JSON) [V4] │ │ │ │ │ ├─ ai_summaries INSERT [V3] │ │ │ ├─ summary_type = CONSOLIDATED │ │ │ ├─ result = AI 응답 전체 │ │ │ └─ keywords, statistics │ │ │ │ │ └─ todos TABLE INSERT (선택) │ │ ├─ extracted_by = 'AI' [V3] │ │ └─ extraction_confidence [V3] │ └─────────────────┬───────────────────────────────────┘ │ ┌─────────────────▼───────────────────────────────────┐ │ 6. 회의록 조회 │ │ ├─ 통합 회의록 조회 │ │ │ └─ minutes + minutes_sections + agenda_sections │ │ ├─ 참석자별 회의록 조회 │ │ │ └─ minutes (user_id=참석자) + minutes_sections │ │ └─ Todo 조회 │ │ └─ agenda_sections.todos 또는 todos 테이블 │ └─────────────────────────────────────────────────────┘ ``` --- ## 11. 성능 최적화 포인트 ### 11.1 인덱스 현황 ``` meetings: - PK: meeting_id minutes: - PK: id - idx_minutes_meeting_user (meeting_id, user_id) [V3] ← 핵심 minutes_sections: - PK: id - FK: minutes_id 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) ``` ### 11.2 추천 추가 인덱스 ```sql -- 빠른 조회를 위한 인덱스 CREATE INDEX idx_minutes_status 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); ``` --- ## 12. 결론 ### 핵심 설계 원칙 1. **참석자별 회의록**: minutes.user_id로 구분 (NULL=AI 통합, NOT NULL=개인) 2. **내용 저장**: minutes_sections.content에 사용자가 작성한 내용 저장 3. **구조화된 요약**: agenda_sections에 AI 요약을 JSON으로 저장 4. **추적 가능성**: extracted_by, section_reference로 Todo 출처 추적 5. **정규화**: V2에서 meeting_participants로 정규화 완료 ### 주의사항 - `minutes` 테이블 자체는 메타데이터만 저장 (title, status 등) - 실제 회의 내용: `minutes_sections.content` - AI 요약 결과: `agenda_sections` (구조화됨) - Todo는 두 곳에 저장 가능: agenda_sections.todos (JSON) / todos 테이블