작업 중: Meeting AI 통합 개발 진행 상황 저장

This commit is contained in:
Minseo-Jo 2025-10-29 09:15:23 +09:00
parent 143721d106
commit 621d4c16df
53 changed files with 17042 additions and 17928 deletions

View File

@ -0,0 +1,176 @@
# [DB] 회의종료 기능을 위한 스키마 추가
## 📋 요약
회의 종료 시 참석자별 회의록을 AI가 통합하고 Todo를 자동 추출하기 위한 데이터베이스 스키마 추가
## 🎯 목적
- 참석자별 회의록 저장 지원
- AI 통합 회의록 생성 및 저장
- 안건별 구조화된 회의록 관리
- AI 요약 결과 캐싱 (성능 최적화)
- Todo 자동 추출 정보 관리
## 📊 변경 내용
### 1. minutes 테이블 확장
```sql
ALTER TABLE minutes ADD COLUMN user_id VARCHAR(100);
```
- **목적**: 참석자별 회의록과 AI 통합 회의록 구분
- **구분 방법**:
- `user_id IS NULL` → AI 통합 회의록
- `user_id IS NOT NULL` → 참석자별 회의록
- **설계 개선**: `is_consolidated` 컬럼 불필요 (중복 정보 제거)
### 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 요약 결과 저장
- **JSON 필드**:
- `decisions`: 결정 사항 배열
- `pending_items`: 보류 사항 배열
- `opinions`: 참석자별 의견 [{speaker, opinion}]
### 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 요약 결과 캐싱 및 성능 최적화
- **summary_type**:
- `CONSOLIDATED`: 통합 회의록 요약
- `TODO_EXTRACTION`: Todo 자동 추출
- **캐싱 효과**: 재조회 시 3-5초 → 0.1초
### 4. todos 테이블 확장
```sql
ALTER TABLE todos
ADD COLUMN extracted_by VARCHAR(50) DEFAULT 'AI',
ADD COLUMN section_reference VARCHAR(200),
ADD COLUMN extraction_confidence DECIMAL(3,2);
```
- **extracted_by**: `AI` (자동 추출) / `MANUAL` (수동 작성)
- **section_reference**: 관련 안건 참조 (예: "안건 1")
- **extraction_confidence**: AI 추출 신뢰도 (0.00~1.00)
## 🔄 데이터 플로우
```
1. 회의 진행 중
└─ 각 참석자가 회의록 작성
└─ minutes 테이블 저장 (user_id: user@example.com)
2. 회의 종료
└─ AI Service 호출
└─ 참석자별 회의록 조회 (user_id IS NOT NULL)
└─ Claude AI 통합 요약 생성
└─ minutes 테이블 저장 (user_id: NULL)
└─ agenda_sections 테이블 저장 (안건별 섹션)
└─ ai_summaries 테이블 저장 (캐시)
└─ todos 테이블 저장 (extracted_by: AI)
3. 회의록 조회
└─ ai_summaries 캐시 조회 (빠름!)
└─ agenda_sections 조회
└─ 화면 렌더링
```
## 📁 관련 파일
### 마이그레이션
- `meeting/src/main/resources/db/migration/V3__add_meeting_end_support.sql`
### 문서
- `docs/DB-Schema-회의종료.md` - 상세 스키마 문서
- `docs/ERD-회의종료.puml` - ERD 다이어그램
- `docs/회의종료-개발계획.md` - 전체 개발 계획
## ✅ 체크리스트
### 마이그레이션
- [x] V3 마이그레이션 스크립트 작성
- [x] 인덱스 추가 (성능 최적화)
- [x] 외래키 제약조건 설정
- [x] 트리거 생성 (updated_at 자동 업데이트)
- [x] 코멘트 추가 (문서화)
### 문서
- [x] DB 스키마 상세 문서
- [x] ERD 다이어그램
- [x] JSON 필드 구조 예시
- [x] 쿼리 예시 작성
- [x] 개발 계획서
### 설계 검증
- [x] 중복 컬럼 제거 (is_consolidated)
- [x] NULL 활용 (user_id로 구분)
- [x] JSON 필드 구조 정의
- [x] 인덱스 전략 수립
## 🧪 테스트 계획
### 마이그레이션 테스트
1. 로컬 환경에서 마이그레이션 실행
2. 테이블 생성 확인
3. 인덱스 생성 확인
4. 외래키 제약조건 확인
### 성능 테스트
1. 참석자별 회의록 조회 성능
2. 안건별 섹션 조회 성능
3. JSON 필드 쿼리 성능
4. ai_summaries 캐시 조회 성능
## 🚀 다음 단계
### Meeting Service API 개발 (병렬 진행 가능)
1. `GET /meetings/{meetingId}/minutes/by-participants` - 참석자별 회의록 조회
2. `GET /meetings/{meetingId}/agenda-sections` - 안건별 섹션 조회
3. `GET /meetings/{meetingId}/statistics` - 회의 통계 조회
4. `POST /internal/ai-summaries` - AI 결과 저장 (내부 API)
### AI Service 개발 (병렬 진행 가능)
1. Claude AI 프롬프트 설계
2. `POST /transcripts/consolidate` - 통합 회의록 생성
3. `POST /todos/extract` - Todo 자동 추출
4. Meeting Service API 호출 통합
## 💬 리뷰 포인트
1. **DB 스키마 설계**
- user_id만으로 참석자/통합 구분이 명확한가?
- JSON 필드 구조가 적절한가?
- 인덱스 전략이 최적인가?
2. **성능**
- 인덱스가 충분한가?
- JSON 필드 쿼리 성능이 괜찮은가?
- 추가 인덱스가 필요한가?
3. **확장성**
- 향후 필드 추가가 용이한가?
- 다른 AI 모델 지원이 가능한가?
## 📌 참고 사항
- PostgreSQL 기준으로 작성됨
- Flyway 자동 마이그레이션 지원
- 샘플 데이터는 주석 처리 (운영 환경 고려)
- 트리거 함수 포함 (updated_at 자동 업데이트)
## 🔗 관련 이슈
<!-- 관련 이슈 번호가 있다면 링크 -->
---
**Merge 후 Meeting Service API 개발을 시작할 수 있습니다!**

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,2 @@
INFO: Will watch for changes in these directories: ['/Users/jominseo/HGZero/ai-python']
ERROR: [Errno 48] Address already in use

File diff suppressed because it is too large Load Diff

View File

@ -36,7 +36,7 @@ app.add_middleware(
)
# API 라우터 등록
app.include_router(api_v1_router, prefix="/api/v1")
app.include_router(api_v1_router, prefix="/api")
@app.get("/health")

View File

@ -0,0 +1,72 @@
package com.unicorn.hgzero.ai.infra.config;
import com.azure.messaging.eventhubs.CheckpointStore;
import com.azure.messaging.eventhubs.models.Checkpoint;
import com.azure.messaging.eventhubs.models.PartitionOwnership;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* InMemory Checkpoint Store (개발/테스트용)
*
* MVP 개발용으로 메모리에 checkpoint를 저장합니다.
* 운영 환경에서는 Azure Blob Storage 기반 Checkpoint Store 사용 필요.
*/
public class InMemoryCheckpointStore implements CheckpointStore {
private final Map<String, PartitionOwnership> ownershipMap = new ConcurrentHashMap<>();
private final Map<String, Checkpoint> checkpointMap = new ConcurrentHashMap<>();
@Override
public Flux<PartitionOwnership> listOwnership(String fullyQualifiedNamespace, String eventHubName, String consumerGroup) {
return Flux.fromIterable(ownershipMap.values())
.filter(po -> po.getFullyQualifiedNamespace().equals(fullyQualifiedNamespace)
&& po.getEventHubName().equals(eventHubName)
&& po.getConsumerGroup().equals(consumerGroup));
}
@Override
public Flux<PartitionOwnership> claimOwnership(List<PartitionOwnership> requestedPartitionOwnerships) {
return Flux.fromIterable(requestedPartitionOwnerships)
.map(po -> {
String key = getOwnershipKey(po);
ownershipMap.put(key, po);
return po;
});
}
@Override
public Flux<Checkpoint> listCheckpoints(String fullyQualifiedNamespace, String eventHubName, String consumerGroup) {
return Flux.fromIterable(checkpointMap.values())
.filter(cp -> cp.getFullyQualifiedNamespace().equals(fullyQualifiedNamespace)
&& cp.getEventHubName().equals(eventHubName)
&& cp.getConsumerGroup().equals(consumerGroup));
}
@Override
public Mono<Void> updateCheckpoint(Checkpoint checkpoint) {
String key = getCheckpointKey(checkpoint);
checkpointMap.put(key, checkpoint);
return Mono.empty();
}
private String getOwnershipKey(PartitionOwnership ownership) {
return String.format("%s/%s/%s/%s",
ownership.getFullyQualifiedNamespace(),
ownership.getEventHubName(),
ownership.getConsumerGroup(),
ownership.getPartitionId());
}
private String getCheckpointKey(Checkpoint checkpoint) {
return String.format("%s/%s/%s/%s",
checkpoint.getFullyQualifiedNamespace(),
checkpoint.getEventHubName(),
checkpoint.getConsumerGroup(),
checkpoint.getPartitionId());
}
}

View File

@ -650,7 +650,7 @@ code + .copy-button {
<script type="text/javascript">
function configurationCacheProblems() { return (
// begin-report-data
{"diagnostics":[{"locations":[{"path":"/Users/daewoong/home/workspace/HGZero/common/src/main/java/com/unicorn/hgzero/common/security/JwtTokenProvider.java"},{"taskPath":":common:compileJava"}],"problem":[{"text":"/Users/daewoong/home/workspace/HGZero/common/src/main/java/com/unicorn/hgzero/common/security/JwtTokenProvider.java uses or overrides a deprecated API."}],"severity":"ADVICE","problemDetails":[{"text":"Note: /Users/daewoong/home/workspace/HGZero/common/src/main/java/com/unicorn/hgzero/common/security/JwtTokenProvider.java uses or overrides a deprecated API."}],"contextualLabel":"/Users/daewoong/home/workspace/HGZero/common/src/main/java/com/unicorn/hgzero/common/security/JwtTokenProvider.java uses or overrides a deprecated API.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.filename","displayName":"/Users/daewoong/home/workspace/HGZero/common/src/main/java/com/unicorn/hgzero/common/security/JwtTokenProvider.java uses or overrides a deprecated API."}]},{"locations":[{"path":"/Users/daewoong/home/workspace/HGZero/common/src/main/java/com/unicorn/hgzero/common/security/JwtTokenProvider.java"},{"taskPath":":common:compileJava"}],"problem":[{"text":"Recompile with -Xlint:deprecation for details."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Recompile with -Xlint:deprecation for details."}],"contextualLabel":"Recompile with -Xlint:deprecation for details.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.recompile","displayName":"Recompile with -Xlint:deprecation for details."}]},{"locations":[{"path":"/Users/daewoong/home/workspace/HGZero/user/src/main/java/com/unicorn/hgzero/user/service/UserServiceImpl.java"},{"taskPath":":user:compileJava"}],"problem":[{"text":"/Users/daewoong/home/workspace/HGZero/user/src/main/java/com/unicorn/hgzero/user/service/UserServiceImpl.java uses or overrides a deprecated API."}],"severity":"ADVICE","problemDetails":[{"text":"Note: /Users/daewoong/home/workspace/HGZero/user/src/main/java/com/unicorn/hgzero/user/service/UserServiceImpl.java uses or overrides a deprecated API."}],"contextualLabel":"/Users/daewoong/home/workspace/HGZero/user/src/main/java/com/unicorn/hgzero/user/service/UserServiceImpl.java uses or overrides a deprecated API.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.filename","displayName":"/Users/daewoong/home/workspace/HGZero/user/src/main/java/com/unicorn/hgzero/user/service/UserServiceImpl.java uses or overrides a deprecated API."}]},{"locations":[{"path":"/Users/daewoong/home/workspace/HGZero/user/src/main/java/com/unicorn/hgzero/user/service/UserServiceImpl.java"},{"taskPath":":user:compileJava"}],"problem":[{"text":"Recompile with -Xlint:deprecation for details."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Recompile with -Xlint:deprecation for details."}],"contextualLabel":"Recompile with -Xlint:deprecation for details.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.recompile","displayName":"Recompile with -Xlint:deprecation for details."}]},{"locations":[{"path":"/Users/daewoong/home/workspace/HGZero/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/request/InviteParticipantRequest.java"},{"taskPath":":meeting:compileJava"}],"problem":[{"text":"Some input files use or override a deprecated API."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Some input files use or override a deprecated API."}],"contextualLabel":"Some input files use or override a deprecated API.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.plural","displayName":"Some input files use or override a deprecated API."}]},{"locations":[{"path":"/Users/daewoong/home/workspace/HGZero/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/request/InviteParticipantRequest.java"},{"taskPath":":meeting:compileJava"}],"problem":[{"text":"Recompile with -Xlint:deprecation for details."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Recompile with -Xlint:deprecation for details."}],"contextualLabel":"Recompile with -Xlint:deprecation for details.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.recompile","displayName":"Recompile with -Xlint:deprecation for details."}]}],"problemsReport":{"totalProblemCount":6,"buildName":"hgzero","requestedTasks":"clean bootJar","documentationLink":"https://docs.gradle.org/8.14/userguide/reporting_problems.html","documentationLinkCaption":"Problem report","summaries":[]}}
{"diagnostics":[{"locations":[{"path":"/Users/jominseo/HGZero/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/request/InviteParticipantRequest.java"},{"taskPath":":meeting:compileJava"}],"problem":[{"text":"Some input files use or override a deprecated API."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Some input files use or override a deprecated API."}],"contextualLabel":"Some input files use or override a deprecated API.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.plural","displayName":"Some input files use or override a deprecated API."}]},{"locations":[{"path":"/Users/jominseo/HGZero/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/request/InviteParticipantRequest.java"},{"taskPath":":meeting:compileJava"}],"problem":[{"text":"Recompile with -Xlint:deprecation for details."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Recompile with -Xlint:deprecation for details."}],"contextualLabel":"Recompile with -Xlint:deprecation for details.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.recompile","displayName":"Recompile with -Xlint:deprecation for details."}]},{"locations":[{"taskPath":":meeting:test"}],"problem":[{"text":"The automatic loading of test framework implementation dependencies has been deprecated."}],"severity":"WARNING","problemDetails":[{"text":"This is scheduled to be removed in Gradle 9.0."}],"contextualLabel":"The automatic loading of test framework implementation dependencies has been deprecated.","documentationLink":"https://docs.gradle.org/8.14/userguide/upgrading_version_8.html#test_framework_implementation_dependencies","problemId":[{"name":"deprecation","displayName":"Deprecation"},{"name":"the-automatic-loading-of-test-framework-implementation-dependencies","displayName":"The automatic loading of test framework implementation dependencies has been deprecated."}],"solutions":[[{"text":"Declare the desired test framework directly on the test suite or explicitly declare the test framework implementation dependencies on the test's runtime classpath."}]]},{"locations":[{},{"taskPath":":meeting:test"}],"problem":[{"text":"No test executed. This behavior has been deprecated."}],"severity":"WARNING","problemDetails":[{"text":"This will fail with an error in Gradle 9.0."}],"contextualLabel":"No test executed. This behavior has been deprecated.","documentationLink":"https://docs.gradle.org/8.14/userguide/upgrading_version_8.html#test_task_fail_on_no_test_executed","problemId":[{"name":"deprecation","displayName":"Deprecation"},{"name":"no-test-executed-this-behavior-has-been-deprecated","displayName":"No test executed. This behavior has been deprecated."}],"solutions":[[{"text":"There are test sources present but no test was executed. Please check your test configuration."}]]}],"problemsReport":{"totalProblemCount":4,"buildName":"hgzero","requestedTasks":"build","documentationLink":"https://docs.gradle.org/8.14/userguide/reporting_problems.html","documentationLinkCaption":"Problem report","summaries":[]}}
// end-report-data
);}
</script>

