mirror of
https://github.com/hwanny1128/HGZero.git
synced 2026-06-12 22:59:10 +00:00
작업 중: Meeting AI 통합 개발 진행 상황 저장
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
# [DB] 회의종료 기능을 위한 스키마 추가
|
||||
|
||||
## 📋 요약
|
||||
회의 종료 시 참석자별 회의록을 AI가 통합하고 Todo를 자동 추출하기 위한 데이터베이스 스키마 추가
|
||||
|
||||
## 🎯 목적
|
||||
- 참석자별 회의록 저장 지원
|
||||
- AI 통합 회의록 생성 및 저장
|
||||
- 안건별 구조화된 회의록 관리
|
||||
- AI 요약 결과 캐싱 (성능 최적화)
|
||||
- Todo 자동 추출 정보 관리
|
||||
|
||||
## 📊 변경 내용
|
||||
|
||||
### 1. minutes 테이블 확장
|
||||
```sql
|
||||
ALTER TABLE minutes ADD COLUMN user_id VARCHAR(100);
|
||||
```
|
||||
- **목적**: 참석자별 회의록과 AI 통합 회의록 구분
|
||||
- **구분 방법**:
|
||||
- `user_id IS NULL` → AI 통합 회의록
|
||||
- `user_id IS NOT NULL` → 참석자별 회의록
|
||||
- **설계 개선**: `is_consolidated` 컬럼 불필요 (중복 정보 제거)
|
||||
|
||||
### 2. agenda_sections 테이블 생성 (신규)
|
||||
```sql
|
||||
CREATE TABLE agenda_sections (
|
||||
id, minutes_id, meeting_id,
|
||||
agenda_number, agenda_title,
|
||||
ai_summary_short, discussions,
|
||||
decisions (JSON), pending_items (JSON), opinions (JSON)
|
||||
);
|
||||
```
|
||||
- **목적**: 안건별 AI 요약 결과 저장
|
||||
- **JSON 필드**:
|
||||
- `decisions`: 결정 사항 배열
|
||||
- `pending_items`: 보류 사항 배열
|
||||
- `opinions`: 참석자별 의견 [{speaker, opinion}]
|
||||
|
||||
### 3. ai_summaries 테이블 생성 (신규)
|
||||
```sql
|
||||
CREATE TABLE ai_summaries (
|
||||
id, meeting_id, summary_type,
|
||||
source_minutes_ids (JSON), result (JSON),
|
||||
processing_time_ms, model_version,
|
||||
keywords (JSON), statistics (JSON)
|
||||
);
|
||||
```
|
||||
- **목적**: AI 요약 결과 캐싱 및 성능 최적화
|
||||
- **summary_type**:
|
||||
- `CONSOLIDATED`: 통합 회의록 요약
|
||||
- `TODO_EXTRACTION`: Todo 자동 추출
|
||||
- **캐싱 효과**: 재조회 시 3-5초 → 0.1초
|
||||
|
||||
### 4. todos 테이블 확장
|
||||
```sql
|
||||
ALTER TABLE todos
|
||||
ADD COLUMN extracted_by VARCHAR(50) DEFAULT 'AI',
|
||||
ADD COLUMN section_reference VARCHAR(200),
|
||||
ADD COLUMN extraction_confidence DECIMAL(3,2);
|
||||
```
|
||||
- **extracted_by**: `AI` (자동 추출) / `MANUAL` (수동 작성)
|
||||
- **section_reference**: 관련 안건 참조 (예: "안건 1")
|
||||
- **extraction_confidence**: AI 추출 신뢰도 (0.00~1.00)
|
||||
|
||||
## 🔄 데이터 플로우
|
||||
|
||||
```
|
||||
1. 회의 진행 중
|
||||
└─ 각 참석자가 회의록 작성
|
||||
└─ minutes 테이블 저장 (user_id: user@example.com)
|
||||
|
||||
2. 회의 종료
|
||||
└─ AI Service 호출
|
||||
└─ 참석자별 회의록 조회 (user_id IS NOT NULL)
|
||||
└─ Claude AI 통합 요약 생성
|
||||
└─ minutes 테이블 저장 (user_id: NULL)
|
||||
└─ agenda_sections 테이블 저장 (안건별 섹션)
|
||||
└─ ai_summaries 테이블 저장 (캐시)
|
||||
└─ todos 테이블 저장 (extracted_by: AI)
|
||||
|
||||
3. 회의록 조회
|
||||
└─ ai_summaries 캐시 조회 (빠름!)
|
||||
└─ agenda_sections 조회
|
||||
└─ 화면 렌더링
|
||||
```
|
||||
|
||||
## 📁 관련 파일
|
||||
|
||||
### 마이그레이션
|
||||
- `meeting/src/main/resources/db/migration/V3__add_meeting_end_support.sql`
|
||||
|
||||
### 문서
|
||||
- `docs/DB-Schema-회의종료.md` - 상세 스키마 문서
|
||||
- `docs/ERD-회의종료.puml` - ERD 다이어그램
|
||||
- `docs/회의종료-개발계획.md` - 전체 개발 계획
|
||||
|
||||
## ✅ 체크리스트
|
||||
|
||||
### 마이그레이션
|
||||
- [x] V3 마이그레이션 스크립트 작성
|
||||
- [x] 인덱스 추가 (성능 최적화)
|
||||
- [x] 외래키 제약조건 설정
|
||||
- [x] 트리거 생성 (updated_at 자동 업데이트)
|
||||
- [x] 코멘트 추가 (문서화)
|
||||
|
||||
### 문서
|
||||
- [x] DB 스키마 상세 문서
|
||||
- [x] ERD 다이어그램
|
||||
- [x] JSON 필드 구조 예시
|
||||
- [x] 쿼리 예시 작성
|
||||
- [x] 개발 계획서
|
||||
|
||||
### 설계 검증
|
||||
- [x] 중복 컬럼 제거 (is_consolidated)
|
||||
- [x] NULL 활용 (user_id로 구분)
|
||||
- [x] JSON 필드 구조 정의
|
||||
- [x] 인덱스 전략 수립
|
||||
|
||||
## 🧪 테스트 계획
|
||||
|
||||
### 마이그레이션 테스트
|
||||
1. 로컬 환경에서 마이그레이션 실행
|
||||
2. 테이블 생성 확인
|
||||
3. 인덱스 생성 확인
|
||||
4. 외래키 제약조건 확인
|
||||
|
||||
### 성능 테스트
|
||||
1. 참석자별 회의록 조회 성능
|
||||
2. 안건별 섹션 조회 성능
|
||||
3. JSON 필드 쿼리 성능
|
||||
4. ai_summaries 캐시 조회 성능
|
||||
|
||||
## 🚀 다음 단계
|
||||
|
||||
### Meeting Service API 개발 (병렬 진행 가능)
|
||||
1. `GET /meetings/{meetingId}/minutes/by-participants` - 참석자별 회의록 조회
|
||||
2. `GET /meetings/{meetingId}/agenda-sections` - 안건별 섹션 조회
|
||||
3. `GET /meetings/{meetingId}/statistics` - 회의 통계 조회
|
||||
4. `POST /internal/ai-summaries` - AI 결과 저장 (내부 API)
|
||||
|
||||
### AI Service 개발 (병렬 진행 가능)
|
||||
1. Claude AI 프롬프트 설계
|
||||
2. `POST /transcripts/consolidate` - 통합 회의록 생성
|
||||
3. `POST /todos/extract` - Todo 자동 추출
|
||||
4. Meeting Service API 호출 통합
|
||||
|
||||
## 💬 리뷰 포인트
|
||||
|
||||
1. **DB 스키마 설계**
|
||||
- user_id만으로 참석자/통합 구분이 명확한가?
|
||||
- JSON 필드 구조가 적절한가?
|
||||
- 인덱스 전략이 최적인가?
|
||||
|
||||
2. **성능**
|
||||
- 인덱스가 충분한가?
|
||||
- JSON 필드 쿼리 성능이 괜찮은가?
|
||||
- 추가 인덱스가 필요한가?
|
||||
|
||||
3. **확장성**
|
||||
- 향후 필드 추가가 용이한가?
|
||||
- 다른 AI 모델 지원이 가능한가?
|
||||
|
||||
## 📌 참고 사항
|
||||
|
||||
- PostgreSQL 기준으로 작성됨
|
||||
- Flyway 자동 마이그레이션 지원
|
||||
- 샘플 데이터는 주석 처리 (운영 환경 고려)
|
||||
- 트리거 함수 포함 (updated_at 자동 업데이트)
|
||||
|
||||
## 🔗 관련 이슈
|
||||
<!-- 관련 이슈 번호가 있다면 링크 -->
|
||||
|
||||
---
|
||||
|
||||
**Merge 후 Meeting Service API 개발을 시작할 수 있습니다!**
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
INFO: Will watch for changes in these directories: ['/Users/jominseo/HGZero/ai-python']
|
||||
ERROR: [Errno 48] Address already in use
|
||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -36,7 +36,7 @@ app.add_middleware(
|
||||
)
|
||||
|
||||
# API 라우터 등록
|
||||
app.include_router(api_v1_router, prefix="/api/v1")
|
||||
app.include_router(api_v1_router, prefix="/api")
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
|
||||
@@ -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">
|
||||
function configurationCacheProblems() { return (
|
||||
// begin-report-data
|
||||
{"diagnostics":[{"locations":[{"path":"/Users/daewoong/home/workspace/HGZero/common/src/main/java/com/unicorn/hgzero/common/security/JwtTokenProvider.java"},{"taskPath":":common:compileJava"}],"problem":[{"text":"/Users/daewoong/home/workspace/HGZero/common/src/main/java/com/unicorn/hgzero/common/security/JwtTokenProvider.java uses or overrides a deprecated API."}],"severity":"ADVICE","problemDetails":[{"text":"Note: /Users/daewoong/home/workspace/HGZero/common/src/main/java/com/unicorn/hgzero/common/security/JwtTokenProvider.java uses or overrides a deprecated API."}],"contextualLabel":"/Users/daewoong/home/workspace/HGZero/common/src/main/java/com/unicorn/hgzero/common/security/JwtTokenProvider.java uses or overrides a deprecated API.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.filename","displayName":"/Users/daewoong/home/workspace/HGZero/common/src/main/java/com/unicorn/hgzero/common/security/JwtTokenProvider.java uses or overrides a deprecated API."}]},{"locations":[{"path":"/Users/daewoong/home/workspace/HGZero/common/src/main/java/com/unicorn/hgzero/common/security/JwtTokenProvider.java"},{"taskPath":":common:compileJava"}],"problem":[{"text":"Recompile with -Xlint:deprecation for details."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Recompile with -Xlint:deprecation for details."}],"contextualLabel":"Recompile with -Xlint:deprecation for details.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.recompile","displayName":"Recompile with -Xlint:deprecation for details."}]},{"locations":[{"path":"/Users/daewoong/home/workspace/HGZero/user/src/main/java/com/unicorn/hgzero/user/service/UserServiceImpl.java"},{"taskPath":":user:compileJava"}],"problem":[{"text":"/Users/daewoong/home/workspace/HGZero/user/src/main/java/com/unicorn/hgzero/user/service/UserServiceImpl.java uses or overrides a deprecated API."}],"severity":"ADVICE","problemDetails":[{"text":"Note: /Users/daewoong/home/workspace/HGZero/user/src/main/java/com/unicorn/hgzero/user/service/UserServiceImpl.java uses or overrides a deprecated API."}],"contextualLabel":"/Users/daewoong/home/workspace/HGZero/user/src/main/java/com/unicorn/hgzero/user/service/UserServiceImpl.java uses or overrides a deprecated API.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.filename","displayName":"/Users/daewoong/home/workspace/HGZero/user/src/main/java/com/unicorn/hgzero/user/service/UserServiceImpl.java uses or overrides a deprecated API."}]},{"locations":[{"path":"/Users/daewoong/home/workspace/HGZero/user/src/main/java/com/unicorn/hgzero/user/service/UserServiceImpl.java"},{"taskPath":":user:compileJava"}],"problem":[{"text":"Recompile with -Xlint:deprecation for details."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Recompile with -Xlint:deprecation for details."}],"contextualLabel":"Recompile with -Xlint:deprecation for details.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.recompile","displayName":"Recompile with -Xlint:deprecation for details."}]},{"locations":[{"path":"/Users/daewoong/home/workspace/HGZero/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/request/InviteParticipantRequest.java"},{"taskPath":":meeting:compileJava"}],"problem":[{"text":"Some input files use or override a deprecated API."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Some input files use or override a deprecated API."}],"contextualLabel":"Some input files use or override a deprecated API.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.plural","displayName":"Some input files use or override a deprecated API."}]},{"locations":[{"path":"/Users/daewoong/home/workspace/HGZero/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/request/InviteParticipantRequest.java"},{"taskPath":":meeting:compileJava"}],"problem":[{"text":"Recompile with -Xlint:deprecation for details."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Recompile with -Xlint:deprecation for details."}],"contextualLabel":"Recompile with -Xlint:deprecation for details.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.recompile","displayName":"Recompile with -Xlint:deprecation for details."}]}],"problemsReport":{"totalProblemCount":6,"buildName":"hgzero","requestedTasks":"clean bootJar","documentationLink":"https://docs.gradle.org/8.14/userguide/reporting_problems.html","documentationLinkCaption":"Problem report","summaries":[]}}
|
||||
{"diagnostics":[{"locations":[{"path":"/Users/jominseo/HGZero/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/request/InviteParticipantRequest.java"},{"taskPath":":meeting:compileJava"}],"problem":[{"text":"Some input files use or override a deprecated API."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Some input files use or override a deprecated API."}],"contextualLabel":"Some input files use or override a deprecated API.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.plural","displayName":"Some input files use or override a deprecated API."}]},{"locations":[{"path":"/Users/jominseo/HGZero/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/request/InviteParticipantRequest.java"},{"taskPath":":meeting:compileJava"}],"problem":[{"text":"Recompile with -Xlint:deprecation for details."}],"severity":"ADVICE","problemDetails":[{"text":"Note: Recompile with -Xlint:deprecation for details."}],"contextualLabel":"Recompile with -Xlint:deprecation for details.","problemId":[{"name":"java","displayName":"Java compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compiler.note.deprecated.recompile","displayName":"Recompile with -Xlint:deprecation for details."}]},{"locations":[{"taskPath":":meeting:test"}],"problem":[{"text":"The automatic loading of test framework implementation dependencies has been deprecated."}],"severity":"WARNING","problemDetails":[{"text":"This is scheduled to be removed in Gradle 9.0."}],"contextualLabel":"The automatic loading of test framework implementation dependencies has been deprecated.","documentationLink":"https://docs.gradle.org/8.14/userguide/upgrading_version_8.html#test_framework_implementation_dependencies","problemId":[{"name":"deprecation","displayName":"Deprecation"},{"name":"the-automatic-loading-of-test-framework-implementation-dependencies","displayName":"The automatic loading of test framework implementation dependencies has been deprecated."}],"solutions":[[{"text":"Declare the desired test framework directly on the test suite or explicitly declare the test framework implementation dependencies on the test's runtime classpath."}]]},{"locations":[{},{"taskPath":":meeting:test"}],"problem":[{"text":"No test executed. This behavior has been deprecated."}],"severity":"WARNING","problemDetails":[{"text":"This will fail with an error in Gradle 9.0."}],"contextualLabel":"No test executed. This behavior has been deprecated.","documentationLink":"https://docs.gradle.org/8.14/userguide/upgrading_version_8.html#test_task_fail_on_no_test_executed","problemId":[{"name":"deprecation","displayName":"Deprecation"},{"name":"no-test-executed-this-behavior-has-been-deprecated","displayName":"No test executed. This behavior has been deprecated."}],"solutions":[[{"text":"There are test sources present but no test was executed. Please check your test configuration."}]]}],"problemsReport":{"totalProblemCount":4,"buildName":"hgzero","requestedTasks":"build","documentationLink":"https://docs.gradle.org/8.14/userguide/reporting_problems.html","documentationLinkCaption":"Problem report","summaries":[]}}
|
||||
// end-report-data
|
||||
);}
|
||||
</script>
|
||||
|
||||
@@ -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
|
||||
**상태**: 완료 및 검증됨
|
||||
@@ -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
|
||||
@@ -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/건
|
||||
```
|
||||
@@ -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
|
||||
@@ -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 테이블
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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]
|
||||
@@ -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'
|
||||
@@ -0,0 +1,2 @@
|
||||
curl: option : blank argument where content is expected
|
||||
curl: try 'curl --help' or 'curl --manual' for more information
|
||||
@@ -0,0 +1,3 @@
|
||||
[INFO] Project root: /Users/jominseo/HGZero
|
||||
[INFO] Reading run configuration files...
|
||||
[ERROR] No execution configurations found
|
||||
@@ -0,0 +1,3 @@
|
||||
[INFO] Project root: /Users/jominseo/HGZero
|
||||
[INFO] Reading run configuration files...
|
||||
[ERROR] No execution configurations found
|
||||
@@ -0,0 +1,3 @@
|
||||
[INFO] Project root: /Users/jominseo/HGZero
|
||||
[INFO] Reading run configuration files...
|
||||
[ERROR] No execution configurations found
|
||||
@@ -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`
|
||||
@@ -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` 파일 참조
|
||||
@@ -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';
|
||||
Executable
+40
@@ -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 "========================================="
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
+1780
-16013
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+49
-20
@@ -4,18 +4,19 @@ import com.unicorn.hgzero.meeting.biz.domain.MeetingAnalysis;
|
||||
import com.unicorn.hgzero.meeting.biz.dto.MeetingEndDTO;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.EndMeetingUseCase;
|
||||
import com.unicorn.hgzero.meeting.infra.client.AIServiceClient;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.ai.AgendaSummaryDTO;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.ai.ConsolidateRequest;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.ai.ConsolidateResponse;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.ai.ExtractedTodoDTO;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.ai.ParticipantMinutesDTO;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.AgendaSectionEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingAnalysisEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesSectionEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.TodoEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.AgendaSectionJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingAnalysisJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MinutesJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MinutesSectionJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.TodoJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -25,6 +26,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -39,7 +41,8 @@ import java.util.stream.Collectors;
|
||||
public class EndMeetingService implements EndMeetingUseCase {
|
||||
|
||||
private final MeetingJpaRepository meetingRepository;
|
||||
private final AgendaSectionJpaRepository agendaRepository;
|
||||
private final MinutesJpaRepository minutesRepository;
|
||||
private final MinutesSectionJpaRepository minutesSectionRepository;
|
||||
private final TodoJpaRepository todoRepository;
|
||||
private final MeetingAnalysisJpaRepository analysisRepository;
|
||||
private final AIServiceClient aiServiceClient;
|
||||
@@ -59,13 +62,26 @@ public class EndMeetingService implements EndMeetingUseCase {
|
||||
MeetingEntity meeting = meetingRepository.findById(meetingId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("회의를 찾을 수 없습니다: " + meetingId));
|
||||
|
||||
// 2. 안건 목록 조회 (실제로는 참석자별 메모 섹션)
|
||||
List<AgendaSectionEntity> agendaSections = agendaRepository.findByMeetingIdOrderByAgendaNumberAsc(meetingId);
|
||||
// 2. 참석자별 회의록 조회 (userId가 있는 회의록들)
|
||||
List<MinutesEntity> participantMinutesList = minutesRepository.findByMeetingIdAndUserIdIsNotNull(meetingId);
|
||||
|
||||
// 3. AI 통합 분석 요청 데이터 생성
|
||||
ConsolidateRequest request = createConsolidateRequest(meeting, agendaSections);
|
||||
if (participantMinutesList.isEmpty()) {
|
||||
throw new IllegalStateException("참석자 회의록이 없습니다: " + meetingId);
|
||||
}
|
||||
|
||||
// 4. AI Service 호출
|
||||
// 3. 각 회의록의 sections 조회 및 통합
|
||||
List<MinutesSectionEntity> allMinutesSections = new ArrayList<>();
|
||||
for (MinutesEntity minutes : participantMinutesList) {
|
||||
List<MinutesSectionEntity> sections = minutesSectionRepository.findByMinutesIdOrderByOrderAsc(
|
||||
minutes.getMinutesId()
|
||||
);
|
||||
allMinutesSections.addAll(sections);
|
||||
}
|
||||
|
||||
// 4. AI 통합 분석 요청 데이터 생성
|
||||
ConsolidateRequest request = createConsolidateRequest(meeting, allMinutesSections, participantMinutesList);
|
||||
|
||||
// 5. AI Service 호출
|
||||
ConsolidateResponse aiResponse = aiServiceClient.consolidateMinutes(request);
|
||||
|
||||
// 5. AI 분석 결과 저장
|
||||
@@ -74,25 +90,38 @@ public class EndMeetingService implements EndMeetingUseCase {
|
||||
// 6. Todo 생성 및 저장
|
||||
List<TodoEntity> todos = createAndSaveTodos(meeting, aiResponse, analysis);
|
||||
|
||||
// 7. 회의 종료 처리
|
||||
// 6. 회의 종료 처리
|
||||
meeting.end();
|
||||
meetingRepository.save(meeting);
|
||||
|
||||
// 8. 응답 DTO 생성
|
||||
return createMeetingEndDTO(meeting, analysis, todos, agendaSections.size());
|
||||
// 7. 응답 DTO 생성
|
||||
return createMeetingEndDTO(meeting, analysis, todos, participantMinutesList.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 통합 분석 요청 데이터 생성
|
||||
* 참석자별 회의록의 섹션들을 참석자별로 그룹화하여 AI 요청 데이터 생성
|
||||
*/
|
||||
private ConsolidateRequest createConsolidateRequest(MeetingEntity meeting, List<AgendaSectionEntity> agendaSections) {
|
||||
// 참석자별 회의록 변환 (AgendaSection → ParticipantMinutes)
|
||||
List<ParticipantMinutesDTO> participantMinutes = agendaSections.stream()
|
||||
.<ParticipantMinutesDTO>map(section -> ParticipantMinutesDTO.builder()
|
||||
.userId(section.getMeetingId()) // 실제로는 participantId 필요
|
||||
.userName(section.getAgendaTitle()) // 실제로는 participantName 필요
|
||||
.content(section.getDiscussions() != null ? section.getDiscussions() : "")
|
||||
.build())
|
||||
private ConsolidateRequest createConsolidateRequest(
|
||||
MeetingEntity meeting,
|
||||
List<MinutesSectionEntity> allMinutesSections,
|
||||
List<MinutesEntity> participantMinutesList) {
|
||||
|
||||
// 참석자별 회의록을 ParticipantMinutesDTO로 변환
|
||||
List<ParticipantMinutesDTO> participantMinutes = participantMinutesList.stream()
|
||||
.<ParticipantMinutesDTO>map(minutes -> {
|
||||
// 해당 회의록의 섹션들만 필터링
|
||||
String content = allMinutesSections.stream()
|
||||
.filter(section -> section.getMinutesId().equals(minutes.getMinutesId()))
|
||||
.<String>map(section -> section.getTitle() + "\n" + section.getContent())
|
||||
.collect(Collectors.joining("\n\n"));
|
||||
|
||||
return ParticipantMinutesDTO.builder()
|
||||
.userId(minutes.getUserId())
|
||||
.userName(minutes.getUserId()) // 실제로는 userName이 필요하지만 일단 userId 사용
|
||||
.content(content)
|
||||
.build();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ConsolidateRequest.builder()
|
||||
|
||||
@@ -46,7 +46,7 @@ public class AIServiceClient {
|
||||
log.info("AI Service 호출 - 회의록 통합 요약: {}", request.getMeetingId());
|
||||
|
||||
try {
|
||||
String url = aiServiceUrl + "/api/v1/transcripts/consolidate";
|
||||
String url = aiServiceUrl + "/api/transcripts/consolidate";
|
||||
|
||||
// HTTP 헤더 설정
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
+1
-12
@@ -37,10 +37,6 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
@Column(name = "title", length = 200, nullable = false)
|
||||
private String title;
|
||||
|
||||
@OneToMany(mappedBy = "minutes", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@Builder.Default
|
||||
private List<MinutesSectionEntity> sections = new ArrayList<>();
|
||||
|
||||
@Column(name = "status", length = 20, nullable = false)
|
||||
@Builder.Default
|
||||
private String status = "DRAFT";
|
||||
@@ -64,9 +60,7 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
.meetingId(this.meetingId)
|
||||
.userId(this.userId)
|
||||
.title(this.title)
|
||||
.sections(this.sections.stream()
|
||||
.map(MinutesSectionEntity::toDomain)
|
||||
.collect(Collectors.toList()))
|
||||
.sections(List.of()) // sections는 별도 조회 필요
|
||||
.status(this.status)
|
||||
.version(this.version)
|
||||
.createdBy(this.createdBy)
|
||||
@@ -83,11 +77,6 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
.meetingId(minutes.getMeetingId())
|
||||
.userId(minutes.getUserId())
|
||||
.title(minutes.getTitle())
|
||||
.sections(minutes.getSections() != null
|
||||
? minutes.getSections().stream()
|
||||
.map(MinutesSectionEntity::fromDomain)
|
||||
.collect(Collectors.toList())
|
||||
: new ArrayList<>())
|
||||
.status(minutes.getStatus())
|
||||
.version(minutes.getVersion())
|
||||
.createdBy(minutes.getCreatedBy())
|
||||
|
||||
+19
-24
@@ -10,6 +10,8 @@ import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 Entity
|
||||
* 참석자가 작성한 메모를 안건별로 저장
|
||||
* AI 분석의 입력 데이터로 사용됨
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "minutes_sections")
|
||||
@@ -20,43 +22,36 @@ import lombok.NoArgsConstructor;
|
||||
public class MinutesSectionEntity extends BaseTimeEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "section_id", length = 50)
|
||||
private String sectionId;
|
||||
@Column(name = "id", length = 50)
|
||||
private String id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "minutes_id", nullable = false)
|
||||
private MinutesEntity minutes;
|
||||
|
||||
@Column(name = "minutes_id", insertable = false, updatable = false)
|
||||
@Column(name = "minutes_id", nullable = false, length = 50)
|
||||
private String minutesId;
|
||||
|
||||
@Column(name = "type", length = 50, nullable = false)
|
||||
@Column(name = "type", length = 50)
|
||||
private String type;
|
||||
|
||||
@Column(name = "title", length = 200, nullable = false)
|
||||
@Column(name = "title", length = 200)
|
||||
private String title;
|
||||
|
||||
@Column(name = "content", columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
@Column(name = "\"order\"")
|
||||
@Builder.Default
|
||||
private Integer order = 0;
|
||||
@Column(name = "order")
|
||||
private Integer order;
|
||||
|
||||
@Column(name = "verified", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean verified = false;
|
||||
@Column(name = "verified")
|
||||
private Boolean verified;
|
||||
|
||||
@Column(name = "locked", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean locked = false;
|
||||
@Column(name = "locked")
|
||||
private Boolean locked;
|
||||
|
||||
@Column(name = "locked_by", length = 50)
|
||||
private String lockedBy;
|
||||
|
||||
public MinutesSection toDomain() {
|
||||
return MinutesSection.builder()
|
||||
.sectionId(this.sectionId)
|
||||
.sectionId(this.id)
|
||||
.minutesId(this.minutesId)
|
||||
.type(this.type)
|
||||
.title(this.title)
|
||||
@@ -70,7 +65,7 @@ public class MinutesSectionEntity extends BaseTimeEntity {
|
||||
|
||||
public static MinutesSectionEntity fromDomain(MinutesSection section) {
|
||||
return MinutesSectionEntity.builder()
|
||||
.sectionId(section.getSectionId())
|
||||
.id(section.getSectionId())
|
||||
.minutesId(section.getMinutesId())
|
||||
.type(section.getType())
|
||||
.title(section.getTitle())
|
||||
@@ -82,6 +77,10 @@ public class MinutesSectionEntity extends BaseTimeEntity {
|
||||
.build();
|
||||
}
|
||||
|
||||
public void verify() {
|
||||
this.verified = true;
|
||||
}
|
||||
|
||||
public void lock(String userId) {
|
||||
this.locked = true;
|
||||
this.lockedBy = userId;
|
||||
@@ -91,8 +90,4 @@ public class MinutesSectionEntity extends BaseTimeEntity {
|
||||
this.locked = false;
|
||||
this.lockedBy = null;
|
||||
}
|
||||
|
||||
public void verify() {
|
||||
this.verified = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ spring:
|
||||
use_sql_comments: true
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
||||
ddl-auto: ${JPA_DDL_AUTO:none}
|
||||
|
||||
# Redis Configuration
|
||||
data:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
+200
-1855
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user