mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 21:56:24 +00:00
작업 중: Meeting AI 통합 개발 진행 상황 저장
This commit is contained in:
parent
143721d106
commit
621d4c16df
176
.github/PULL_REQUEST_TEMPLATE_meeting_ai_db.md
vendored
Normal file
176
.github/PULL_REQUEST_TEMPLATE_meeting_ai_db.md
vendored
Normal 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 개발을 시작할 수 있습니다!**
|
||||||
BIN
ai-python/__pycache__/main.cpython-313.pyc
Normal file
BIN
ai-python/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ai-python/app/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
ai-python/app/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ai-python/app/__pycache__/config.cpython-313.pyc
Normal file
BIN
ai-python/app/__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ai-python/app/api/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
ai-python/app/api/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ai-python/app/api/v1/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
ai-python/app/api/v1/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ai-python/app/api/v1/__pycache__/suggestions.cpython-313.pyc
Normal file
BIN
ai-python/app/api/v1/__pycache__/suggestions.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ai-python/app/api/v1/__pycache__/transcripts.cpython-313.pyc
Normal file
BIN
ai-python/app/api/v1/__pycache__/transcripts.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ai-python/app/models/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
ai-python/app/models/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ai-python/app/models/__pycache__/response.cpython-313.pyc
Normal file
BIN
ai-python/app/models/__pycache__/response.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ai-python/app/models/__pycache__/transcript.cpython-313.pyc
Normal file
BIN
ai-python/app/models/__pycache__/transcript.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
ai-python/app/services/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
ai-python/app/services/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
ai-python/app/services/__pycache__/redis_service.cpython-313.pyc
Normal file
BIN
ai-python/app/services/__pycache__/redis_service.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
2
ai-python/logs/ai-python.log
Normal file
2
ai-python/logs/ai-python.log
Normal 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
|
||||||
1577
ai-python/logs/ai-service.log
Normal file
1577
ai-python/logs/ai-service.log
Normal file
File diff suppressed because it is too large
Load Diff
@ -36,7 +36,7 @@ app.add_middleware(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# API 라우터 등록
|
# API 라우터 등록
|
||||||
app.include_router(api_v1_router, prefix="/api/v1")
|
app.include_router(api_v1_router, prefix="/api")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
|
|||||||
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -650,7 +650,7 @@ code + .copy-button {
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
function configurationCacheProblems() { return (
|
function configurationCacheProblems() { return (
|
||||||
// begin-report-data
|
// 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
|
// end-report-data
|
||||||
);}
|
);}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
322
claude/README-SCHEMA-ANALYSIS.md
Normal file
322
claude/README-SCHEMA-ANALYSIS.md
Normal 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
|
||||||
|
**상태**: 완료 및 검증됨
|
||||||
607
claude/SCHEMA-REPORT-SUMMARY.md
Normal file
607
claude/SCHEMA-REPORT-SUMMARY.md
Normal 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
560
claude/data-flow-diagram.md
Normal 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/건
|
||||||
|
```
|
||||||
130
claude/database-diagram.puml
Normal file
130
claude/database-diagram.puml
Normal 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
|
||||||
675
claude/database-schema-analysis.md
Normal file
675
claude/database-schema-analysis.md
Normal 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
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
271
logs/ai-python-restart.log
Normal 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
385
logs/ai-python.log
Normal 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
2
logs/api-test-result.log
Normal 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
3
logs/meeting-service.log
Normal 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
3
logs/stt-restart.log
Normal 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
3
logs/stt-service.log
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[INFO] Project root: /Users/jominseo/HGZero
|
||||||
|
[INFO] Reading run configuration files...
|
||||||
|
[ERROR] No execution configurations found
|
||||||
56
meeting/QUICK-FIX-GUIDE.md
Normal file
56
meeting/QUICK-FIX-GUIDE.md
Normal 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`
|
||||||
72
meeting/README-FIX-MINUTES-SECTIONS.md
Normal file
72
meeting/README-FIX-MINUTES-SECTIONS.md
Normal 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` 파일 참조
|
||||||
18
meeting/check-minutes-table.sql
Normal file
18
meeting/check-minutes-table.sql
Normal 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';
|
||||||
40
meeting/cleanup-minutes-sections.sh
Executable file
40
meeting/cleanup-minutes-sections.sh
Executable 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 "========================================="
|
||||||
26
meeting/cleanup-minutes-sections.sql
Normal file
26
meeting/cleanup-minutes-sections.sql
Normal 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;
|
||||||
39
meeting/fix-minutes-sections-direct.sql
Normal file
39
meeting/fix-minutes-sections-direct.sql
Normal 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
BIN
meeting/logs/meeting-service.log.2025-10-28.0.gz
Normal file
BIN
meeting/logs/meeting-service.log.2025-10-28.0.gz
Normal file
Binary file not shown.
@ -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.dto.MeetingEndDTO;
|
||||||
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.EndMeetingUseCase;
|
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.EndMeetingUseCase;
|
||||||
import com.unicorn.hgzero.meeting.infra.client.AIServiceClient;
|
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.ConsolidateRequest;
|
||||||
import com.unicorn.hgzero.meeting.infra.dto.ai.ConsolidateResponse;
|
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.ExtractedTodoDTO;
|
||||||
import com.unicorn.hgzero.meeting.infra.dto.ai.ParticipantMinutesDTO;
|
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.MeetingAnalysisEntity;
|
||||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingEntity;
|
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.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.MeetingAnalysisJpaRepository;
|
||||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingJpaRepository;
|
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 com.unicorn.hgzero.meeting.infra.gateway.repository.TodoJpaRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -25,6 +26,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@ -39,7 +41,8 @@ import java.util.stream.Collectors;
|
|||||||
public class EndMeetingService implements EndMeetingUseCase {
|
public class EndMeetingService implements EndMeetingUseCase {
|
||||||
|
|
||||||
private final MeetingJpaRepository meetingRepository;
|
private final MeetingJpaRepository meetingRepository;
|
||||||
private final AgendaSectionJpaRepository agendaRepository;
|
private final MinutesJpaRepository minutesRepository;
|
||||||
|
private final MinutesSectionJpaRepository minutesSectionRepository;
|
||||||
private final TodoJpaRepository todoRepository;
|
private final TodoJpaRepository todoRepository;
|
||||||
private final MeetingAnalysisJpaRepository analysisRepository;
|
private final MeetingAnalysisJpaRepository analysisRepository;
|
||||||
private final AIServiceClient aiServiceClient;
|
private final AIServiceClient aiServiceClient;
|
||||||
@ -59,13 +62,26 @@ public class EndMeetingService implements EndMeetingUseCase {
|
|||||||
MeetingEntity meeting = meetingRepository.findById(meetingId)
|
MeetingEntity meeting = meetingRepository.findById(meetingId)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("회의를 찾을 수 없습니다: " + meetingId));
|
.orElseThrow(() -> new IllegalArgumentException("회의를 찾을 수 없습니다: " + meetingId));
|
||||||
|
|
||||||
// 2. 안건 목록 조회 (실제로는 참석자별 메모 섹션)
|
// 2. 참석자별 회의록 조회 (userId가 있는 회의록들)
|
||||||
List<AgendaSectionEntity> agendaSections = agendaRepository.findByMeetingIdOrderByAgendaNumberAsc(meetingId);
|
List<MinutesEntity> participantMinutesList = minutesRepository.findByMeetingIdAndUserIdIsNotNull(meetingId);
|
||||||
|
|
||||||
// 3. AI 통합 분석 요청 데이터 생성
|
if (participantMinutesList.isEmpty()) {
|
||||||
ConsolidateRequest request = createConsolidateRequest(meeting, agendaSections);
|
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);
|
ConsolidateResponse aiResponse = aiServiceClient.consolidateMinutes(request);
|
||||||
|
|
||||||
// 5. AI 분석 결과 저장
|
// 5. AI 분석 결과 저장
|
||||||
@ -74,25 +90,38 @@ public class EndMeetingService implements EndMeetingUseCase {
|
|||||||
// 6. Todo 생성 및 저장
|
// 6. Todo 생성 및 저장
|
||||||
List<TodoEntity> todos = createAndSaveTodos(meeting, aiResponse, analysis);
|
List<TodoEntity> todos = createAndSaveTodos(meeting, aiResponse, analysis);
|
||||||
|
|
||||||
// 7. 회의 종료 처리
|
// 6. 회의 종료 처리
|
||||||
meeting.end();
|
meeting.end();
|
||||||
meetingRepository.save(meeting);
|
meetingRepository.save(meeting);
|
||||||
|
|
||||||
// 8. 응답 DTO 생성
|
// 7. 응답 DTO 생성
|
||||||
return createMeetingEndDTO(meeting, analysis, todos, agendaSections.size());
|
return createMeetingEndDTO(meeting, analysis, todos, participantMinutesList.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI 통합 분석 요청 데이터 생성
|
* AI 통합 분석 요청 데이터 생성
|
||||||
|
* 참석자별 회의록의 섹션들을 참석자별로 그룹화하여 AI 요청 데이터 생성
|
||||||
*/
|
*/
|
||||||
private ConsolidateRequest createConsolidateRequest(MeetingEntity meeting, List<AgendaSectionEntity> agendaSections) {
|
private ConsolidateRequest createConsolidateRequest(
|
||||||
// 참석자별 회의록 변환 (AgendaSection → ParticipantMinutes)
|
MeetingEntity meeting,
|
||||||
List<ParticipantMinutesDTO> participantMinutes = agendaSections.stream()
|
List<MinutesSectionEntity> allMinutesSections,
|
||||||
.<ParticipantMinutesDTO>map(section -> ParticipantMinutesDTO.builder()
|
List<MinutesEntity> participantMinutesList) {
|
||||||
.userId(section.getMeetingId()) // 실제로는 participantId 필요
|
|
||||||
.userName(section.getAgendaTitle()) // 실제로는 participantName 필요
|
// 참석자별 회의록을 ParticipantMinutesDTO로 변환
|
||||||
.content(section.getDiscussions() != null ? section.getDiscussions() : "")
|
List<ParticipantMinutesDTO> participantMinutes = participantMinutesList.stream()
|
||||||
.build())
|
.<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());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
return ConsolidateRequest.builder()
|
return ConsolidateRequest.builder()
|
||||||
|
|||||||
@ -46,7 +46,7 @@ public class AIServiceClient {
|
|||||||
log.info("AI Service 호출 - 회의록 통합 요약: {}", request.getMeetingId());
|
log.info("AI Service 호출 - 회의록 통합 요약: {}", request.getMeetingId());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String url = aiServiceUrl + "/api/v1/transcripts/consolidate";
|
String url = aiServiceUrl + "/api/transcripts/consolidate";
|
||||||
|
|
||||||
// HTTP 헤더 설정
|
// HTTP 헤더 설정
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
|||||||
@ -37,10 +37,6 @@ public class MinutesEntity extends BaseTimeEntity {
|
|||||||
@Column(name = "title", length = 200, nullable = false)
|
@Column(name = "title", length = 200, nullable = false)
|
||||||
private String title;
|
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)
|
@Column(name = "status", length = 20, nullable = false)
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private String status = "DRAFT";
|
private String status = "DRAFT";
|
||||||
@ -64,9 +60,7 @@ public class MinutesEntity extends BaseTimeEntity {
|
|||||||
.meetingId(this.meetingId)
|
.meetingId(this.meetingId)
|
||||||
.userId(this.userId)
|
.userId(this.userId)
|
||||||
.title(this.title)
|
.title(this.title)
|
||||||
.sections(this.sections.stream()
|
.sections(List.of()) // sections는 별도 조회 필요
|
||||||
.map(MinutesSectionEntity::toDomain)
|
|
||||||
.collect(Collectors.toList()))
|
|
||||||
.status(this.status)
|
.status(this.status)
|
||||||
.version(this.version)
|
.version(this.version)
|
||||||
.createdBy(this.createdBy)
|
.createdBy(this.createdBy)
|
||||||
@ -83,11 +77,6 @@ public class MinutesEntity extends BaseTimeEntity {
|
|||||||
.meetingId(minutes.getMeetingId())
|
.meetingId(minutes.getMeetingId())
|
||||||
.userId(minutes.getUserId())
|
.userId(minutes.getUserId())
|
||||||
.title(minutes.getTitle())
|
.title(minutes.getTitle())
|
||||||
.sections(minutes.getSections() != null
|
|
||||||
? minutes.getSections().stream()
|
|
||||||
.map(MinutesSectionEntity::fromDomain)
|
|
||||||
.collect(Collectors.toList())
|
|
||||||
: new ArrayList<>())
|
|
||||||
.status(minutes.getStatus())
|
.status(minutes.getStatus())
|
||||||
.version(minutes.getVersion())
|
.version(minutes.getVersion())
|
||||||
.createdBy(minutes.getCreatedBy())
|
.createdBy(minutes.getCreatedBy())
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import lombok.NoArgsConstructor;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 회의록 섹션 Entity
|
* 회의록 섹션 Entity
|
||||||
|
* 참석자가 작성한 메모를 안건별로 저장
|
||||||
|
* AI 분석의 입력 데이터로 사용됨
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "minutes_sections")
|
@Table(name = "minutes_sections")
|
||||||
@ -20,43 +22,36 @@ import lombok.NoArgsConstructor;
|
|||||||
public class MinutesSectionEntity extends BaseTimeEntity {
|
public class MinutesSectionEntity extends BaseTimeEntity {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@Column(name = "section_id", length = 50)
|
@Column(name = "id", length = 50)
|
||||||
private String sectionId;
|
private String id;
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@Column(name = "minutes_id", nullable = false, length = 50)
|
||||||
@JoinColumn(name = "minutes_id", nullable = false)
|
|
||||||
private MinutesEntity minutes;
|
|
||||||
|
|
||||||
@Column(name = "minutes_id", insertable = false, updatable = false)
|
|
||||||
private String minutesId;
|
private String minutesId;
|
||||||
|
|
||||||
@Column(name = "type", length = 50, nullable = false)
|
@Column(name = "type", length = 50)
|
||||||
private String type;
|
private String type;
|
||||||
|
|
||||||
@Column(name = "title", length = 200, nullable = false)
|
@Column(name = "title", length = 200)
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
@Column(name = "content", columnDefinition = "TEXT")
|
@Column(name = "content", columnDefinition = "TEXT")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
@Column(name = "\"order\"")
|
@Column(name = "order")
|
||||||
@Builder.Default
|
private Integer order;
|
||||||
private Integer order = 0;
|
|
||||||
|
|
||||||
@Column(name = "verified", nullable = false)
|
@Column(name = "verified")
|
||||||
@Builder.Default
|
private Boolean verified;
|
||||||
private Boolean verified = false;
|
|
||||||
|
|
||||||
@Column(name = "locked", nullable = false)
|
@Column(name = "locked")
|
||||||
@Builder.Default
|
private Boolean locked;
|
||||||
private Boolean locked = false;
|
|
||||||
|
|
||||||
@Column(name = "locked_by", length = 50)
|
@Column(name = "locked_by", length = 50)
|
||||||
private String lockedBy;
|
private String lockedBy;
|
||||||
|
|
||||||
public MinutesSection toDomain() {
|
public MinutesSection toDomain() {
|
||||||
return MinutesSection.builder()
|
return MinutesSection.builder()
|
||||||
.sectionId(this.sectionId)
|
.sectionId(this.id)
|
||||||
.minutesId(this.minutesId)
|
.minutesId(this.minutesId)
|
||||||
.type(this.type)
|
.type(this.type)
|
||||||
.title(this.title)
|
.title(this.title)
|
||||||
@ -70,7 +65,7 @@ public class MinutesSectionEntity extends BaseTimeEntity {
|
|||||||
|
|
||||||
public static MinutesSectionEntity fromDomain(MinutesSection section) {
|
public static MinutesSectionEntity fromDomain(MinutesSection section) {
|
||||||
return MinutesSectionEntity.builder()
|
return MinutesSectionEntity.builder()
|
||||||
.sectionId(section.getSectionId())
|
.id(section.getSectionId())
|
||||||
.minutesId(section.getMinutesId())
|
.minutesId(section.getMinutesId())
|
||||||
.type(section.getType())
|
.type(section.getType())
|
||||||
.title(section.getTitle())
|
.title(section.getTitle())
|
||||||
@ -82,6 +77,10 @@ public class MinutesSectionEntity extends BaseTimeEntity {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void verify() {
|
||||||
|
this.verified = true;
|
||||||
|
}
|
||||||
|
|
||||||
public void lock(String userId) {
|
public void lock(String userId) {
|
||||||
this.locked = true;
|
this.locked = true;
|
||||||
this.lockedBy = userId;
|
this.lockedBy = userId;
|
||||||
@ -91,8 +90,4 @@ public class MinutesSectionEntity extends BaseTimeEntity {
|
|||||||
this.locked = false;
|
this.locked = false;
|
||||||
this.lockedBy = null;
|
this.lockedBy = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void verify() {
|
|
||||||
this.verified = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,7 @@ spring:
|
|||||||
use_sql_comments: true
|
use_sql_comments: true
|
||||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
ddl-auto: ${JPA_DDL_AUTO:none}
|
||||||
|
|
||||||
# Redis Configuration
|
# Redis Configuration
|
||||||
data:
|
data:
|
||||||
|
|||||||
@ -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();
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
130
meeting/src/test/resources/test-data-minutes-sections.sql
Normal file
130
meeting/src/test/resources/test-data-minutes-sections.sql
Normal 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;
|
||||||
2055
stt/logs/stt.log
2055
stt/logs/stt.log
File diff suppressed because it is too large
Load Diff
BIN
stt/logs/stt.log.2025-10-27.0.gz
Normal file
BIN
stt/logs/stt.log.2025-10-27.0.gz
Normal file
Binary file not shown.
BIN
stt/logs/stt.log.2025-10-28.0.gz
Normal file
BIN
stt/logs/stt.log.2025-10-28.0.gz
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user