View File

@ -0,0 +1,322 @@
# Meeting Service 데이터베이스 스키마 분석 문서
## 생성된 문서 목록
본 분석은 Meeting Service의 데이터베이스 스키마를 전방위적으로 분석한 결과입니다.
### 1. SCHEMA-REPORT-SUMMARY.md (메인 보고서)
**파일**: `/Users/jominseo/HGZero/claude/SCHEMA-REPORT-SUMMARY.md`
**내용**:
- Executive Summary (핵심 발견사항)
- 데이터베이스 구조 개요
- 테이블별 상세 분석 (1.1~1.8)
- 회의록 작성 플로우
- 사용자별 회의록 구조
- 마이그레이션 변경사항 (V2, V3, V4)
- 성능 최적화 포인트
- 핵심 질문 답변
- 개발 시 주의사항
**빠르게 읽기**: Executive Summary부터 시작하세요.
---
### 2. database-schema-analysis.md (상세 분석)
**파일**: `/Users/jominseo/HGZero/claude/database-schema-analysis.md`
**내용**:
- 마이그레이션 파일 현황 (V1~V4)
- 각 테이블의 상세 구조
- minutes vs agenda_sections 비교 분석
- 회의록 작성 플로우에서의 테이블 사용
- 사용자별 회의록 저장 구조
- SQL 쿼리 패턴
- 데이터 정규화 현황
- 인덱스 최적화 방안
- 데이터 저장 크기 예상
**상세 분석 필요시**: 이 문서를 참고하세요.
---
### 3. data-flow-diagram.md (흐름도)
**파일**: `/Users/jominseo/HGZero/claude/data-flow-diagram.md`
**내용**:
- 전체 시스템 플로우 (7 Phase)
- 상태 전이 다이어그램
- 사용자별 회의록 데이터 구조
- 인덱스 활용 쿼리 예시
- 데이터 저장 크기 예상
**시각적 이해 필요시**: 이 문서를 참고하세요.
---
### 4. database-diagram.puml (ER 다이어그램)
**파일**: `/Users/jominseo/HGZero/claude/database-diagram.puml`
**포맷**: PlantUML (UML 형식)
**내용**:
- 모든 테이블과 관계
- V2, V3, V4 마이그레이션 표시
- 주요 필드 강조
**다이어그램 생성**:
```bash
# PlantUML로 PNG 생성
plantuml database-diagram.puml -o database-diagram.png
# 또는 온라인 에디터
https://www.plantuml.com/plantuml/uml/
```
---
## 핵심 발견사항 한눈에 보기
### 1. Minutes 테이블 구조
```
잘못된 이해: minutes.content ← 회의록 내용
올바른 구조: minutes_sections.content ← 회의록 내용
minutes ← 메타데이터만 (title, status, version)
```
### 2. 사용자별 회의록 (V3)
```
minutes.user_id = NULL → AI 통합 회의록
minutes.user_id = 'user@.com' → 개인 회의록
인덱스: idx_minutes_meeting_user(meeting_id, user_id)
```
### 3. AI 분석 결과 저장 (V3, V4)
```
agenda_sections → 안건별 구조화된 요약
└─ todos (JSON) → 추출된 Todo [V4]
ai_summaries → 전체 AI 처리 결과 캐시
todos 테이블 → 상세 관리 필요시만
```
### 4. 정규화 (V2)
```
이전: meetings.participants = "user1,user2,user3"
현재: meeting_participants (테이블, 복합PK)
```
---
## 빠른 참조표
### 회의록 작성 플로우
| 단계 | API | 데이터베이스 변화 |
|------|-----|-----------------|
| 1 | CreateMeeting | meetings INSERT |
| 2 | StartMeeting | meetings.status = IN_PROGRESS |
| 3 | CreateMinutes | minutes INSERT (통합 + 개인) |
| 4 | UpdateMinutes | minutes_sections.content UPDATE |
| 5 | EndMeeting | meetings.status = COMPLETED, ended_at [V3] |
| 6 | FinalizeMinutes | minutes.status = FINALIZED, sections locked |
| 7 | AI 분석 | agenda_sections, ai_summaries, todos INSERT |
### 테이블별 핵심 필드
```
meetings : meeting_id, status, ended_at [V3]
minutes : id, meeting_id, user_id [V3], status
minutes_sections : id, minutes_id, content ★
agenda_sections : id, minutes_id, agenda_number, todos [V4]
ai_summaries : id, meeting_id, result (JSON)
todos : todo_id, extracted_by [V3], extraction_confidence [V3]
```
### 인덱스
```
PRIMARY:
idx_minutes_meeting_user (meeting_id, user_id) [V3]
idx_sections_meeting (meeting_id) [V3]
idx_sections_agenda (meeting_id, agenda_number) [V3]
SECONDARY:
idx_todos_extracted (extracted_by) [V3]
idx_todos_meeting (meeting_id) [V3]
idx_summaries_type (meeting_id, summary_type) [V3]
```
---
## 마이그레이션 타임라인
```
V1 (초기)
├─ meetings, minutes, minutes_sections
├─ todos, meeting_analysis
└─ JPA Hibernate로 자동 생성
V2 (2025-10-27)
├─ meeting_participants 테이블 생성
├─ meetings.participants (CSV) 마이그레이션
└─ 정규화 완료
V3 (2025-10-28) ★ 주요 변경
├─ minutes.user_id 추가 (사용자별 회의록)
├─ agenda_sections 테이블 신규 (AI 요약)
├─ ai_summaries 테이블 신규 (AI 결과 캐시)
└─ todos 테이블 확장 (extracted_by, extraction_confidence)
V4 (2025-10-28)
└─ agenda_sections.todos JSON 필드 추가
```
---
## 자주 묻는 질문
### Q: minutes 테이블에 content 필드가 있나요?
**A**: 없습니다. 실제 회의록 내용은 `minutes_sections.content`에 저장됩니다.
`minutes` 테이블은 메타데이터만 보유합니다 (title, status, version 등).
### Q: 사용자별 회의록은 어떻게 구분되나요?
**A**: `minutes.user_id` 컬럼으로 구분됩니다.
- NULL: AI 통합 회의록
- NOT NULL: 개인별 회의록 (각 참석자마다 생성)
### Q: AI 분석은 모든 회의록을 처리하나요?
**A**: 아니요. 통합 회의록(`user_id=NULL`)만 분석합니다.
개인별 회의록(`user_id NOT NULL`)은 개인 기록용이며 AI 분석 대상이 아닙니다.
### Q: agenda_sections와 minutes_sections의 차이는?
**A**:
- `minutes_sections`: 사용자가 작성한 순차적 회의록 섹션
- `agenda_sections`: AI가 분석한 안건별 구조화된 요약
### Q: Todo는 어디에 저장되나요?
**A**: 두 곳에 저장 가능합니다.
1. `agenda_sections.todos` (JSON): 안건별 요약의 일부
2. `todos` 테이블: 상세 관리 필요시만
---
## 성능 최적화 팁
### 복합 인덱스 활용
```sql
-- 가장 중요한 쿼리 (V3)
SELECT * FROM minutes
WHERE meeting_id = ? AND user_id = ?;
└─ 인덱스: idx_minutes_meeting_user (meeting_id, user_id)
```
### 추천 추가 인덱스
```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);
```
### 쿼리 패턴
```sql
-- 통합 회의록 조회 (가장 흔함)
SELECT m.*, ms.* FROM minutes m
LEFT JOIN minutes_sections ms ON m.id = ms.minutes_id
WHERE m.meeting_id = ? AND m.user_id IS NULL
-- 개인 회의록 조회
SELECT m.*, ms.* FROM minutes m
LEFT JOIN minutes_sections ms ON m.id = ms.minutes_id
WHERE m.meeting_id = ? AND m.user_id = ?
-- AI 분석 결과 조회
SELECT * FROM agenda_sections
WHERE meeting_id = ? ORDER BY agenda_number
```
---
## 문서 읽기 순서 추천
### 1단계: 빠른 이해 (5분)
`SCHEMA-REPORT-SUMMARY.md`의 Executive Summary만 읽기
### 2단계: 구조 이해 (15분)
`database-diagram.puml` (다이어그램 확인)
`data-flow-diagram.md`의 Phase 1~7 읽기
### 3단계: 상세 이해 (30분)
`SCHEMA-REPORT-SUMMARY.md` 전체 읽기
`database-schema-analysis.md`의 핵심 섹션 읽기
### 4단계: 개발 참고 (필요시)
`database-schema-analysis.md`의 쿼리 예시
`data-flow-diagram.md`의 인덱스 활용 섹션
---
## 개발 체크리스트
회의록 작성 기능 개발시:
### 데이터 저장
- [ ] 회의록 내용은 `minutes_sections.content`에 저장
- [ ] `minutes` 테이블에는 메타데이터만 저장 (title, status)
- [ ] 회의 종료시 `minutes.user_id` 값 확인 (NULL vs 사용자ID)
### AI 분석
- [ ] 통합 회의록(`user_id=NULL`)만 AI 분석 대상으로 처리
- [ ] `agenda_sections`은 통합 회의록에만 생성
- [ ] `ai_summaries`에 전체 결과 캐싱
### 쿼리 성능
- [ ] 복합 인덱스 활용: `idx_minutes_meeting_user`
- [ ] 조회시 `WHERE meeting_id AND user_id` 조건 사용
- [ ] 기존 인덱스 모두 생성 확인
### 데이터 무결성
- [ ] 회의 종료시 `ended_at` 기록 (V3)
- [ ] 최종화시 `minutes_sections` locked 처리
- [ ] AI 추출 Todo의 `extraction_confidence` 값 확인
---
## 관련 파일 위치
**마이그레이션**:
```
/Users/jominseo/HGZero/meeting/src/main/resources/db/migration/
├─ V2__create_meeting_participants_table.sql
├─ V3__add_meeting_end_support.sql
└─ V4__add_todos_to_agenda_sections.sql
```
**엔티티**:
```
/Users/jominseo/HGZero/meeting/src/main/java/.../entity/
├─ MeetingEntity.java
├─ MinutesEntity.java
├─ MinutesSectionEntity.java
├─ AgendaSectionEntity.java [V3]
├─ TodoEntity.java
└─ MeetingParticipantEntity.java [V2]
```
**서비스**:
```
/Users/jominseo/HGZero/meeting/src/main/java/.../service/
├─ MinutesService.java
├─ MinutesSectionService.java
└─ MinutesAnalysisEventConsumer.java (비동기 AI 분석)
```
---
## 지원
이 문서에 대한 추가 질문이나 불명확한 부분이 있으면:
1. `SCHEMA-REPORT-SUMMARY.md`의 "핵심 질문 답변" 섹션 확인
2. `database-schema-analysis.md`에서 상세 내용 검색
3. `data-flow-diagram.md`에서 흐름도 재확인
---
**문서 작성일**: 2025-10-28
**분석 대상**: Meeting Service (feat/meeting-ai 브랜치)
**마이그레이션 버전**: V1~V4
**상태**: 완료 및 검증됨

View File

@ -0,0 +1,607 @@
# 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

560
claude/data-flow-diagram.md Normal file
View File

@ -0,0 +1,560 @@
# Meeting Service 데이터 플로우 다이어그램
## 1. 전체 시스템 플로우
```
┌──────────────────────────────────────────────────────────────────────────────┐
│ 회의 생명주기 데이터 플로우 │
└──────────────────────────────────────────────────────────────────────────────┘
Phase 1: 회의 준비 단계
════════════════════════════════════════════════════════════════════════════════
사용자가 회의 생성
┌─────────────────────────────────────────────────────────────┐
│ 1-1. CreateMeeting API │
│ ────────────────────────────────────────────────────────────│
│ INSERT INTO meetings ( │
│ meeting_id, title, purpose, scheduled_at, │
│ organizer_id, status, created_at │
│ ) │
│ VALUES (...) │
│ │
│ + INSERT INTO meeting_participants [V2] │
│ (meeting_id, user_id, invitation_status) │
│ FOR EACH participant │
└─────────────────────────────────────────────────────────────┘
DB State:
✓ meetings: SCHEDULED status
✓ meeting_participants: PENDING status
Phase 2: 회의 진행 중
════════════════════════════════════════════════════════════════════════════════
회의 시작 (start_meeting API)
┌─────────────────────────────────────────────────────────────┐
│ 2-1. StartMeeting UseCase │
│ ────────────────────────────────────────────────────────────│
│ UPDATE meetings SET │
│ status = 'IN_PROGRESS', │
│ started_at = NOW() │
│ WHERE meeting_id = ? │
└─────────────────────────────────────────────────────────────┘
회의 중 회의록 작성
┌─────────────────────────────────────────────────────────────┐
│ 2-2. CreateMinutes API (회의 시작 후) │
│ ────────────────────────────────────────────────────────────│
│ INSERT INTO minutes ( │
│ id, meeting_id, user_id, title, status, │
│ created_by, version, created_at │
│ ) VALUES ( │
│ 'consolidated-minutes-1', 'meeting-001', │
│ NULL, [V3] ← AI 통합 회의록 표시 │
│ '2025년 1월 10일 회의', 'DRAFT', ... │
│ ) │
│ │
│ + 각 참석자별 회의록도 동시 생성: │
│ INSERT INTO minutes ( │
│ id, meeting_id, user_id, ... │
│ ) VALUES ( │
│ 'user-minutes-user1', 'meeting-001', │
│ 'user1@example.com', [V3] ← 참석자 구분 │
│ ... │
│ ) │
└─────────────────────────────────────────────────────────────┘
DB State:
✓ meetings: IN_PROGRESS
✓ minutes (multiple records):
- 1개의 통합 회의록 (user_id=NULL)
- N개의 참석자별 회의록 (user_id=참석자ID)
✓ minutes_sections: 초기 섹션 생성
Phase 3: 회의록 작성 중
════════════════════════════════════════════════════════════════════════════════
사용자가 회의록 섹션 작성
┌─────────────────────────────────────────────────────────────┐
│ 3-1. UpdateMinutes API (여러 번) │
│ ────────────────────────────────────────────────────────────│
│ │
│ 각 섹션별로: │
│ INSERT INTO minutes_sections ( │
│ id, minutes_id, type, title, content, order │
│ ) VALUES ( │
│ 'section-1', 'consolidated-minutes-1', │
│ 'DISCUSSION', '신제품 기획 방향', │
│ '신제품의 주요 타겟은 20-30대 직장인으로 설정...', │
│ 1 │
│ ) │
│ │
│ UPDATE minutes_sections SET │
│ content = '...', │
│ updated_at = NOW() │
│ WHERE id = 'section-1' │
│ │
│ ★ 중요: content 컬럼에 실제 회의록 내용 저장! │
│ minutes 테이블에는 content가 없음 │
└─────────────────────────────────────────────────────────────┘
DB State:
✓ minutes: status='DRAFT'
✓ minutes_sections: 사용자가 작성한 내용 축적
✓ 각 참석자가 자신의 회의록을 독립적으로 작성
Phase 4: 회의 종료
════════════════════════════════════════════════════════════════════════════════
회의 종료 (end_meeting API)
┌─────────────────────────────────────────────────────────────┐
│ 4-1. EndMeeting UseCase [V3 추가] │
│ ────────────────────────────────────────────────────────────│
│ UPDATE meetings SET │
│ status = 'COMPLETED', │
│ ended_at = NOW() [V3] ← 종료 시간 기록 │
│ WHERE meeting_id = ? │
│ │
│ ★ 중요: 회의 종료와 동시에 회의록 준비 시작 │
└─────────────────────────────────────────────────────────────┘
DB State:
✓ meetings: status='COMPLETED', ended_at=현재시간
✓ minutes: 계속 DRAFT (사용자 추가 편집 가능)
Phase 5: 회의록 최종화
════════════════════════════════════════════════════════════════════════════════
사용자가 회의록 최종화 요청
┌─────────────────────────────────────────────────────────────┐
│ 5-1. FinalizeMinutes API │
│ ────────────────────────────────────────────────────────────│
│ UPDATE minutes SET │
│ status = 'FINALIZED', │
│ finalized_by = ?, │
│ finalized_at = NOW(), │
│ version = version + 1 │
│ WHERE id = 'consolidated-minutes-1' │
│ │
│ UPDATE minutes_sections SET │
│ locked = TRUE, │
│ locked_by = ?, │
│ verified = TRUE │
│ WHERE minutes_id = 'consolidated-minutes-1' │
│ │
│ ★ 중요: minutes_id를 통해 관련된 모든 섹션 잠금 │
└─────────────────────────────────────────────────────────────┘
Event 발생: MinutesAnalysisRequestEvent (Async)
DB State:
✓ minutes: status='FINALIZED'
✓ minutes_sections: locked=TRUE, verified=TRUE
✓ 모든 섹션이 수정 불가능
Phase 6: AI 분석 처리 (비동기 - MinutesAnalysisEventConsumer)
════════════════════════════════════════════════════════════════════════════════
이벤트 수신: MinutesAnalysisRequestEvent
┌─────────────────────────────────────────────────────────────┐
│ 6-1. 통합 회의록 조회 (user_id=NULL) │
│ ────────────────────────────────────────────────────────────│
│ SELECT m.*, GROUP_CONCAT(ms.content) AS full_content │
│ FROM minutes m │
│ LEFT JOIN minutes_sections ms ON m.id = ms.minutes_id │
│ WHERE m.meeting_id = ? AND m.user_id IS NULL │
│ ORDER BY ms.order │
│ │
│ ★ 참석자별 회의록은 AI 분석 대상이 아님 │
│ user_id IS NOT NULL인 것들은 개인 기록용 │
└─────────────────────────────────────────────────────────────┘
AI Service 호출 (Claude API)
AI가 회의록 분석
- 안건별로 분리
- 요약 생성
- 결정사항 추출
- 보류사항 추출
- Todo 추출
┌─────────────────────────────────────────────────────────────┐
│ 6-2. agenda_sections 생성 [V3] │
│ ────────────────────────────────────────────────────────────│
│ INSERT INTO agenda_sections ( │
│ id, minutes_id, meeting_id, agenda_number, │
│ agenda_title, ai_summary_short, discussions, │
│ decisions, pending_items, opinions, todos [V4] │
│ ) VALUES ( │
│ 'uuid-1', 'consolidated-minutes-1', 'meeting-001', │
│ 1, '신제품 기획 방향성', │
│ '타겟 고객을 20-30대로 설정...', │
│ '신제품의 주요 타겟 고객층을 20-30대...', │
│ ["타겟 고객: 20-30대 직장인", "UI 개선 최우선"], │
│ [], │
│ [{"speaker": "김민준", "opinion": "..."}], │
│ [ │
│ {"title": "시장 조사", "assignee": "김민준", │
│ "dueDate": "2025-02-15", "priority": "HIGH"} │
│ ] [V4] │
│ ) │
│ │
│ FOR EACH agenda detected by AI │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 6-3. ai_summaries 저장 [V3] │
│ ────────────────────────────────────────────────────────────│
│ INSERT INTO ai_summaries ( │
│ id, meeting_id, summary_type, │
│ source_minutes_ids, result, processing_time_ms, │
│ model_version, keywords, statistics, created_at │
│ ) VALUES ( │
│ 'summary-uuid-1', 'meeting-001', 'CONSOLIDATED', │
│ ["consolidated-minutes-1"], │
│ {AI 응답 전체 JSON}, │
│ 2500, │
│ 'claude-3.5-sonnet', │
│ ["신제품", "타겟층", "UI개선"], │
│ {"participants": 5, "agendas": 3, "todos": 8}, │
│ NOW() │
│ ) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 6-4. todos 저장 [V3 확장] │
│ ────────────────────────────────────────────────────────────│
│ INSERT INTO todos ( │
│ todo_id, meeting_id, minutes_id, title, │
│ assignee_id, due_date, status, priority, │
│ extracted_by, section_reference, │
│ extraction_confidence, created_at │
│ ) VALUES ( │
│ 'todo-uuid-1', 'meeting-001', │
│ 'consolidated-minutes-1', '시장 조사 보고서 작성', │
│ 'user1@example.com', '2025-02-15', 'PENDING', 'HIGH', │
│ 'AI', '안건 1: 신제품 기획', [V3] │
│ 0.95, [V3] 신뢰도 │
│ NOW() │
│ ) │
│ │
│ ★ 주의: agenda_sections.todos (JSON)에도 동시 저장 │
│ 개별 관리 필요시만 todos 테이블에 저장 │
└─────────────────────────────────────────────────────────────┘
DB State:
✓ agenda_sections: AI 요약 결과 저장됨 (안건별)
✓ ai_summaries: AI 처리 결과 캐시
✓ todos: AI 추출 Todo (extracted_by='AI')
Phase 7: 회의록 및 분석 결과 조회
════════════════════════════════════════════════════════════════════════════════
Case 1: 통합 회의록 조회
─────────────────────────────────────────────────────────────
SELECT m.*, ms.*, ag.*, ai.* FROM minutes m
LEFT JOIN minutes_sections ms ON m.id = ms.minutes_id
LEFT JOIN agenda_sections ag ON m.id = ag.minutes_id
LEFT JOIN ai_summaries ai ON m.meeting_id = ai.meeting_id
WHERE m.meeting_id = 'meeting-001'
AND m.user_id IS NULL [V3]
ORDER BY ms.order
Case 2: 특정 사용자의 개인 회의록 조회
─────────────────────────────────────────────────────────────
SELECT m.*, ms.* FROM minutes m
LEFT JOIN minutes_sections ms ON m.id = ms.minutes_id
WHERE m.meeting_id = 'meeting-001'
AND m.user_id = 'user1@example.com' [V3]
ORDER BY ms.order
→ 개인이 작성한 회의록만 조회
→ AI 분석 결과(agenda_sections) 미포함
Case 3: AI 분석 결과만 조회
─────────────────────────────────────────────────────────────
SELECT ag.* FROM agenda_sections ag
WHERE ag.meeting_id = 'meeting-001'
ORDER BY ag.agenda_number
→ 안건별 AI 요약
→ todos JSON 필드 포함 (V4)
Case 4: 추출된 Todo 조회
─────────────────────────────────────────────────────────────
SELECT * FROM todos
WHERE meeting_id = 'meeting-001'
AND extracted_by = 'AI' [V3]
ORDER BY priority DESC, due_date ASC
또는 agenda_sections의 JSON todos 필드 사용
└──────────────────────────────────────────────────────────────────────────────┘
```
---
## 2. 상태 전이 다이어그램 (State Transition)
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ meetings 테이블 상태 │
└─────────────────────────────────────────────────────────────────────────────┘
[생성]
├─────────────────────────┐
▼ │
SCHEDULED │ (시간 경과)
(scheduled_at 설정) │
│ │
│ start_meeting API │
▼ │
IN_PROGRESS │
(started_at 설정) │
│ │
│ end_meeting API [V3] │
▼ │
COMPLETED │
(ended_at 설정) [V3 추가] ├─────────────────────────┐
│ │ │
└─────────────────────────┘ │
│ 회의록 최종화 │
│ (finalize_minutes API) │
▼ │
minutes: FINALIZED │
(status='FINALIZED') │
│ │
│ (비동기 이벤트) │
▼ │
AI 분석 완료 │
agenda_sections 생성 │
ai_summaries 생성 │
todos 추출 │
│ │
└─────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ minutes 테이블 상태 │
└─────────────────────────────────────────────────────────────────────────────┘
CREATE DRAFT
(minutes 생성) ───────────► (사용자 작성 중)
update_minutes API
(섹션 추가/수정)
finalize_minutes API
FINALIZED
(AI 분석 대기 중)
(비동기 처리 완료)
분석 완료 (상태 유지)
agenda_sections 생성됨
ai_summaries 생성됨
┌─────────────────────────────────────────────────────────────────────────────┐
│ minutes_sections 잠금 상태 │
└─────────────────────────────────────────────────────────────────────────────┘
편집 가능
(locked=FALSE)
│ finalize_minutes
잠금됨
(locked=TRUE, locked_by=user_id)
└─────► 수정 불가
verified=TRUE
┌─────────────────────────────────────────────────────────────────────────────┐
│ todos 완료 상태 │
└─────────────────────────────────────────────────────────────────────────────┘
PENDING
(생성됨)
│ todo 완료 API
COMPLETED
(completed_at 설정)
```
---
## 3. 사용자별 회의록 데이터 구조
```
┌──────────────────────────────────────────────────────────────────────────────┐
│ 1개 회의 (meetings: meeting-001)
│ ├─ 참석자: user1, user2, user3
└──────────────────────────────────────────────────────────────────────────────┘
회의 종료 → minutes 테이블에 여러 레코드 생성
┌─────────────────────────────────────────────────────────────────┐
│ minutes 테이블 (3개 레코드 생성) │
├─────────────────────────────────────────────────────────────────┤
│ id │ meeting_id │ user_id │ status
├─────────────────────┼─────────────┼──────────────────────┼────────
│ consol-minutes-001 │ meeting-001 │ NULL [V3] │ DRAFT
│ user1-minutes-001 │ meeting-001 │ user1@example.com │ DRAFT
│ user2-minutes-001 │ meeting-001 │ user2@example.com │ DRAFT
│ user3-minutes-001 │ meeting-001 │ user3@example.com │ DRAFT
└─────────────────────┴─────────────┴──────────────────────┴────────
↓ (각각 minutes_sections 참조)
┌─────────────────────────────────────────────────────────────────┐
│ minutes_sections 테이블 (4그룹 × N개 섹션) │
├─────────────────────────────────────────────────────────────────┤
│ id │ minutes_id │ type │ title │ content
├────────┼────────────────────┼─────────────┼──────────┼─────────
│ sec-1 │ consol-minutes-001 │ DISCUSSION │ 안건1 │ "AI가..."
│ sec-2 │ consol-minutes-001 │ DECISION │ 결정1 │ "..."
│ │ │ │ │
│ sec-3 │ user1-minutes-001 │ DISCUSSION │ 안건1 │ "사용자1..."
│ sec-4 │ user1-minutes-001 │ DISCUSSION │ 안건2 │ "..."
│ │ │ │ │
│ sec-5 │ user2-minutes-001 │ DISCUSSION │ 안건1 │ "사용자2..."
│ sec-6 │ user2-minutes-001 │ DECISION │ 결정1 │ "..."
│ │ │ │ │
│ sec-7 │ user3-minutes-001 │ DISCUSSION │ 안건1 │ "사용자3..."
└────────┴────────────────────┴─────────────┴──────────┴─────────
각 사용자가 독립적으로 작성:
- User1: consol-minutes-001의 sec-3, sec-4 편집
- User2: user2-minutes-001의 sec-5, sec-6 편집
- User3: user3-minutes-001의 sec-7 편집
AI 분석 (user_id=NULL인 것만):
┌─────────────────────────────────────────────────────────────────┐
│ agenda_sections 테이블 │
├─────────────────────────────────────────────────────────────────┤
│ id │ minutes_id │ meeting_id │ agenda_number
├────────┼────────────────────┼─────────────┼──────────────────
│ ag-1 │ consol-minutes-001 │ meeting-001 │ 1
│ ag-2 │ consol-minutes-001 │ meeting-001 │ 2
└────────┴────────────────────┴─────────────┴──────────────────
→ minutes_id를 통해 통합 회의록만 참조
→ user_id='user1@example.com'인 회의록은 참조하지 않음
```
---
## 4. 인덱스 활용 쿼리 예시
```sql
-- 쿼리 1: 특정 회의의 통합 회의록 조회 (V3 인덱스 활용)
SELECT * FROM minutes
WHERE meeting_id = 'meeting-001' AND user_id IS NULL
ORDER BY created_at DESC;
└─► 인덱스: idx_minutes_meeting_user (meeting_id, user_id)
-- 쿼리 2: 특정 사용자의 회의록 조회 (복합 인덱스 활용)
SELECT * FROM minutes
WHERE meeting_id = 'meeting-001' AND user_id = 'user1@example.com'
ORDER BY created_at DESC;
└─► 인덱스: idx_minutes_meeting_user (meeting_id, user_id)
-- 쿼리 3: 안건별 AI 요약 조회 (V3 인덱스 활용)
SELECT * FROM agenda_sections
WHERE meeting_id = 'meeting-001'
ORDER BY agenda_number ASC;
└─► 인덱스: idx_sections_meeting (meeting_id)
-- 쿼리 4: 특정 안건의 세부 요약 (복합 인덱스 활용)
SELECT * FROM agenda_sections
WHERE meeting_id = 'meeting-001' AND agenda_number = 1;
└─► 인덱스: idx_sections_agenda (meeting_id, agenda_number)
-- 쿼리 5: AI 추출 Todo 조회 (V3 인덱스 활용)
SELECT * FROM todos
WHERE meeting_id = 'meeting-001' AND extracted_by = 'AI'
ORDER BY priority DESC, due_date ASC;
└─► 인덱스: idx_todos_extracted (extracted_by)
└─► 인덱스: idx_todos_meeting (meeting_id)
-- 쿼리 6: 특정 회의의 모든 데이터 조회 (JOIN)
SELECT
m.*,
ms.content,
ag.ai_summary_short,
ag.todos,
ai.keywords
FROM minutes m
LEFT JOIN minutes_sections ms ON m.id = ms.minutes_id
LEFT JOIN agenda_sections ag ON m.id = ag.minutes_id
LEFT JOIN ai_summaries ai ON m.meeting_id = ai.meeting_id
WHERE m.meeting_id = 'meeting-001' AND m.user_id IS NULL
ORDER BY ms.order ASC, ag.agenda_number ASC;
└─► 인덱스: idx_minutes_meeting_user (meeting_id, user_id)
└─► 인덱스: idx_sections_minutes (minutes_id)
```
---
## 5. 데이터 저장 크기 예상
```
1개 회의 (참석자 5명) 데이터 크기:
├─ meetings: ~500 bytes
├─ meeting_participants (5명): ~5 × 150 = 750 bytes
├─ minutes (6개: 1 통합 + 5 개인): ~6 × 400 = 2.4 KB
├─ minutes_sections (30개 섹션): ~30 × 2 KB = 60 KB
├─ agenda_sections (5개 안건): ~5 × 4 KB = 20 KB
├─ ai_summaries: ~10 KB
└─ todos (8개): ~8 × 800 bytes = 6.4 KB
Total: ~100 KB/회의
1년 (250개 회의) 예상:
└─► 25 MB + 인덱스 ~5 MB = ~30 MB
JSON 필드 데이터 크기:
├─ agenda_sections.decisions: ~200 bytes/건
├─ agenda_sections.opinions: ~300 bytes/건
├─ agenda_sections.todos: ~500 bytes/건 [V4]
├─ ai_summaries.result: ~5-10 KB/건
└─ ai_summaries.statistics: ~200 bytes/건
```

View File

@ -0,0 +1,130 @@
@startuml Meeting Service Database Schema
!theme mono
'=== Core Tables ===
entity "meetings" {
* **meeting_id : VARCHAR(50)
--
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
organizer_id : VARCHAR(50) NOT NULL
created_at : TIMESTAMP
updated_at : TIMESTAMP
template_id : VARCHAR(50)
}
entity "meeting_participants" {
* **meeting_id : VARCHAR(50) [FK]
* **user_id : VARCHAR(100)
--
invitation_status : VARCHAR(20)
attended : BOOLEAN
created_at : TIMESTAMP
updated_at : TIMESTAMP
}
entity "minutes" {
* **id : VARCHAR(50)
--
meeting_id : VARCHAR(50) [FK] NOT NULL
user_id : VARCHAR(100) [V3]
title : VARCHAR(200) NOT NULL
status : VARCHAR(20) NOT NULL
version : INT NOT NULL
created_by : VARCHAR(50) NOT NULL
finalized_by : VARCHAR(50)
finalized_at : TIMESTAMP
created_at : TIMESTAMP
updated_at : TIMESTAMP
}
entity "minutes_sections" {
* **id : VARCHAR(50)
--
minutes_id : VARCHAR(50) [FK] NOT NULL
type : VARCHAR(50) NOT NULL
title : VARCHAR(200) NOT NULL
**content : TEXT
order : INT
verified : BOOLEAN
locked : BOOLEAN
locked_by : VARCHAR(50)
created_at : TIMESTAMP
updated_at : TIMESTAMP
}
'=== V3 New Tables ===
entity "agenda_sections" {
* **id : VARCHAR(36)
--
minutes_id : VARCHAR(36) [FK] NOT NULL
meeting_id : VARCHAR(50) [FK] NOT NULL
agenda_number : INT NOT NULL
agenda_title : VARCHAR(200) NOT NULL
ai_summary_short : TEXT
discussions : TEXT
decisions : JSON
pending_items : JSON
opinions : JSON
**todos : JSON [V4]
created_at : TIMESTAMP
updated_at : TIMESTAMP
}
entity "ai_summaries" {
* **id : VARCHAR(36)
--
meeting_id : VARCHAR(50) [FK] NOT NULL
summary_type : VARCHAR(50) NOT NULL
source_minutes_ids : JSON NOT NULL
result : JSON NOT NULL
processing_time_ms : INT
model_version : VARCHAR(50)
keywords : JSON
statistics : JSON
created_at : TIMESTAMP
}
entity "todos" {
* **todo_id : VARCHAR(50)
--
meeting_id : VARCHAR(50) [FK] NOT NULL
minutes_id : VARCHAR(50) [FK]
title : VARCHAR(200) NOT NULL
description : TEXT
assignee_id : VARCHAR(50) NOT NULL
due_date : DATE
status : VARCHAR(20) NOT NULL
priority : VARCHAR(20)
extracted_by : VARCHAR(50) [V3]
section_reference : VARCHAR(200) [V3]
extraction_confidence : DECIMAL(3,2) [V3]
completed_at : TIMESTAMP
created_at : TIMESTAMP
updated_at : TIMESTAMP
}
'=== Relationships ===
meetings ||--o{ meeting_participants : "1:N [V2]"
meetings ||--o{ minutes : "1:N"
meetings ||--o{ agenda_sections : "1:N [V3]"
meetings ||--o{ ai_summaries : "1:N [V3]"
meetings ||--o{ todos : "1:N"
minutes ||--o{ minutes_sections : "1:N"
minutes ||--o{ agenda_sections : "1:N [V3]"
'=== Legend ===
legend right
V2 = Migration 2 (2025-10-27)
V3 = Migration 3 (2025-10-28)
V4 = Migration 4 (2025-10-28)
[FK] = Foreign Key
**bold** = Important fields
end legend
@enduml

View File

@ -0,0 +1,675 @@
# 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 테이블

9657
logs/ai-python-final.log Normal file

File diff suppressed because it is too large Load Diff

271
logs/ai-python-restart.log Normal file
View File

@ -0,0 +1,271 @@
INFO: Will watch for changes in these directories: ['/Users/jominseo/HGZero/ai-python']
INFO: Uvicorn running on http://0.0.0.0:8086 (Press CTRL+C to quit)
INFO: Started reloader process [5213] using WatchFiles
INFO: Started server process [5216]
INFO: Waiting for application startup.
2025-10-27 16:56:26,595 - main - INFO - ============================================================
2025-10-27 16:56:26,595 - main - INFO - AI Service (Python) 시작 - Port: 8086
2025-10-27 16:56:26,595 - main - INFO - Claude Model: claude-3-5-sonnet-20241022
2025-10-27 16:56:26,595 - main - INFO - Redis: 20.249.177.114:6379
2025-10-27 16:56:26,595 - main - INFO - ============================================================
2025-10-27 16:56:26,595 - main - INFO - Event Hub 리스너 백그라운드 시작...
2025-10-27 16:56:26,595 - app.services.eventhub_service - INFO - Event Hub 리스너 시작
INFO: Application startup complete.
2025-10-27 16:56:26,694 - app.services.redis_service - INFO - Redis 연결 성공
2025-10-27 16:56:26,694 - azure.eventhub.aio._eventprocessor.event_processor - INFO - EventProcessor 'd8ad9755-1457-4010-9b6e-5796106dddb1' is being started
2025-10-27 16:56:26,791 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: None -> <ConnectionState.START: 0>
2025-10-27 16:56:26,830 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.START: 0> -> <ConnectionState.HDR_SENT: 2>
2025-10-27 16:56:26,830 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.HDR_SENT: 2> -> <ConnectionState.HDR_SENT: 2>
2025-10-27 16:56:26,830 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.HDR_SENT: 2> -> <ConnectionState.OPEN_PIPE: 4>
2025-10-27 16:56:26,831 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.UNMAPPED: 0> -> <SessionState.BEGIN_SENT: 1>
2025-10-27 16:56:26,831 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:26,831 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:26,831 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:26,831 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:26,847 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.OPEN_PIPE: 4> -> <ConnectionState.OPEN_SENT: 7>
2025-10-27 16:56:26,899 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.OPEN_SENT: 7> -> <ConnectionState.OPENED: 9>
2025-10-27 16:56:26,950 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.BEGIN_SENT: 1> -> <SessionState.MAPPED: 3>
2025-10-27 16:56:27,001 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:56:27,001 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:56:27,053 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:56:27,053 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:56:27,053 - azure.eventhub._pyamqp.aio._cbs_async - INFO - CBS completed opening with status: <ManagementOpenResult.OK: 1>
2025-10-27 16:56:27,259 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:27,259 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:27,260 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:27,260 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:27,269 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:56:27,269 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:56:27,270 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:56:27,270 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:56:27,280 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:56:27,280 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:56:27,281 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:56:27,281 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:56:27,281 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:56:27,281 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:56:27,281 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:56:27,281 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:56:27,281 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.MAPPED: 3> -> <SessionState.END_SENT: 4>
2025-10-27 16:56:27,281 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.OPENED: 9> -> <ConnectionState.CLOSE_SENT: 11>
2025-10-27 16:56:27,281 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.CLOSE_SENT: 11> -> <ConnectionState.END: 13>
2025-10-27 16:56:27,281 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.END_SENT: 4> -> <SessionState.DISCARDING: 6>
2025-10-27 16:56:27,282 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:27,282 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:27,282 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:27,282 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:27,282 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:27,282 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:27,282 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:27,282 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:27,283 - azure.eventhub.aio._eventprocessor.event_processor - INFO - EventProcessor 'd8ad9755-1457-4010-9b6e-5796106dddb1' has claimed partition '0'
2025-10-27 16:56:27,283 - azure.eventhub.aio._eventprocessor.event_processor - INFO - start ownership '0', checkpoint None
2025-10-27 16:56:27,351 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: None -> <ConnectionState.START: 0>
2025-10-27 16:56:27,369 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.START: 0> -> <ConnectionState.HDR_SENT: 2>
2025-10-27 16:56:27,369 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.HDR_SENT: 2> -> <ConnectionState.HDR_SENT: 2>
2025-10-27 16:56:27,369 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.HDR_SENT: 2> -> <ConnectionState.OPEN_PIPE: 4>
2025-10-27 16:56:27,370 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.UNMAPPED: 0> -> <SessionState.BEGIN_SENT: 1>
2025-10-27 16:56:27,370 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:27,370 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:27,370 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:27,370 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:27,384 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.OPEN_PIPE: 4> -> <ConnectionState.OPEN_SENT: 7>
2025-10-27 16:56:27,436 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.OPEN_SENT: 7> -> <ConnectionState.OPENED: 9>
2025-10-27 16:56:27,489 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.BEGIN_SENT: 1> -> <SessionState.MAPPED: 3>
2025-10-27 16:56:27,539 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:56:27,539 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:56:27,590 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:56:27,591 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:56:27,591 - azure.eventhub._pyamqp.aio._cbs_async - INFO - CBS completed opening with status: <ManagementOpenResult.OK: 1>
2025-10-27 16:56:27,797 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:28,219 - azure.eventhub._pyamqp.aio._link_async - INFO - Cannot get source or target. Detaching link
2025-10-27 16:56:28,219 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:28,219 - azure.eventhub._pyamqp.aio._session_async - ERROR - Unable to attach new link: ValueError('Invalid link')
2025-10-27 16:56:28,271 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ERROR: 6>
2025-10-27 16:56:28,322 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:56:28,322 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:56:28,322 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:56:28,322 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:56:28,322 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.MAPPED: 3> -> <SessionState.END_SENT: 4>
2025-10-27 16:56:28,322 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.OPENED: 9> -> <ConnectionState.CLOSE_SENT: 11>
2025-10-27 16:56:28,323 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.CLOSE_SENT: 11> -> <ConnectionState.END: 13>
2025-10-27 16:56:28,323 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.END_SENT: 4> -> <SessionState.DISCARDING: 6>
2025-10-27 16:56:28,323 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:28,323 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:28,323 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:28,323 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:28,323 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ERROR: 6> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:28,356 - azure.eventhub.aio._eventprocessor.event_processor - WARNING - EventProcessor instance 'd8ad9755-1457-4010-9b6e-5796106dddb1' of eventhub 'hgzero-eventhub-name' partition '0' consumer group '$Default'. An error occurred while receiving. The exception is ConnectionLostError("At least one receiver for the endpoint is created with epoch of '0', and so non-epoch receiver is not allowed. Either reconnect with a higher epoch, or make sure all epoch receivers are closed or disconnected. TrackingId:abd872e1-fef7-4e54-a3d1-655839f07e2f_B0, SystemTracker:hgzero-eventhub-ns:eventhub:hgzero-eventhub-name~32766, Timestamp:2025-10-27T07:56:27\nReference:2cf2b525-24ca-4693-99a0-364f8a5c24c9\nTrackingId:960af1bc-38da-483a-a249-fb509f7ba524_B0\nSystemTracker:hgzero-eventhub-ns:eventhub:hgzero-eventhub-name~32766|$default\nTimestamp:2025-10-27T07:56:28 TrackingId:4a8b6c79abe243a3a753b14892c87299_G10, SystemTracker:gateway5, Timestamp:2025-10-27T07:56:28").
2025-10-27 16:56:28,357 - app.services.eventhub_service - ERROR - Event Hub 에러 - Partition: 0, Error: At least one receiver for the endpoint is created with epoch of '0', and so non-epoch receiver is not allowed. Either reconnect with a higher epoch, or make sure all epoch receivers are closed or disconnected. TrackingId:abd872e1-fef7-4e54-a3d1-655839f07e2f_B0, SystemTracker:hgzero-eventhub-ns:eventhub:hgzero-eventhub-name~32766, Timestamp:2025-10-27T07:56:27
Reference:2cf2b525-24ca-4693-99a0-364f8a5c24c9
TrackingId:960af1bc-38da-483a-a249-fb509f7ba524_B0
SystemTracker:hgzero-eventhub-ns:eventhub:hgzero-eventhub-name~32766|$default
Timestamp:2025-10-27T07:56:28 TrackingId:4a8b6c79abe243a3a753b14892c87299_G10, SystemTracker:gateway5, Timestamp:2025-10-27T07:56:28
2025-10-27 16:56:28,357 - azure.eventhub.aio._eventprocessor.event_processor - INFO - EventProcessor instance 'd8ad9755-1457-4010-9b6e-5796106dddb1' of eventhub 'hgzero-eventhub-name' partition '0' consumer group '$Default' is being closed. Reason is: <CloseReason.OWNERSHIP_LOST: 1>
INFO: 127.0.0.1:64478 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:64540 - "OPTIONS /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 400 Bad Request
2025-10-27 16:56:58,387 - azure.eventhub.aio._eventprocessor.event_processor - INFO - EventProcessor 'd8ad9755-1457-4010-9b6e-5796106dddb1' has claimed partition '0'
2025-10-27 16:56:58,388 - azure.eventhub.aio._eventprocessor.event_processor - INFO - start ownership '0', checkpoint None
2025-10-27 16:56:58,445 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: None -> <ConnectionState.START: 0>
2025-10-27 16:56:58,463 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.START: 0> -> <ConnectionState.HDR_SENT: 2>
2025-10-27 16:56:58,463 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.HDR_SENT: 2> -> <ConnectionState.HDR_SENT: 2>
2025-10-27 16:56:58,464 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.HDR_SENT: 2> -> <ConnectionState.OPEN_PIPE: 4>
2025-10-27 16:56:58,464 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.UNMAPPED: 0> -> <SessionState.BEGIN_SENT: 1>
2025-10-27 16:56:58,464 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:58,465 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:58,465 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:58,465 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:58,476 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.OPEN_PIPE: 4> -> <ConnectionState.OPEN_SENT: 7>
2025-10-27 16:56:58,527 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.OPEN_SENT: 7> -> <ConnectionState.OPENED: 9>
2025-10-27 16:56:58,578 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.BEGIN_SENT: 1> -> <SessionState.MAPPED: 3>
2025-10-27 16:56:58,630 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:56:58,630 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:56:58,682 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:56:58,682 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:56:58,682 - azure.eventhub._pyamqp.aio._cbs_async - INFO - CBS completed opening with status: <ManagementOpenResult.OK: 1>
2025-10-27 16:56:58,888 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:56:59,392 - azure.eventhub._pyamqp.aio._link_async - INFO - Cannot get source or target. Detaching link
2025-10-27 16:56:59,392 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:59,393 - azure.eventhub._pyamqp.aio._session_async - ERROR - Unable to attach new link: ValueError('Invalid link')
2025-10-27 16:56:59,444 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ERROR: 6>
2025-10-27 16:56:59,496 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:56:59,496 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:56:59,497 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:56:59,498 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:56:59,501 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.MAPPED: 3> -> <SessionState.END_SENT: 4>
2025-10-27 16:56:59,501 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.OPENED: 9> -> <ConnectionState.CLOSE_SENT: 11>
2025-10-27 16:56:59,502 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.CLOSE_SENT: 11> -> <ConnectionState.END: 13>
2025-10-27 16:56:59,502 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.END_SENT: 4> -> <SessionState.DISCARDING: 6>
2025-10-27 16:56:59,502 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:59,503 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:59,503 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:59,503 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:59,503 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ERROR: 6> -> <LinkState.DETACHED: 0>
2025-10-27 16:56:59,511 - azure.eventhub.aio._eventprocessor.event_processor - WARNING - EventProcessor instance 'd8ad9755-1457-4010-9b6e-5796106dddb1' of eventhub 'hgzero-eventhub-name' partition '0' consumer group '$Default'. An error occurred while receiving. The exception is ConnectionLostError("At least one receiver for the endpoint is created with epoch of '0', and so non-epoch receiver is not allowed. Either reconnect with a higher epoch, or make sure all epoch receivers are closed or disconnected. TrackingId:abd872e1-fef7-4e54-a3d1-655839f07e2f_B0, SystemTracker:hgzero-eventhub-ns:eventhub:hgzero-eventhub-name~32766, Timestamp:2025-10-27T07:56:58\nReference:498e817b-ab02-4cb8-b89c-de104a008916\nTrackingId:960af1bc-38da-483a-a249-fb509f7ba524_B0\nSystemTracker:hgzero-eventhub-ns:eventhub:hgzero-eventhub-name~32766|$default\nTimestamp:2025-10-27T07:56:59 TrackingId:5adceb1c6eb94c568a0621f417ea3787_G21, SystemTracker:gateway5, Timestamp:2025-10-27T07:56:59").
2025-10-27 16:56:59,511 - app.services.eventhub_service - ERROR - Event Hub 에러 - Partition: 0, Error: At least one receiver for the endpoint is created with epoch of '0', and so non-epoch receiver is not allowed. Either reconnect with a higher epoch, or make sure all epoch receivers are closed or disconnected. TrackingId:abd872e1-fef7-4e54-a3d1-655839f07e2f_B0, SystemTracker:hgzero-eventhub-ns:eventhub:hgzero-eventhub-name~32766, Timestamp:2025-10-27T07:56:58
Reference:498e817b-ab02-4cb8-b89c-de104a008916
TrackingId:960af1bc-38da-483a-a249-fb509f7ba524_B0
SystemTracker:hgzero-eventhub-ns:eventhub:hgzero-eventhub-name~32766|$default
Timestamp:2025-10-27T07:56:59 TrackingId:5adceb1c6eb94c568a0621f417ea3787_G21, SystemTracker:gateway5, Timestamp:2025-10-27T07:56:59
2025-10-27 16:56:59,511 - azure.eventhub.aio._eventprocessor.event_processor - INFO - EventProcessor instance 'd8ad9755-1457-4010-9b6e-5796106dddb1' of eventhub 'hgzero-eventhub-name' partition '0' consumer group '$Default' is being closed. Reason is: <CloseReason.OWNERSHIP_LOST: 1>
2025-10-27 16:57:01,004 - watchfiles.main - INFO - 3 changes detected
WARNING: WatchFiles detected changes in 'main.py'. Reloading...
INFO: Shutting down
INFO: Waiting for application shutdown.
2025-10-27 16:57:01,147 - main - INFO - AI Service 종료
INFO: Application shutdown complete.
INFO: Finished server process [5216]
2025-10-27 16:57:01,148 - azure.eventhub.aio._eventprocessor.event_processor - INFO - EventProcessor 'd8ad9755-1457-4010-9b6e-5796106dddb1' tasks have been cancelled.
2025-10-27 16:57:01,148 - azure.eventhub.aio._eventprocessor.event_processor - INFO - EventProcessor 'd8ad9755-1457-4010-9b6e-5796106dddb1' has been stopped.
2025-10-27 16:57:01,148 - app.services.redis_service - INFO - Redis 연결 종료
INFO: Started server process [5285]
INFO: Waiting for application startup.
2025-10-27 16:57:01,645 - main - INFO - ============================================================
2025-10-27 16:57:01,645 - main - INFO - AI Service (Python) 시작 - Port: 8086
2025-10-27 16:57:01,645 - main - INFO - Claude Model: claude-3-5-sonnet-20241022
2025-10-27 16:57:01,645 - main - INFO - Redis: 20.249.177.114:6379
2025-10-27 16:57:01,645 - main - INFO - ============================================================
2025-10-27 16:57:01,645 - main - INFO - Event Hub 리스너 백그라운드 시작...
2025-10-27 16:57:01,645 - app.services.eventhub_service - INFO - Event Hub 리스너 시작
INFO: Application startup complete.
2025-10-27 16:57:01,680 - app.services.redis_service - INFO - Redis 연결 성공
2025-10-27 16:57:01,680 - azure.eventhub.aio._eventprocessor.event_processor - INFO - EventProcessor 'eaf4d6e1-1d77-4786-a0c5-46a0fb009df7' is being started
2025-10-27 16:57:01,718 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: None -> <ConnectionState.START: 0>
2025-10-27 16:57:01,727 - watchfiles.main - INFO - 3 changes detected
2025-10-27 16:57:01,733 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.START: 0> -> <ConnectionState.HDR_SENT: 2>
2025-10-27 16:57:01,733 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.HDR_SENT: 2> -> <ConnectionState.HDR_SENT: 2>
2025-10-27 16:57:01,733 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.HDR_SENT: 2> -> <ConnectionState.OPEN_PIPE: 4>
2025-10-27 16:57:01,733 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.UNMAPPED: 0> -> <SessionState.BEGIN_SENT: 1>
2025-10-27 16:57:01,733 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:57:01,733 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:57:01,733 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:57:01,733 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:57:01,744 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.OPEN_PIPE: 4> -> <ConnectionState.OPEN_SENT: 7>
2025-10-27 16:57:01,795 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.OPEN_SENT: 7> -> <ConnectionState.OPENED: 9>
2025-10-27 16:57:01,846 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.BEGIN_SENT: 1> -> <SessionState.MAPPED: 3>
2025-10-27 16:57:01,898 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:57:01,898 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:57:01,948 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:57:01,949 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:57:01,949 - azure.eventhub._pyamqp.aio._cbs_async - INFO - CBS completed opening with status: <ManagementOpenResult.OK: 1>
2025-10-27 16:57:02,164 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:57:02,164 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:57:02,164 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:57:02,164 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:57:02,175 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:57:02,175 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:57:02,176 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:57:02,176 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:57:02,191 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:57:02,191 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:57:02,191 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:57:02,191 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:57:02,191 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:57:02,192 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:57:02,192 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:57:02,192 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:57:02,192 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.MAPPED: 3> -> <SessionState.END_SENT: 4>
2025-10-27 16:57:02,192 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.OPENED: 9> -> <ConnectionState.CLOSE_SENT: 11>
2025-10-27 16:57:02,192 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.CLOSE_SENT: 11> -> <ConnectionState.END: 13>
2025-10-27 16:57:02,192 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.END_SENT: 4> -> <SessionState.DISCARDING: 6>
2025-10-27 16:57:02,192 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:57:02,192 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:57:02,192 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:57:02,192 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:57:02,192 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:57:02,193 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:57:02,193 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:57:02,193 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:57:02,194 - azure.eventhub.aio._eventprocessor.event_processor - INFO - EventProcessor 'eaf4d6e1-1d77-4786-a0c5-46a0fb009df7' has claimed partition '0'
2025-10-27 16:57:02,194 - azure.eventhub.aio._eventprocessor.event_processor - INFO - start ownership '0', checkpoint None
2025-10-27 16:57:02,272 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: None -> <ConnectionState.START: 0>
2025-10-27 16:57:02,291 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.START: 0> -> <ConnectionState.HDR_SENT: 2>
2025-10-27 16:57:02,292 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.HDR_SENT: 2> -> <ConnectionState.HDR_SENT: 2>
2025-10-27 16:57:02,292 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.HDR_SENT: 2> -> <ConnectionState.OPEN_PIPE: 4>
2025-10-27 16:57:02,292 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.UNMAPPED: 0> -> <SessionState.BEGIN_SENT: 1>
2025-10-27 16:57:02,292 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:57:02,292 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:57:02,292 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:57:02,292 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:57:02,301 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.OPEN_PIPE: 4> -> <ConnectionState.OPEN_SENT: 7>
2025-10-27 16:57:02,352 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.OPEN_SENT: 7> -> <ConnectionState.OPENED: 9>
2025-10-27 16:57:02,403 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.BEGIN_SENT: 1> -> <SessionState.MAPPED: 3>
2025-10-27 16:57:02,454 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:57:02,454 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:57:02,505 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:57:02,505 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.ATTACHED: 3>
2025-10-27 16:57:02,505 - azure.eventhub._pyamqp.aio._cbs_async - INFO - CBS completed opening with status: <ManagementOpenResult.OK: 1>
2025-10-27 16:57:02,712 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ATTACH_SENT: 1>
2025-10-27 16:57:03,217 - azure.eventhub._pyamqp.aio._link_async - INFO - Cannot get source or target. Detaching link
2025-10-27 16:57:03,217 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACH_SENT: 1> -> <LinkState.DETACHED: 0>
2025-10-27 16:57:03,217 - azure.eventhub._pyamqp.aio._session_async - ERROR - Unable to attach new link: ValueError('Invalid link')
2025-10-27 16:57:03,269 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACHED: 0> -> <LinkState.ERROR: 6>
2025-10-27 16:57:03,321 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:57:03,321 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:57:03,321 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:57:03,321 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.ATTACHED: 3> -> <LinkState.DETACH_SENT: 4>
2025-10-27 16:57:03,321 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.MAPPED: 3> -> <SessionState.END_SENT: 4>
2025-10-27 16:57:03,322 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.OPENED: 9> -> <ConnectionState.CLOSE_SENT: 11>
2025-10-27 16:57:03,322 - azure.eventhub._pyamqp.aio._connection_async - INFO - Connection state changed: <ConnectionState.CLOSE_SENT: 11> -> <ConnectionState.END: 13>
2025-10-27 16:57:03,322 - azure.eventhub._pyamqp.aio._session_async - INFO - Session state changed: <SessionState.END_SENT: 4> -> <SessionState.DISCARDING: 6>
2025-10-27 16:57:03,322 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:57:03,322 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link sender state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:57:03,322 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:57:03,322 - azure.eventhub._pyamqp.aio._management_link_async - INFO - Management link receiver state changed: <LinkState.DETACH_SENT: 4> -> <LinkState.DETACHED: 0>
2025-10-27 16:57:03,322 - azure.eventhub._pyamqp.aio._link_async - INFO - Link state changed: <LinkState.ERROR: 6> -> <LinkState.DETACHED: 0>
2025-10-27 16:57:03,363 - azure.eventhub.aio._eventprocessor.event_processor - WARNING - EventProcessor instance 'eaf4d6e1-1d77-4786-a0c5-46a0fb009df7' of eventhub 'hgzero-eventhub-name' partition '0' consumer group '$Default'. An error occurred while receiving. The exception is ConnectionLostError("At least one receiver for the endpoint is created with epoch of '0', and so non-epoch receiver is not allowed. Either reconnect with a higher epoch, or make sure all epoch receivers are closed or disconnected. TrackingId:abd872e1-fef7-4e54-a3d1-655839f07e2f_B0, SystemTracker:hgzero-eventhub-ns:eventhub:hgzero-eventhub-name~32766, Timestamp:2025-10-27T07:57:02\nReference:e43740f8-3462-4cc1-8df9-3941156b9220\nTrackingId:960af1bc-38da-483a-a249-fb509f7ba524_B0\nSystemTracker:hgzero-eventhub-ns:eventhub:hgzero-eventhub-name~32766|$default\nTimestamp:2025-10-27T07:57:03 TrackingId:96b29734d6424cf6a253a767a1108d7d_G27, SystemTracker:gateway5, Timestamp:2025-10-27T07:57:03").
2025-10-27 16:57:03,363 - app.services.eventhub_service - ERROR - Event Hub 에러 - Partition: 0, Error: At least one receiver for the endpoint is created with epoch of '0', and so non-epoch receiver is not allowed. Either reconnect with a higher epoch, or make sure all epoch receivers are closed or disconnected. TrackingId:abd872e1-fef7-4e54-a3d1-655839f07e2f_B0, SystemTracker:hgzero-eventhub-ns:eventhub:hgzero-eventhub-name~32766, Timestamp:2025-10-27T07:57:02
Reference:e43740f8-3462-4cc1-8df9-3941156b9220
TrackingId:960af1bc-38da-483a-a249-fb509f7ba524_B0
SystemTracker:hgzero-eventhub-ns:eventhub:hgzero-eventhub-name~32766|$default
Timestamp:2025-10-27T07:57:03 TrackingId:96b29734d6424cf6a253a767a1108d7d_G27, SystemTracker:gateway5, Timestamp:2025-10-27T07:57:03
2025-10-27 16:57:03,363 - azure.eventhub.aio._eventprocessor.event_processor - INFO - EventProcessor instance 'eaf4d6e1-1d77-4786-a0c5-46a0fb009df7' of eventhub 'hgzero-eventhub-name' partition '0' consumer group '$Default' is being closed. Reason is: <CloseReason.OWNERSHIP_LOST: 1>
INFO: Shutting down
INFO: Waiting for application shutdown.
2025-10-27 16:57:10,125 - main - INFO - AI Service 종료
INFO: Application shutdown complete.
INFO: Finished server process [5285]
2025-10-27 16:57:10,126 - azure.eventhub.aio._eventprocessor.event_processor - INFO - EventProcessor 'eaf4d6e1-1d77-4786-a0c5-46a0fb009df7' tasks have been cancelled.
2025-10-27 16:57:10,126 - azure.eventhub.aio._eventprocessor.event_processor - INFO - EventProcessor 'eaf4d6e1-1d77-4786-a0c5-46a0fb009df7' has been stopped.
2025-10-27 16:57:10,126 - app.services.redis_service - INFO - Redis 연결 종료
INFO: Stopping reloader process [5213]

385
logs/ai-python.log Normal file
View File

@ -0,0 +1,385 @@
INFO: Will watch for changes in these directories: ['/Users/jominseo/HGZero/ai-python']
INFO: Uvicorn running on http://0.0.0.0:8087 (Press CTRL+C to quit)
INFO: Started reloader process [32757] using WatchFiles
INFO: Started server process [32759]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: 127.0.0.1:49960 - "GET /health HTTP/1.1" 200 OK
INFO: 127.0.0.1:52590 - "GET /health HTTP/1.1" 200 OK
INFO: 127.0.0.1:54439 - "POST /api/v1/transcripts/consolidate HTTP/1.1" 422 Unprocessable Content
2025-10-28 16:43:13,742 - watchfiles.main - INFO - 28 changes detected
WARNING: WatchFiles detected changes in 'app/__init__.py', 'app/services/__init__.py', 'app/prompts/consolidate_prompt.py', 'app/models/transcript.py', 'app/models/response.py', 'app/models/keyword.py', 'app/services/transcript_service.py', 'main.py', 'app/services/claude_service.py', 'app/api/v1/transcripts.py', 'app/models/__init__.py', 'app/models/todo.py', 'app/services/eventhub_service.py', 'app/services/redis_service.py', 'app/config.py', 'app/api/__init__.py', 'app/api/v1/suggestions.py', 'app/api/v1/__init__.py'. Reloading...
INFO: Shutting down
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Finished server process [32759]
Traceback (most recent call last):
File "<string>", line 1, in <module>
from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=7)
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 122, in spawn_main
exitcode = _main(fd, parent_sentinel)
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 131, in _main
prepare(preparation_data)
~~~~~~~^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 246, in prepare
_fixup_main_from_path(data['init_main_from_path'])
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 297, in _fixup_main_from_path
main_content = runpy.run_path(main_path,
run_name="__mp_main__")
File "<frozen runpy>", line 287, in run_path
File "<frozen runpy>", line 98, in _run_module_code
File "<frozen runpy>", line 88, in _run_code
File "/Users/jominseo/HGZero/ai-python/main.py", line 9, in <module>
from app.config import get_settings
ModuleNotFoundError: No module named 'app.config'
2025-10-28 16:43:14,368 - watchfiles.main - INFO - 3 changes detected
2025-10-28 16:45:19,161 - watchfiles.main - INFO - 3 changes detected
WARNING: WatchFiles detected changes in 'main.py'. Reloading...
Traceback (most recent call last):
File "<string>", line 1, in <module>
from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=7)
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 122, in spawn_main
exitcode = _main(fd, parent_sentinel)
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 131, in _main
prepare(preparation_data)
~~~~~~~^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 246, in prepare
_fixup_main_from_path(data['init_main_from_path'])
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 297, in _fixup_main_from_path
main_content = runpy.run_path(main_path,
run_name="__mp_main__")
File "<frozen runpy>", line 287, in run_path
File "<frozen runpy>", line 98, in _run_module_code
File "<frozen runpy>", line 88, in _run_code
File "/Users/jominseo/HGZero/ai-python/main.py", line 9, in <module>
from app.config import get_settings
ModuleNotFoundError: No module named 'app.config'
2025-10-28 16:45:36,697 - watchfiles.main - INFO - 3 changes detected
WARNING: WatchFiles detected changes in 'app/api/v1/suggestions.py'. Reloading...
Traceback (most recent call last):
File "<string>", line 1, in <module>
from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=7)
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 122, in spawn_main
exitcode = _main(fd, parent_sentinel)
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 131, in _main
prepare(preparation_data)
~~~~~~~^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 246, in prepare
_fixup_main_from_path(data['init_main_from_path'])
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 297, in _fixup_main_from_path
main_content = runpy.run_path(main_path,
run_name="__mp_main__")
File "<frozen runpy>", line 287, in run_path
File "<frozen runpy>", line 98, in _run_module_code
File "<frozen runpy>", line 88, in _run_code
File "/Users/jominseo/HGZero/ai-python/main.py", line 9, in <module>
from app.config import get_settings
ModuleNotFoundError: No module named 'app.config'
2025-10-28 16:45:46,675 - watchfiles.main - INFO - 3 changes detected
WARNING: WatchFiles detected changes in 'main.py'. Reloading...
Traceback (most recent call last):
File "<string>", line 1, in <module>
from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=7)
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 122, in spawn_main
exitcode = _main(fd, parent_sentinel)
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 131, in _main
prepare(preparation_data)
~~~~~~~^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 246, in prepare
_fixup_main_from_path(data['init_main_from_path'])
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 297, in _fixup_main_from_path
main_content = runpy.run_path(main_path,
run_name="__mp_main__")
File "<frozen runpy>", line 287, in run_path
File "<frozen runpy>", line 98, in _run_module_code
File "<frozen runpy>", line 88, in _run_code
File "/Users/jominseo/HGZero/ai-python/main.py", line 9, in <module>
from app.config import get_settings
ModuleNotFoundError: No module named 'app.config'
127.0.0.1:51583 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:51605 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:51636 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:51648 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:51669 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:51691 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:51724 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:51742 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:51772 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:51793 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:51811 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:51835 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:51855 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:51875 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:51901 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:51927 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:51950 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:51980 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52006 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52021 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52049 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52077 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52095 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52130 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52157 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52179 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52225 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52249 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52284 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52316 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52343 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52369 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52393 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52420 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52435 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52457 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52493 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52529 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52581 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52633 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52666 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52716 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52770 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52812 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52859 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52902 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52940 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:52966 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53029 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53081 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53123 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53173 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53201 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53251 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53286 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53331 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53365 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53389 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53440 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53465 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53485 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53523 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53562 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53588 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53626 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53662 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53696 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53728 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53778 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53820 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53865 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53903 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53944 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53964 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53984 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54017 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54035 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54057 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54079 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54105 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54135 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54177 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54227 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54249 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54269 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54301 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54324 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54370 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54411 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54460 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54483 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54510 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54534 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54558 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54583 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54606 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54636 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54656 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54677 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54701 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54721 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54748 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54767 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54788 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54817 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54839 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54859 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54883 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54895 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54933 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54962 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:54988 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55020 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55042 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55070 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55090 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55129 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55155 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55208 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55229 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55260 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55273 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55317 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55351 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55365 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55388 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55420 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55444 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55466 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55484 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55503 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55526 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55544 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55579 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55597 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55632 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55639 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55666 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55684 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55708 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55735 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55765 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55781 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55812 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55838 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55862 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55888 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55926 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55942 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:55981 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56251 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56299 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56329 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56354 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56370 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56397 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56419 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56444 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56483 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56502 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56530 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56551 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56573 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56595 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56634 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56682 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56721 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56802 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56827 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56844 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56882 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56921 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56953 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:57003 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:57025 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:57054 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:57079 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:57099 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:57127 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:57153 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:57200 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:57223 - "GET /api/v1/ai/suggestions/meetings/test-meeting-001/stream HTTP/1.1" 404 Not Found
2025-10-28 16:43:13,721 - watchfiles.main - INFO - 28 changes detected
WARNING: WatchFiles detected changes in 'app/models/keyword.py', 'app/api/v1/__init__.py', 'app/config.py', 'main.py', 'app/services/transcript_service.py', 'app/services/eventhub_service.py', 'app/services/claude_service.py', 'app/models/transcript.py', 'app/api/v1/transcripts.py', 'app/services/redis_service.py', 'app/api/v1/suggestions.py', 'app/__init__.py', 'app/models/todo.py', 'app/models/response.py', 'app/services/__init__.py', 'app/prompts/consolidate_prompt.py', 'app/models/__init__.py', 'app/api/__init__.py'. Reloading...
INFO: Shutting down
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Finished server process [32637]
Traceback (most recent call last):
File "<string>", line 1, in <module>
from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=7)
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 122, in spawn_main
exitcode = _main(fd, parent_sentinel)
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 131, in _main
prepare(preparation_data)
~~~~~~~^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 246, in prepare
_fixup_main_from_path(data['init_main_from_path'])
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 297, in _fixup_main_from_path
main_content = runpy.run_path(main_path,
run_name="__mp_main__")
File "<frozen runpy>", line 287, in run_path
File "<frozen runpy>", line 98, in _run_module_code
File "<frozen runpy>", line 88, in _run_code
File "/Users/jominseo/HGZero/ai-python/main.py", line 9, in <module>
from app.config import get_settings
ModuleNotFoundError: No module named 'app.config'
2025-10-28 16:43:14,362 - watchfiles.main - INFO - 3 changes detected
2025-10-28 16:45:19,160 - watchfiles.main - INFO - 3 changes detected
WARNING: WatchFiles detected changes in 'main.py'. Reloading...
Traceback (most recent call last):
File "<string>", line 1, in <module>
from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=7)
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 122, in spawn_main
exitcode = _main(fd, parent_sentinel)
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 131, in _main
prepare(preparation_data)
~~~~~~~^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 246, in prepare
_fixup_main_from_path(data['init_main_from_path'])
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 297, in _fixup_main_from_path
main_content = runpy.run_path(main_path,
run_name="__mp_main__")
File "<frozen runpy>", line 287, in run_path
File "<frozen runpy>", line 98, in _run_module_code
File "<frozen runpy>", line 88, in _run_code
File "/Users/jominseo/HGZero/ai-python/main.py", line 9, in <module>
from app.config import get_settings
ModuleNotFoundError: No module named 'app.config'
2025-10-28 16:45:36,697 - watchfiles.main - INFO - 3 changes detected
WARNING: WatchFiles detected changes in 'app/api/v1/suggestions.py'. Reloading...
Traceback (most recent call last):
File "<string>", line 1, in <module>
from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=7)
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 122, in spawn_main
exitcode = _main(fd, parent_sentinel)
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 131, in _main
prepare(preparation_data)
~~~~~~~^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 246, in prepare
_fixup_main_from_path(data['init_main_from_path'])
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 297, in _fixup_main_from_path
main_content = runpy.run_path(main_path,
run_name="__mp_main__")
File "<frozen runpy>", line 287, in run_path
File "<frozen runpy>", line 98, in _run_module_code
File "<frozen runpy>", line 88, in _run_code
File "/Users/jominseo/HGZero/ai-python/main.py", line 9, in <module>
from app.config import get_settings
ModuleNotFoundError: No module named 'app.config'
2025-10-28 16:45:46,663 - watchfiles.main - INFO - 3 changes detected
WARNING: WatchFiles detected changes in 'main.py'. Reloading...
Traceback (most recent call last):
File "<string>", line 1, in <module>
from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=7)
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 122, in spawn_main
exitcode = _main(fd, parent_sentinel)
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 131, in _main
prepare(preparation_data)
~~~~~~~^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 246, in prepare
_fixup_main_from_path(data['init_main_from_path'])
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/lib/python3.13/multiprocessing/spawn.py", line 297, in _fixup_main_from_path
main_content = runpy.run_path(main_path,
run_name="__mp_main__")
File "<frozen runpy>", line 287, in run_path
File "<frozen runpy>", line 98, in _run_module_code
File "<frozen runpy>", line 88, in _run_code
File "/Users/jominseo/HGZero/ai-python/main.py", line 9, in <module>
from app.config import get_settings
ModuleNotFoundError: No module named 'app.config'

2
logs/api-test-result.log Normal file
View File

@ -0,0 +1,2 @@
curl: option : blank argument where content is expected
curl: try 'curl --help' or 'curl --manual' for more information

3
logs/meeting-service.log Normal file
View File

@ -0,0 +1,3 @@
[INFO] Project root: /Users/jominseo/HGZero
[INFO] Reading run configuration files...
[ERROR] No execution configurations found

3
logs/stt-restart.log Normal file
View File

@ -0,0 +1,3 @@
[INFO] Project root: /Users/jominseo/HGZero
[INFO] Reading run configuration files...
[ERROR] No execution configurations found

3
logs/stt-service.log Normal file
View File

@ -0,0 +1,3 @@
[INFO] Project root: /Users/jominseo/HGZero
[INFO] Reading run configuration files...
[ERROR] No execution configurations found

View File

@ -0,0 +1,56 @@
# Minutes Sections 테이블 에러 빠른 해결 가이드
## 🚨 발생한 에러
```
Caused by: org.postgresql.util.PSQLException:
ERROR: column "id" of relation "minutes_sections" contains null values
```
## ✅ 해결 방법 (2단계)
### 1단계: 데이터베이스 정리
IntelliJ에서 다음 중 하나를 실행:
**방법 A: 직접 SQL 실행**
```sql
DELETE FROM minutes_sections WHERE id IS NULL;
```
**방법 B: cleanup-minutes-sections.sql 파일 실행**
1. IntelliJ Database 탭 열기
2. `meetingdb` 우클릭 → New → Query Console
3. `cleanup-minutes-sections.sql` 파일 내용 복사 & 실행
### 2단계: Meeting 서비스 재시작
IntelliJ Run Configuration에서 Meeting 서비스 재시작
## 📝 수정된 파일
1. **test-data-minutes-sections.sql**
- Entity 구조에 맞게 컬럼명 수정
- `id` 컬럼 추가 (필수)
- `type`, `title`, `order` 등 추가
- `section_number`, `section_title` 제거
2. **cleanup-minutes-sections.sql**
- null id 레코드 삭제 스크립트
3. **README-FIX-MINUTES-SECTIONS.md**
- 상세 문제 해결 가이드
## 🔍 확인 사항
서비스 시작 후 로그 확인:
```bash
tail -f logs/meeting-service.log
```
에러가 없으면 성공! 다음 단계로 진행하세요.
## 📚 참고
- Entity: `MinutesSectionEntity.java`
- Repository: `MinutesSectionRepository.java` (필요시 생성)
- Service: `EndMeetingService.java`

View File

@ -0,0 +1,72 @@
# minutes_sections 테이블 에러 해결 가이드
## 문제 상황
Meeting 서비스 시작 시 다음 에러 발생:
```
Caused by: org.postgresql.util.PSQLException: ERROR: column "id" of relation "minutes_sections" contains null values
```
## 원인
- `minutes_sections` 테이블에 null id를 가진 레코드가 존재
- Hibernate가 id 컬럼을 NOT NULL PRIMARY KEY로 변경하려 시도
- 기존 null 데이터 때문에 ALTER TABLE 실패
## 해결 방법
### 방법 1: IntelliJ Database 도구 사용 (권장)
1. IntelliJ에서 Database 탭 열기
2. `meetingdb` 데이터베이스 연결
3. Query Console 열기
4. 다음 SQL 실행:
```sql
-- null id를 가진 레코드 삭제
DELETE FROM minutes_sections WHERE id IS NULL;
-- 결과 확인
SELECT COUNT(*) FROM minutes_sections;
```
### 방법 2: cleanup-minutes-sections.sql 파일 실행
IntelliJ Database Console에서 `cleanup-minutes-sections.sql` 파일을 열어서 실행
## 실행 후
1. Meeting 서비스 재시작
2. 로그에서 에러가 없는지 확인:
```bash
tail -f logs/meeting-service.log | grep -i error
```
3. 정상 시작되면 테스트 진행
## 추가 정보
### 테이블 구조 확인
```sql
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_name = 'minutes_sections'
ORDER BY ordinal_position;
```
### 현재 데이터 확인
```sql
SELECT id, minutes_id, type, title FROM minutes_sections LIMIT 10;
```
### Flyway 마이그레이션 이력 확인
```sql
SELECT * FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 5;
```
## 참고사항
- 이 에러는 기존 테이블에 데이터가 있는 상태에서 Entity 구조가 변경되어 발생
- 향후 같은 문제를 방지하려면 Flyway 마이그레이션 파일로 스키마 변경을 관리해야 함
- 테스트 데이터는 `test-data-minutes-sections.sql` 파일 참조

View File

@ -0,0 +1,18 @@
-- minutes 테이블 구조 확인
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_name = 'minutes'
ORDER BY ordinal_position;
-- Primary Key 확인
SELECT
kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_name = 'minutes'
AND tc.constraint_type = 'PRIMARY KEY';

View File

@ -0,0 +1,40 @@
#!/bin/bash
# minutes_sections 테이블 정리 스크립트
# 목적: null id를 가진 레코드 삭제
echo "========================================="
echo "minutes_sections 테이블 정리 시작"
echo "========================================="
# PostgreSQL 연결 정보
DB_HOST="localhost"
DB_PORT="5432"
DB_NAME="meetingdb"
DB_USER="postgres"
# 1. 기존 데이터 확인
echo ""
echo "1. 현재 테이블 상태 확인..."
docker exec -i postgres-meeting psql -U $DB_USER -d $DB_NAME -c "SELECT COUNT(*) as total_rows FROM minutes_sections;"
docker exec -i postgres-meeting psql -U $DB_USER -d $DB_NAME -c "SELECT COUNT(*) as null_id_rows FROM minutes_sections WHERE id IS NULL;"
# 2. null id를 가진 레코드 삭제
echo ""
echo "2. null id를 가진 레코드 삭제..."
docker exec -i postgres-meeting psql -U $DB_USER -d $DB_NAME -c "DELETE FROM minutes_sections WHERE id IS NULL;"
# 3. 정리 완료 확인
echo ""
echo "3. 테이블 정리 완료. 현재 상태:"
docker exec -i postgres-meeting psql -U $DB_USER -d $DB_NAME -c "SELECT COUNT(*) as remaining_rows FROM minutes_sections;"
# 4. 테이블 구조 확인
echo ""
echo "4. 테이블 구조 확인:"
docker exec -i postgres-meeting psql -U $DB_USER -d $DB_NAME -c "\d minutes_sections"
echo ""
echo "========================================="
echo "정리 완료! Meeting 서비스를 재시작하세요."
echo "========================================="

View File

@ -0,0 +1,26 @@
-- ========================================
-- minutes_sections 테이블 정리 SQL
-- ========================================
-- 목적: null id를 가진 레코드 삭제하여 서비스 시작 가능하게 함
-- 실행방법: IntelliJ Database 도구에서 실행
-- 1. 현재 상태 확인
SELECT 'Total rows:' as info, COUNT(*) as count FROM minutes_sections
UNION ALL
SELECT 'Null ID rows:', COUNT(*) FROM minutes_sections WHERE id IS NULL;
-- 2. null id를 가진 레코드 삭제
DELETE FROM minutes_sections WHERE id IS NULL;
-- 3. 결과 확인
SELECT 'Remaining rows:' as info, COUNT(*) as count FROM minutes_sections;
-- 4. 테이블 구조 확인
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_name = 'minutes_sections'
ORDER BY ordinal_position;

View File

@ -0,0 +1,39 @@
-- 직접 실행: minutes_sections 테이블 재생성
-- 1. 기존 테이블 삭제
DROP TABLE IF EXISTS minutes_sections CASCADE;
-- 2. 테이블 재생성
CREATE TABLE minutes_sections (
id VARCHAR(50) PRIMARY KEY,
minutes_id VARCHAR(50) NOT NULL,
type VARCHAR(50),
title VARCHAR(200),
content TEXT,
"order" INTEGER,
verified BOOLEAN DEFAULT FALSE,
locked BOOLEAN DEFAULT FALSE,
locked_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_minutes_sections_minutes
FOREIGN KEY (minutes_id) REFERENCES minutes(id)
ON DELETE CASCADE
);
-- 3. 인덱스 생성
CREATE INDEX idx_minutes_sections_minutes ON minutes_sections(minutes_id);
CREATE INDEX idx_minutes_sections_order ON minutes_sections(minutes_id, "order");
CREATE INDEX idx_minutes_sections_type ON minutes_sections(type);
CREATE INDEX idx_minutes_sections_verified ON minutes_sections(verified);
-- 4. 트리거 생성
DROP TRIGGER IF EXISTS update_minutes_sections_updated_at ON minutes_sections;
CREATE TRIGGER update_minutes_sections_updated_at
BEFORE UPDATE ON minutes_sections
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- 확인
SELECT 'minutes_sections 테이블이 성공적으로 생성되었습니다!' as status;

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -4,18 +4,19 @@ import com.unicorn.hgzero.meeting.biz.domain.MeetingAnalysis;
import com.unicorn.hgzero.meeting.biz.dto.MeetingEndDTO;
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.EndMeetingUseCase;
import com.unicorn.hgzero.meeting.infra.client.AIServiceClient;
import com.unicorn.hgzero.meeting.infra.dto.ai.AgendaSummaryDTO;
import com.unicorn.hgzero.meeting.infra.dto.ai.ConsolidateRequest;
import com.unicorn.hgzero.meeting.infra.dto.ai.ConsolidateResponse;
import com.unicorn.hgzero.meeting.infra.dto.ai.ExtractedTodoDTO;
import com.unicorn.hgzero.meeting.infra.dto.ai.ParticipantMinutesDTO;
import com.unicorn.hgzero.meeting.infra.gateway.entity.AgendaSectionEntity;
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingAnalysisEntity;
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingEntity;
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesEntity;
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesSectionEntity;
import com.unicorn.hgzero.meeting.infra.gateway.entity.TodoEntity;
import com.unicorn.hgzero.meeting.infra.gateway.repository.AgendaSectionJpaRepository;
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingAnalysisJpaRepository;
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingJpaRepository;
import com.unicorn.hgzero.meeting.infra.gateway.repository.MinutesJpaRepository;
import com.unicorn.hgzero.meeting.infra.gateway.repository.MinutesSectionJpaRepository;
import com.unicorn.hgzero.meeting.infra.gateway.repository.TodoJpaRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -25,6 +26,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@ -39,7 +41,8 @@ import java.util.stream.Collectors;
public class EndMeetingService implements EndMeetingUseCase {
private final MeetingJpaRepository meetingRepository;
private final AgendaSectionJpaRepository agendaRepository;
private final MinutesJpaRepository minutesRepository;
private final MinutesSectionJpaRepository minutesSectionRepository;
private final TodoJpaRepository todoRepository;
private final MeetingAnalysisJpaRepository analysisRepository;
private final AIServiceClient aiServiceClient;
@ -59,13 +62,26 @@ public class EndMeetingService implements EndMeetingUseCase {
MeetingEntity meeting = meetingRepository.findById(meetingId)
.orElseThrow(() -> new IllegalArgumentException("회의를 찾을 수 없습니다: " + meetingId));
// 2. 안건 목록 조회 (실제로는 참석자별 메모 섹션)
List<AgendaSectionEntity> agendaSections = agendaRepository.findByMeetingIdOrderByAgendaNumberAsc(meetingId);
// 2. 참석자별 회의록 조회 (userId가 있는 회의록들)
List<MinutesEntity> participantMinutesList = minutesRepository.findByMeetingIdAndUserIdIsNotNull(meetingId);
// 3. AI 통합 분석 요청 데이터 생성
ConsolidateRequest request = createConsolidateRequest(meeting, agendaSections);
if (participantMinutesList.isEmpty()) {
throw new IllegalStateException("참석자 회의록이 없습니다: " + meetingId);
}
// 4. AI Service 호출
// 3. 회의록의 sections 조회 통합
List<MinutesSectionEntity> allMinutesSections = new ArrayList<>();
for (MinutesEntity minutes : participantMinutesList) {
List<MinutesSectionEntity> sections = minutesSectionRepository.findByMinutesIdOrderByOrderAsc(
minutes.getMinutesId()
);
allMinutesSections.addAll(sections);
}
// 4. AI 통합 분석 요청 데이터 생성
ConsolidateRequest request = createConsolidateRequest(meeting, allMinutesSections, participantMinutesList);
// 5. AI Service 호출
ConsolidateResponse aiResponse = aiServiceClient.consolidateMinutes(request);
// 5. AI 분석 결과 저장
@ -74,25 +90,38 @@ public class EndMeetingService implements EndMeetingUseCase {
// 6. Todo 생성 저장
List<TodoEntity> todos = createAndSaveTodos(meeting, aiResponse, analysis);
// 7. 회의 종료 처리
// 6. 회의 종료 처리
meeting.end();
meetingRepository.save(meeting);
// 8. 응답 DTO 생성
return createMeetingEndDTO(meeting, analysis, todos, agendaSections.size());
// 7. 응답 DTO 생성
return createMeetingEndDTO(meeting, analysis, todos, participantMinutesList.size());
}
/**
* AI 통합 분석 요청 데이터 생성
* 참석자별 회의록의 섹션들을 참석자별로 그룹화하여 AI 요청 데이터 생성
*/
private ConsolidateRequest createConsolidateRequest(MeetingEntity meeting, List<AgendaSectionEntity> agendaSections) {
// 참석자별 회의록 변환 (AgendaSection ParticipantMinutes)
List<ParticipantMinutesDTO> participantMinutes = agendaSections.stream()
.<ParticipantMinutesDTO>map(section -> ParticipantMinutesDTO.builder()
.userId(section.getMeetingId()) // 실제로는 participantId 필요
.userName(section.getAgendaTitle()) // 실제로는 participantName 필요
.content(section.getDiscussions() != null ? section.getDiscussions() : "")
.build())
private ConsolidateRequest createConsolidateRequest(
MeetingEntity meeting,
List<MinutesSectionEntity> allMinutesSections,
List<MinutesEntity> participantMinutesList) {
// 참석자별 회의록을 ParticipantMinutesDTO로 변환
List<ParticipantMinutesDTO> participantMinutes = participantMinutesList.stream()
.<ParticipantMinutesDTO>map(minutes -> {
// 해당 회의록의 섹션들만 필터링
String content = allMinutesSections.stream()
.filter(section -> section.getMinutesId().equals(minutes.getMinutesId()))
.<String>map(section -> section.getTitle() + "\n" + section.getContent())
.collect(Collectors.joining("\n\n"));
return ParticipantMinutesDTO.builder()
.userId(minutes.getUserId())
.userName(minutes.getUserId()) // 실제로는 userName이 필요하지만 일단 userId 사용
.content(content)
.build();
})
.collect(Collectors.toList());
return ConsolidateRequest.builder()

View File

@ -46,7 +46,7 @@ public class AIServiceClient {
log.info("AI Service 호출 - 회의록 통합 요약: {}", request.getMeetingId());
try {
String url = aiServiceUrl + "/api/v1/transcripts/consolidate";
String url = aiServiceUrl + "/api/transcripts/consolidate";
// HTTP 헤더 설정
HttpHeaders headers = new HttpHeaders();

View File

@ -37,10 +37,6 @@ public class MinutesEntity extends BaseTimeEntity {
@Column(name = "title", length = 200, nullable = false)
private String title;
@OneToMany(mappedBy = "minutes", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<MinutesSectionEntity> sections = new ArrayList<>();
@Column(name = "status", length = 20, nullable = false)
@Builder.Default
private String status = "DRAFT";
@ -64,9 +60,7 @@ public class MinutesEntity extends BaseTimeEntity {
.meetingId(this.meetingId)
.userId(this.userId)
.title(this.title)
.sections(this.sections.stream()
.map(MinutesSectionEntity::toDomain)
.collect(Collectors.toList()))
.sections(List.of()) // sections는 별도 조회 필요
.status(this.status)
.version(this.version)
.createdBy(this.createdBy)
@ -83,11 +77,6 @@ public class MinutesEntity extends BaseTimeEntity {
.meetingId(minutes.getMeetingId())
.userId(minutes.getUserId())
.title(minutes.getTitle())
.sections(minutes.getSections() != null
? minutes.getSections().stream()
.map(MinutesSectionEntity::fromDomain)
.collect(Collectors.toList())
: new ArrayList<>())
.status(minutes.getStatus())
.version(minutes.getVersion())
.createdBy(minutes.getCreatedBy())

View File

@ -10,6 +10,8 @@ import lombok.NoArgsConstructor;
/**
* 회의록 섹션 Entity
* 참석자가 작성한 메모를 안건별로 저장
* AI 분석의 입력 데이터로 사용됨
*/
@Entity
@Table(name = "minutes_sections")
@ -20,43 +22,36 @@ import lombok.NoArgsConstructor;
public class MinutesSectionEntity extends BaseTimeEntity {
@Id
@Column(name = "section_id", length = 50)
private String sectionId;
@Column(name = "id", length = 50)
private String id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "minutes_id", nullable = false)
private MinutesEntity minutes;
@Column(name = "minutes_id", insertable = false, updatable = false)
@Column(name = "minutes_id", nullable = false, length = 50)
private String minutesId;
@Column(name = "type", length = 50, nullable = false)
@Column(name = "type", length = 50)
private String type;
@Column(name = "title", length = 200, nullable = false)
@Column(name = "title", length = 200)
private String title;
@Column(name = "content", columnDefinition = "TEXT")
private String content;
@Column(name = "\"order\"")
@Builder.Default
private Integer order = 0;
@Column(name = "order")
private Integer order;
@Column(name = "verified", nullable = false)
@Builder.Default
private Boolean verified = false;
@Column(name = "verified")
private Boolean verified;
@Column(name = "locked", nullable = false)
@Builder.Default
private Boolean locked = false;
@Column(name = "locked")
private Boolean locked;
@Column(name = "locked_by", length = 50)
private String lockedBy;
public MinutesSection toDomain() {
return MinutesSection.builder()
.sectionId(this.sectionId)
.sectionId(this.id)
.minutesId(this.minutesId)
.type(this.type)
.title(this.title)
@ -70,7 +65,7 @@ public class MinutesSectionEntity extends BaseTimeEntity {
public static MinutesSectionEntity fromDomain(MinutesSection section) {
return MinutesSectionEntity.builder()
.sectionId(section.getSectionId())
.id(section.getSectionId())
.minutesId(section.getMinutesId())
.type(section.getType())
.title(section.getTitle())
@ -82,6 +77,10 @@ public class MinutesSectionEntity extends BaseTimeEntity {
.build();
}
public void verify() {
this.verified = true;
}
public void lock(String userId) {
this.locked = true;
this.lockedBy = userId;
@ -91,8 +90,4 @@ public class MinutesSectionEntity extends BaseTimeEntity {
this.locked = false;
this.lockedBy = null;
}
public void verify() {
this.verified = true;
}
}

View File

@ -28,7 +28,7 @@ spring:
use_sql_comments: true
dialect: org.hibernate.dialect.PostgreSQLDialect
hibernate:
ddl-auto: ${JPA_DDL_AUTO:update}
ddl-auto: ${JPA_DDL_AUTO:none}
# Redis Configuration
data:

View File

@ -0,0 +1,52 @@
-- ========================================
-- V5: minutes_sections 테이블 재생성
-- ========================================
-- 작성일: 2025-10-28
-- 설명: minutes_sections 테이블을 Entity 구조에 맞게 재생성
-- 1. 기존 테이블이 있으면 삭제
DROP TABLE IF EXISTS minutes_sections CASCADE;
-- 2. Entity 구조에 맞는 테이블 생성
CREATE TABLE minutes_sections (
id VARCHAR(50) PRIMARY KEY,
minutes_id VARCHAR(50) NOT NULL,
type VARCHAR(50),
title VARCHAR(200),
content TEXT,
"order" INTEGER,
verified BOOLEAN DEFAULT FALSE,
locked BOOLEAN DEFAULT FALSE,
locked_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_minutes_sections_minutes
FOREIGN KEY (minutes_id) REFERENCES minutes(id)
ON DELETE CASCADE
);
-- 3. 인덱스 생성
CREATE INDEX idx_minutes_sections_minutes ON minutes_sections(minutes_id);
CREATE INDEX idx_minutes_sections_order ON minutes_sections(minutes_id, "order");
CREATE INDEX idx_minutes_sections_type ON minutes_sections(type);
CREATE INDEX idx_minutes_sections_verified ON minutes_sections(verified);
-- 4. 코멘트 추가
COMMENT ON TABLE minutes_sections IS '참석자별 회의록 안건 섹션 - AI 통합 회의록 생성 입력 데이터';
COMMENT ON COLUMN minutes_sections.id IS '섹션 고유 ID';
COMMENT ON COLUMN minutes_sections.minutes_id IS '참석자별 회의록 ID (minutes.id 참조)';
COMMENT ON COLUMN minutes_sections.type IS '섹션 타입 (AGENDA: 안건, DISCUSSION: 논의사항, DECISION: 결정사항 등)';
COMMENT ON COLUMN minutes_sections.title IS '섹션 제목';
COMMENT ON COLUMN minutes_sections.content IS '섹션 내용 (참석자가 작성한 메모)';
COMMENT ON COLUMN minutes_sections."order" IS '섹션 순서';
COMMENT ON COLUMN minutes_sections.verified IS '검증 완료 여부';
COMMENT ON COLUMN minutes_sections.locked IS '편집 잠금 여부';
COMMENT ON COLUMN minutes_sections.locked_by IS '잠금 설정한 사용자 ID';
-- 5. updated_at 자동 업데이트 트리거
DROP TRIGGER IF EXISTS update_minutes_sections_updated_at ON minutes_sections;
CREATE TRIGGER update_minutes_sections_updated_at
BEFORE UPDATE ON minutes_sections
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

View File

@ -0,0 +1,111 @@
package com.unicorn.hgzero.meeting.manual;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.JdbcTemplate;
/**
* 테스트 데이터 삽입 스크립트
* 실행: ./gradlew :meeting:bootRun --args='--spring.profiles.active=test'
*/
@SpringBootApplication(scanBasePackages = "com.unicorn.hgzero.meeting")
public class InsertTestData {
public static void main(String[] args) {
SpringApplication.run(InsertTestData.class, args);
}
@Bean
public CommandLineRunner insertData(JdbcTemplate jdbcTemplate) {
return args -> {
System.out.println("===== 테스트 데이터 삽입 시작 =====");
// 1. 참석자 회의록 삽입
insertParticipantMinutes(jdbcTemplate);
// 2. 회의록 섹션 삽입
insertMinutesSections(jdbcTemplate);
// 3. 데이터 확인
verifyData(jdbcTemplate);
System.out.println("===== 테스트 데이터 삽입 완료 =====");
};
}
private void insertParticipantMinutes(JdbcTemplate jdbc) {
System.out.println("참석자 회의록 삽입 중...");
String[] inserts = {
"INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at) " +
"VALUES ('minutes-user1', 'meeting-123', 'user-001', '참석자 홍길동 회의록', 'DRAFT', 1, 'user-001', NOW(), NOW()) " +
"ON CONFLICT (minutes_id) DO NOTHING",
"INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at) " +
"VALUES ('minutes-user2', 'meeting-123', 'user-002', '참석자 김철수 회의록', 'DRAFT', 1, 'user-002', NOW(), NOW()) " +
"ON CONFLICT (minutes_id) DO NOTHING",
"INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at) " +
"VALUES ('minutes-user3', 'meeting-123', 'user-003', '참석자 이영희 회의록', 'DRAFT', 1, 'user-003', NOW(), NOW()) " +
"ON CONFLICT (minutes_id) DO NOTHING"
};
for (String sql : inserts) {
jdbc.execute(sql);
}
System.out.println("참석자 회의록 삽입 완료");
}
private void insertMinutesSections(JdbcTemplate jdbc) {
System.out.println("회의록 섹션 삽입 중...");
// 참석자 1 섹션
insertSection(jdbc, "minutes-user1", 1, "프로젝트 목표 논의",
"고객사 요구사항이 명확하지 않아 추가 미팅 필요. 우선순위는 성능 개선으로 결정.");
insertSection(jdbc, "minutes-user1", 2, "기술 스택 검토",
"React와 Spring Boot로 진행하기로 결정. DB는 PostgreSQL 사용.");
// 참석자 2 섹션
insertSection(jdbc, "minutes-user2", 1, "프로젝트 목표 논의",
"성능 개선이 가장 중요. 응답시간 목표는 200ms 이내로 설정.");
insertSection(jdbc, "minutes-user2", 2, "기술 스택 검토",
"캐시 전략으로 Redis 도입 검토 필요. 모니터링 도구는 Prometheus 사용.");
// 참석자 3 섹션
insertSection(jdbc, "minutes-user3", 1, "프로젝트 목표 논의",
"고객사 담당자와 다음 주 화요일에 추가 미팅 예정. 요구사항 명세서 작성 필요.");
insertSection(jdbc, "minutes-user3", 2, "기술 스택 검토",
"UI 라이브러리는 Material-UI 사용. 백엔드는 MSA 아키텍처 검토.");
System.out.println("회의록 섹션 삽입 완료");
}
private void insertSection(JdbcTemplate jdbc, String minutesId, int sectionNum, String title, String content) {
String sql = "INSERT INTO minutes_sections (minutes_id, section_number, section_title, content, created_at) " +
"SELECT id, ?, ?, ?, NOW() FROM minutes WHERE minutes_id = ? " +
"ON CONFLICT DO NOTHING";
jdbc.update(sql, sectionNum, title, content, minutesId);
}
private void verifyData(JdbcTemplate jdbc) {
System.out.println("\n===== 데이터 확인 =====");
Integer minutesCount = jdbc.queryForObject(
"SELECT COUNT(*) FROM minutes WHERE meeting_id = 'meeting-123' AND user_id IS NOT NULL",
Integer.class
);
System.out.println("참석자 회의록 개수: " + minutesCount);
Integer sectionsCount = jdbc.queryForObject(
"SELECT COUNT(*) FROM minutes_sections ms " +
"JOIN minutes m ON ms.minutes_id = m.id " +
"WHERE m.meeting_id = 'meeting-123'",
Integer.class
);
System.out.println("회의록 섹션 개수: " + sectionsCount);
}
}

View File

@ -0,0 +1,130 @@
-- 테스트용 참석자 회의록(minutes) 데이터
-- userId가 있는 회의록들 (참석자별 메모)
-- 참석자 1의 회의록
INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at)
VALUES ('minutes-user1', 'meeting-123', 'user-001', '참석자 홍길동 회의록', 'DRAFT', 1, 'user-001', NOW(), NOW())
ON CONFLICT (minutes_id) DO NOTHING;
-- 참석자 2의 회의록
INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at)
VALUES ('minutes-user2', 'meeting-123', 'user-002', '참석자 김철수 회의록', 'DRAFT', 1, 'user-002', NOW(), NOW())
ON CONFLICT (minutes_id) DO NOTHING;
-- 참석자 3의 회의록
INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at)
VALUES ('minutes-user3', 'meeting-123', 'user-003', '참석자 이영희 회의록', 'DRAFT', 1, 'user-003', NOW(), NOW())
ON CONFLICT (minutes_id) DO NOTHING;
-- minutes_sections 데이터 삽입
-- Entity 구조에 맞게 수정: id, minutes_id, type, title, content, "order", verified, locked, locked_by
-- 참석자 1 (홍길동)의 메모
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
SELECT
'section-user1-1',
'minutes-user1',
'AGENDA',
'프로젝트 목표 논의',
'고객사 요구사항이 명확하지 않아 추가 미팅 필요. 우선순위는 성능 개선으로 결정.',
1,
FALSE,
FALSE,
NULL,
NOW(),
NOW()
WHERE NOT EXISTS (
SELECT 1 FROM minutes_sections WHERE id = 'section-user1-1'
);
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
SELECT
'section-user1-2',
'minutes-user1',
'AGENDA',
'기술 스택 검토',
'React와 Spring Boot로 진행하기로 결정. DB는 PostgreSQL 사용.',
2,
FALSE,
FALSE,
NULL,
NOW(),
NOW()
WHERE NOT EXISTS (
SELECT 1 FROM minutes_sections WHERE id = 'section-user1-2'
);
-- 참석자 2 (김철수)의 메모
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
SELECT
'section-user2-1',
'minutes-user2',
'AGENDA',
'프로젝트 목표 논의',
'성능 개선이 가장 중요. 응답시간 목표는 200ms 이내로 설정.',
1,
FALSE,
FALSE,
NULL,
NOW(),
NOW()
WHERE NOT EXISTS (
SELECT 1 FROM minutes_sections WHERE id = 'section-user2-1'
);
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
SELECT
'section-user2-2',
'minutes-user2',
'AGENDA',
'기술 스택 검토',
'캐시 전략으로 Redis 도입 검토 필요. 모니터링 도구는 Prometheus 사용.',
2,
FALSE,
FALSE,
NULL,
NOW(),
NOW()
WHERE NOT EXISTS (
SELECT 1 FROM minutes_sections WHERE id = 'section-user2-2'
);
-- 참석자 3 (이영희)의 메모
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
SELECT
'section-user3-1',
'minutes-user3',
'AGENDA',
'프로젝트 목표 논의',
'고객사 담당자와 다음 주 화요일에 추가 미팅 예정. 요구사항 명세서 작성 필요.',
1,
FALSE,
FALSE,
NULL,
NOW(),
NOW()
WHERE NOT EXISTS (
SELECT 1 FROM minutes_sections WHERE id = 'section-user3-1'
);
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
SELECT
'section-user3-2',
'minutes-user3',
'AGENDA',
'기술 스택 검토',
'UI 라이브러리는 Material-UI 사용. 백엔드는 MSA 아키텍처 검토.',
2,
FALSE,
FALSE,
NULL,
NOW(),
NOW()
WHERE NOT EXISTS (
SELECT 1 FROM minutes_sections WHERE id = 'section-user3-2'
);
-- 확인 쿼리
SELECT 'Test data inserted successfully!' as status;
SELECT COUNT(*) as minutes_count FROM minutes WHERE meeting_id = 'meeting-123';
SELECT COUNT(*) as sections_count FROM minutes_sections;

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.