mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 06:46:24 +00:00
for merge
This commit is contained in:
commit
a84449e88d
176
.github/PULL_REQUEST_TEMPLATE_meeting_ai_db.md
vendored
Normal file
176
.github/PULL_REQUEST_TEMPLATE_meeting_ai_db.md
vendored
Normal file
@ -0,0 +1,176 @@
|
||||
# [DB] 회의종료 기능을 위한 스키마 추가
|
||||
|
||||
## 📋 요약
|
||||
회의 종료 시 참석자별 회의록을 AI가 통합하고 Todo를 자동 추출하기 위한 데이터베이스 스키마 추가
|
||||
|
||||
## 🎯 목적
|
||||
- 참석자별 회의록 저장 지원
|
||||
- AI 통합 회의록 생성 및 저장
|
||||
- 안건별 구조화된 회의록 관리
|
||||
- AI 요약 결과 캐싱 (성능 최적화)
|
||||
- Todo 자동 추출 정보 관리
|
||||
|
||||
## 📊 변경 내용
|
||||
|
||||
### 1. minutes 테이블 확장
|
||||
```sql
|
||||
ALTER TABLE minutes ADD COLUMN user_id VARCHAR(100);
|
||||
```
|
||||
- **목적**: 참석자별 회의록과 AI 통합 회의록 구분
|
||||
- **구분 방법**:
|
||||
- `user_id IS NULL` → AI 통합 회의록
|
||||
- `user_id IS NOT NULL` → 참석자별 회의록
|
||||
- **설계 개선**: `is_consolidated` 컬럼 불필요 (중복 정보 제거)
|
||||
|
||||
### 2. agenda_sections 테이블 생성 (신규)
|
||||
```sql
|
||||
CREATE TABLE agenda_sections (
|
||||
id, minutes_id, meeting_id,
|
||||
agenda_number, agenda_title,
|
||||
ai_summary_short, discussions,
|
||||
decisions (JSON), pending_items (JSON), opinions (JSON)
|
||||
);
|
||||
```
|
||||
- **목적**: 안건별 AI 요약 결과 저장
|
||||
- **JSON 필드**:
|
||||
- `decisions`: 결정 사항 배열
|
||||
- `pending_items`: 보류 사항 배열
|
||||
- `opinions`: 참석자별 의견 [{speaker, opinion}]
|
||||
|
||||
### 3. ai_summaries 테이블 생성 (신규)
|
||||
```sql
|
||||
CREATE TABLE ai_summaries (
|
||||
id, meeting_id, summary_type,
|
||||
source_minutes_ids (JSON), result (JSON),
|
||||
processing_time_ms, model_version,
|
||||
keywords (JSON), statistics (JSON)
|
||||
);
|
||||
```
|
||||
- **목적**: AI 요약 결과 캐싱 및 성능 최적화
|
||||
- **summary_type**:
|
||||
- `CONSOLIDATED`: 통합 회의록 요약
|
||||
- `TODO_EXTRACTION`: Todo 자동 추출
|
||||
- **캐싱 효과**: 재조회 시 3-5초 → 0.1초
|
||||
|
||||
### 4. todos 테이블 확장
|
||||
```sql
|
||||
ALTER TABLE todos
|
||||
ADD COLUMN extracted_by VARCHAR(50) DEFAULT 'AI',
|
||||
ADD COLUMN section_reference VARCHAR(200),
|
||||
ADD COLUMN extraction_confidence DECIMAL(3,2);
|
||||
```
|
||||
- **extracted_by**: `AI` (자동 추출) / `MANUAL` (수동 작성)
|
||||
- **section_reference**: 관련 안건 참조 (예: "안건 1")
|
||||
- **extraction_confidence**: AI 추출 신뢰도 (0.00~1.00)
|
||||
|
||||
## 🔄 데이터 플로우
|
||||
|
||||
```
|
||||
1. 회의 진행 중
|
||||
└─ 각 참석자가 회의록 작성
|
||||
└─ minutes 테이블 저장 (user_id: user@example.com)
|
||||
|
||||
2. 회의 종료
|
||||
└─ AI Service 호출
|
||||
└─ 참석자별 회의록 조회 (user_id IS NOT NULL)
|
||||
└─ Claude AI 통합 요약 생성
|
||||
└─ minutes 테이블 저장 (user_id: NULL)
|
||||
└─ agenda_sections 테이블 저장 (안건별 섹션)
|
||||
└─ ai_summaries 테이블 저장 (캐시)
|
||||
└─ todos 테이블 저장 (extracted_by: AI)
|
||||
|
||||
3. 회의록 조회
|
||||
└─ ai_summaries 캐시 조회 (빠름!)
|
||||
└─ agenda_sections 조회
|
||||
└─ 화면 렌더링
|
||||
```
|
||||
|
||||
## 📁 관련 파일
|
||||
|
||||
### 마이그레이션
|
||||
- `meeting/src/main/resources/db/migration/V3__add_meeting_end_support.sql`
|
||||
|
||||
### 문서
|
||||
- `docs/DB-Schema-회의종료.md` - 상세 스키마 문서
|
||||
- `docs/ERD-회의종료.puml` - ERD 다이어그램
|
||||
- `docs/회의종료-개발계획.md` - 전체 개발 계획
|
||||
|
||||
## ✅ 체크리스트
|
||||
|
||||
### 마이그레이션
|
||||
- [x] V3 마이그레이션 스크립트 작성
|
||||
- [x] 인덱스 추가 (성능 최적화)
|
||||
- [x] 외래키 제약조건 설정
|
||||
- [x] 트리거 생성 (updated_at 자동 업데이트)
|
||||
- [x] 코멘트 추가 (문서화)
|
||||
|
||||
### 문서
|
||||
- [x] DB 스키마 상세 문서
|
||||
- [x] ERD 다이어그램
|
||||
- [x] JSON 필드 구조 예시
|
||||
- [x] 쿼리 예시 작성
|
||||
- [x] 개발 계획서
|
||||
|
||||
### 설계 검증
|
||||
- [x] 중복 컬럼 제거 (is_consolidated)
|
||||
- [x] NULL 활용 (user_id로 구분)
|
||||
- [x] JSON 필드 구조 정의
|
||||
- [x] 인덱스 전략 수립
|
||||
|
||||
## 🧪 테스트 계획
|
||||
|
||||
### 마이그레이션 테스트
|
||||
1. 로컬 환경에서 마이그레이션 실행
|
||||
2. 테이블 생성 확인
|
||||
3. 인덱스 생성 확인
|
||||
4. 외래키 제약조건 확인
|
||||
|
||||
### 성능 테스트
|
||||
1. 참석자별 회의록 조회 성능
|
||||
2. 안건별 섹션 조회 성능
|
||||
3. JSON 필드 쿼리 성능
|
||||
4. ai_summaries 캐시 조회 성능
|
||||
|
||||
## 🚀 다음 단계
|
||||
|
||||
### Meeting Service API 개발 (병렬 진행 가능)
|
||||
1. `GET /meetings/{meetingId}/minutes/by-participants` - 참석자별 회의록 조회
|
||||
2. `GET /meetings/{meetingId}/agenda-sections` - 안건별 섹션 조회
|
||||
3. `GET /meetings/{meetingId}/statistics` - 회의 통계 조회
|
||||
4. `POST /internal/ai-summaries` - AI 결과 저장 (내부 API)
|
||||
|
||||
### AI Service 개발 (병렬 진행 가능)
|
||||
1. Claude AI 프롬프트 설계
|
||||
2. `POST /transcripts/consolidate` - 통합 회의록 생성
|
||||
3. `POST /todos/extract` - Todo 자동 추출
|
||||
4. Meeting Service API 호출 통합
|
||||
|
||||
## 💬 리뷰 포인트
|
||||
|
||||
1. **DB 스키마 설계**
|
||||
- user_id만으로 참석자/통합 구분이 명확한가?
|
||||
- JSON 필드 구조가 적절한가?
|
||||
- 인덱스 전략이 최적인가?
|
||||
|
||||
2. **성능**
|
||||
- 인덱스가 충분한가?
|
||||
- JSON 필드 쿼리 성능이 괜찮은가?
|
||||
- 추가 인덱스가 필요한가?
|
||||
|
||||
3. **확장성**
|
||||
- 향후 필드 추가가 용이한가?
|
||||
- 다른 AI 모델 지원이 가능한가?
|
||||
|
||||
## 📌 참고 사항
|
||||
|
||||
- PostgreSQL 기준으로 작성됨
|
||||
- Flyway 자동 마이그레이션 지원
|
||||
- 샘플 데이터는 주석 처리 (운영 환경 고려)
|
||||
- 트리거 함수 포함 (updated_at 자동 업데이트)
|
||||
|
||||
## 🔗 관련 이슈
|
||||
<!-- 관련 이슈 번호가 있다면 링크 -->
|
||||
|
||||
---
|
||||
|
||||
**Merge 후 Meeting Service API 개발을 시작할 수 있습니다!**
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -51,3 +51,9 @@ design/*/*back*
|
||||
design/*back*
|
||||
backup/
|
||||
claudedocs/*back*
|
||||
|
||||
# Log files
|
||||
logs/
|
||||
**/logs/
|
||||
*.log
|
||||
**/*.log
|
||||
|
||||
BIN
ai-python/__pycache__/main.cpython-313.pyc
Normal file
BIN
ai-python/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ai-python/app/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
ai-python/app/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ai-python/app/__pycache__/config.cpython-313.pyc
Normal file
BIN
ai-python/app/__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ai-python/app/api/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
ai-python/app/api/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
8
ai-python/app/api/v1/__init__.py
Normal file
8
ai-python/app/api/v1/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
"""API v1 Router"""
|
||||
from fastapi import APIRouter
|
||||
from .transcripts import router as transcripts_router
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 라우터 등록
|
||||
router.include_router(transcripts_router, prefix="/transcripts", tags=["Transcripts"])
|
||||
BIN
ai-python/app/api/v1/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
ai-python/app/api/v1/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ai-python/app/api/v1/__pycache__/suggestions.cpython-313.pyc
Normal file
BIN
ai-python/app/api/v1/__pycache__/suggestions.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ai-python/app/api/v1/__pycache__/transcripts.cpython-313.pyc
Normal file
BIN
ai-python/app/api/v1/__pycache__/transcripts.cpython-313.pyc
Normal file
Binary file not shown.
53
ai-python/app/api/v1/transcripts.py
Normal file
53
ai-python/app/api/v1/transcripts.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""Transcripts API Router"""
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
import logging
|
||||
from app.models.transcript import ConsolidateRequest, ConsolidateResponse
|
||||
from app.services.transcript_service import transcript_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/consolidate", response_model=ConsolidateResponse, status_code=status.HTTP_200_OK)
|
||||
async def consolidate_minutes(request: ConsolidateRequest):
|
||||
"""
|
||||
회의록 통합 요약
|
||||
|
||||
참석자별로 작성한 회의록을 AI가 통합하여 요약합니다.
|
||||
|
||||
- **meeting_id**: 회의 ID
|
||||
- **participant_minutes**: 참석자별 회의록 목록
|
||||
- **agendas**: 안건 목록 (선택)
|
||||
- **duration_minutes**: 회의 시간(분) (선택)
|
||||
|
||||
Returns:
|
||||
- 통합 요약, 키워드, 안건별 분석, Todo 자동 추출
|
||||
"""
|
||||
try:
|
||||
logger.info(f"POST /transcripts/consolidate - Meeting ID: {request.meeting_id}")
|
||||
|
||||
# 입력 검증
|
||||
if not request.participant_minutes:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="참석자 회의록이 비어있습니다"
|
||||
)
|
||||
|
||||
# 회의록 통합 처리
|
||||
response = await transcript_service.consolidate_minutes(request)
|
||||
|
||||
return response
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"입력 값 오류: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"회의록 통합 실패: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"회의록 통합 처리 중 오류가 발생했습니다: {str(e)}"
|
||||
)
|
||||
57
ai-python/app/config.py
Normal file
57
ai-python/app/config.py
Normal file
@ -0,0 +1,57 @@
|
||||
"""환경 설정"""
|
||||
from pydantic_settings import BaseSettings
|
||||
from functools import lru_cache
|
||||
from typing import List
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""환경 설정 클래스"""
|
||||
|
||||
# 서버 설정
|
||||
app_name: str = "AI Service (Python)"
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 8087 # feature/stt-ai 브랜치 AI Service(8086)와 충돌 방지
|
||||
|
||||
# Claude API
|
||||
claude_api_key: str = "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"
|
||||
claude_model: str = "claude-3-5-sonnet-20240620"
|
||||
claude_max_tokens: int = 250000
|
||||
claude_temperature: float = 0.7
|
||||
|
||||
# Redis
|
||||
redis_host: str = "20.249.177.114"
|
||||
redis_port: int = 6379
|
||||
redis_password: str = ""
|
||||
redis_db: int = 4
|
||||
|
||||
# Azure Event Hub
|
||||
eventhub_connection_string: str = "Endpoint=sb://hgzero-eventhub-ns.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=VUqZ9vFgu35E3c6RiUzoOGVUP8IZpFvlV+AEhC6sUpo="
|
||||
eventhub_name: str = "hgzero-eventhub-name"
|
||||
eventhub_consumer_group: str = "ai-transcript-group"
|
||||
|
||||
# CORS
|
||||
cors_origins: List[str] = [
|
||||
"http://localhost:8888",
|
||||
"http://localhost:8080",
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:8888",
|
||||
"http://127.0.0.1:8080",
|
||||
"http://127.0.0.1:3000"
|
||||
]
|
||||
|
||||
# 로깅
|
||||
log_level: str = "INFO"
|
||||
|
||||
# 분석 임계값
|
||||
min_segments_for_analysis: int = 10
|
||||
text_retention_seconds: int = 300 # 5분
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = False
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_settings() -> Settings:
|
||||
"""싱글톤 설정 인스턴스"""
|
||||
return Settings()
|
||||
16
ai-python/app/models/__init__.py
Normal file
16
ai-python/app/models/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""Data Models"""
|
||||
from .transcript import (
|
||||
ConsolidateRequest,
|
||||
ConsolidateResponse,
|
||||
AgendaSummary,
|
||||
ParticipantMinutes,
|
||||
ExtractedTodo
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ConsolidateRequest",
|
||||
"ConsolidateResponse",
|
||||
"AgendaSummary",
|
||||
"ParticipantMinutes",
|
||||
"ExtractedTodo",
|
||||
]
|
||||
BIN
ai-python/app/models/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
ai-python/app/models/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ai-python/app/models/__pycache__/response.cpython-313.pyc
Normal file
BIN
ai-python/app/models/__pycache__/response.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ai-python/app/models/__pycache__/transcript.cpython-313.pyc
Normal file
BIN
ai-python/app/models/__pycache__/transcript.cpython-313.pyc
Normal file
Binary file not shown.
69
ai-python/app/models/keyword.py
Normal file
69
ai-python/app/models/keyword.py
Normal file
@ -0,0 +1,69 @@
|
||||
"""Keyword Models"""
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class KeywordExtractRequest(BaseModel):
|
||||
"""주요 키워드 추출 요청"""
|
||||
meeting_id: str = Field(..., description="회의 ID")
|
||||
transcript_text: str = Field(..., description="전체 회의록 텍스트")
|
||||
max_keywords: int = Field(default=10, ge=1, le=20, description="최대 키워드 개수")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"meeting_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"transcript_text": "안건 1: 신제품 기획...\n타겟 고객을 20-30대로 설정...",
|
||||
"max_keywords": 10
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ExtractedKeyword(BaseModel):
|
||||
"""추출된 키워드"""
|
||||
keyword: str = Field(..., description="키워드")
|
||||
relevance_score: float = Field(..., ge=0.0, le=1.0, description="관련성 점수")
|
||||
frequency: int = Field(..., description="출현 빈도")
|
||||
category: str = Field(..., description="카테고리 (예: 기술, 전략, 일정 등)")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"keyword": "신제품기획",
|
||||
"relevance_score": 0.95,
|
||||
"frequency": 15,
|
||||
"category": "전략"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class KeywordExtractResponse(BaseModel):
|
||||
"""주요 키워드 추출 응답"""
|
||||
meeting_id: str = Field(..., description="회의 ID")
|
||||
keywords: List[ExtractedKeyword] = Field(..., description="추출된 키워드 목록")
|
||||
total_count: int = Field(..., description="전체 키워드 개수")
|
||||
extracted_at: datetime = Field(default_factory=datetime.utcnow, description="추출 시각")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"meeting_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"keywords": [
|
||||
{
|
||||
"keyword": "신제품기획",
|
||||
"relevance_score": 0.95,
|
||||
"frequency": 15,
|
||||
"category": "전략"
|
||||
},
|
||||
{
|
||||
"keyword": "예산편성",
|
||||
"relevance_score": 0.88,
|
||||
"frequency": 12,
|
||||
"category": "재무"
|
||||
}
|
||||
],
|
||||
"total_count": 10,
|
||||
"extracted_at": "2025-01-23T10:30:00Z"
|
||||
}
|
||||
}
|
||||
80
ai-python/app/models/todo.py
Normal file
80
ai-python/app/models/todo.py
Normal file
@ -0,0 +1,80 @@
|
||||
"""Todo Models"""
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, date
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class PriorityLevel(str, Enum):
|
||||
"""우선순위"""
|
||||
HIGH = "HIGH"
|
||||
MEDIUM = "MEDIUM"
|
||||
LOW = "LOW"
|
||||
|
||||
|
||||
class TodoExtractRequest(BaseModel):
|
||||
"""Todo 자동 추출 요청"""
|
||||
meeting_id: str = Field(..., description="회의 ID")
|
||||
transcript_text: str = Field(..., description="전체 회의록 텍스트")
|
||||
participants: List[str] = Field(..., description="참석자 목록")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"meeting_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"transcript_text": "안건 1: 신제품 기획...\n결정사항: API 설계서는 박민수님이 1월 30일까지 작성...",
|
||||
"participants": ["김민준", "박서연", "이준호", "박민수"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ExtractedTodo(BaseModel):
|
||||
"""추출된 Todo"""
|
||||
title: str = Field(..., description="Todo 제목")
|
||||
description: Optional[str] = Field(None, description="상세 설명")
|
||||
assignee: str = Field(..., description="담당자 이름")
|
||||
due_date: Optional[date] = Field(None, description="마감일")
|
||||
priority: PriorityLevel = Field(default=PriorityLevel.MEDIUM, description="우선순위")
|
||||
section_reference: str = Field(..., description="섹션 참조 (예: '결정사항 #1')")
|
||||
confidence_score: float = Field(..., ge=0.0, le=1.0, description="신뢰도 점수")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"title": "API 설계서 작성",
|
||||
"description": "신규 프로젝트 API 설계서 작성 완료",
|
||||
"assignee": "박민수",
|
||||
"due_date": "2025-01-30",
|
||||
"priority": "HIGH",
|
||||
"section_reference": "결정사항 #1",
|
||||
"confidence_score": 0.92
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TodoExtractResponse(BaseModel):
|
||||
"""Todo 자동 추출 응답"""
|
||||
meeting_id: str = Field(..., description="회의 ID")
|
||||
todos: List[ExtractedTodo] = Field(..., description="추출된 Todo 목록")
|
||||
total_count: int = Field(..., description="전체 Todo 개수")
|
||||
extracted_at: datetime = Field(default_factory=datetime.utcnow, description="추출 시각")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"meeting_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"todos": [
|
||||
{
|
||||
"title": "API 설계서 작성",
|
||||
"description": "신규 프로젝트 API 설계서 작성 완료",
|
||||
"assignee": "박민수",
|
||||
"due_date": "2025-01-30",
|
||||
"priority": "HIGH",
|
||||
"section_reference": "결정사항 #1",
|
||||
"confidence_score": 0.92
|
||||
}
|
||||
],
|
||||
"total_count": 5,
|
||||
"extracted_at": "2025-01-23T10:30:00Z"
|
||||
}
|
||||
}
|
||||
44
ai-python/app/models/transcript.py
Normal file
44
ai-python/app/models/transcript.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""Transcript Models"""
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Dict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ParticipantMinutes(BaseModel):
|
||||
"""참석자별 회의록"""
|
||||
user_id: str = Field(..., description="사용자 ID")
|
||||
user_name: str = Field(..., description="사용자 이름")
|
||||
content: str = Field(..., description="회의록 전체 내용 (MEMO 섹션)")
|
||||
|
||||
|
||||
class ConsolidateRequest(BaseModel):
|
||||
"""회의록 통합 요약 요청"""
|
||||
meeting_id: str = Field(..., description="회의 ID")
|
||||
participant_minutes: List[ParticipantMinutes] = Field(..., description="참석자별 회의록 목록")
|
||||
agendas: Optional[List[str]] = Field(None, description="안건 목록")
|
||||
duration_minutes: Optional[int] = Field(None, description="회의 시간(분)")
|
||||
|
||||
|
||||
class ExtractedTodo(BaseModel):
|
||||
"""추출된 Todo (제목만)"""
|
||||
title: str = Field(..., description="Todo 제목")
|
||||
|
||||
|
||||
class AgendaSummary(BaseModel):
|
||||
"""안건별 요약"""
|
||||
agenda_number: int = Field(..., description="안건 번호")
|
||||
agenda_title: str = Field(..., description="안건 제목")
|
||||
summary_short: str = Field(..., description="AI 생성 짧은 요약 (1줄, 20자 이내)")
|
||||
summary: str = Field(..., description="안건별 회의록 요약 (논의사항+결정사항, 사용자 수정 가능)")
|
||||
pending: List[str] = Field(default_factory=list, description="보류 사항")
|
||||
todos: List[ExtractedTodo] = Field(default_factory=list, description="Todo 목록 (제목만)")
|
||||
|
||||
|
||||
class ConsolidateResponse(BaseModel):
|
||||
"""회의록 통합 요약 응답"""
|
||||
meeting_id: str = Field(..., description="회의 ID")
|
||||
keywords: List[str] = Field(..., description="주요 키워드")
|
||||
statistics: Dict[str, int] = Field(..., description="통계 정보")
|
||||
decisions: str = Field(..., description="회의 전체 결정사항 (TEXT 형식)")
|
||||
agenda_summaries: List[AgendaSummary] = Field(..., description="안건별 요약")
|
||||
generated_at: datetime = Field(default_factory=datetime.utcnow, description="생성 시각")
|
||||
Binary file not shown.
111
ai-python/app/prompts/consolidate_prompt.py
Normal file
111
ai-python/app/prompts/consolidate_prompt.py
Normal file
@ -0,0 +1,111 @@
|
||||
"""회의록 통합 요약 프롬프트"""
|
||||
|
||||
|
||||
def get_consolidate_prompt(participant_minutes: list, agendas: list = None) -> str:
|
||||
"""
|
||||
참석자별 회의록을 통합하여 요약하는 프롬프트 생성
|
||||
"""
|
||||
|
||||
# 참석자 회의록 결합
|
||||
participants_content = "\n\n".join([
|
||||
f"## {p['user_name']}님의 회의록:\n{p['content']}"
|
||||
for p in participant_minutes
|
||||
])
|
||||
|
||||
# 안건 정보 (있는 경우)
|
||||
agendas_info = ""
|
||||
if agendas:
|
||||
agendas_info = "\n\n**사전 정의된 안건**:\n" + "\n".join([
|
||||
f"{i+1}. {agenda}" for i, agenda in enumerate(agendas)
|
||||
])
|
||||
|
||||
prompt = f"""당신은 회의록 작성 전문가입니다. 여러 참석자가 작성한 회의록을 통합하여 정확하고 체계적인 회의록을 생성해주세요.
|
||||
|
||||
# 입력 데이터
|
||||
|
||||
{participants_content}{agendas_info}
|
||||
|
||||
---
|
||||
|
||||
# 작업 지침
|
||||
|
||||
1. **주요 키워드 (keywords)**:
|
||||
- 회의에서 자주 언급된 핵심 키워드 5-10개 추출
|
||||
- 단어 또는 짧은 구문 (예: "신제품기획", "예산편성")
|
||||
|
||||
2. **통계 정보 (statistics)**:
|
||||
- agendas_count: 안건 개수 (내용 기반 추정)
|
||||
- todos_count: 추출된 Todo 총 개수
|
||||
|
||||
3. **회의 전체 결정사항 (decisions)**:
|
||||
- 회의 전체에서 최종 결정된 사항들을 TEXT 형식으로 정리
|
||||
- 안건별 결정사항을 모두 포함하여 회의록 수정 페이지에서 사용자가 확인 및 수정할 수 있도록 작성
|
||||
- 형식: "**안건1 결정사항:**\n- 결정1\n- 결정2\n\n**안건2 결정사항:**\n- 결정3"
|
||||
|
||||
4. **안건별 요약 (agenda_summaries)**:
|
||||
회의 내용을 분석하여 안건별로 구조화:
|
||||
|
||||
각 안건마다:
|
||||
- **agenda_number**: 안건 번호 (1, 2, 3...)
|
||||
- **agenda_title**: 안건 제목 (간결하게)
|
||||
- **summary_short**: AI가 생성한 1줄 요약 (20자 이내, 사용자 수정 불가)
|
||||
- **summary**: 안건별 회의록 요약 (논의사항과 결정사항을 포함한 전체 요약)
|
||||
* 회의록 수정 페이지에서 사용자가 수정할 수 있는 입력 필드
|
||||
* 형식: "**논의 사항:**\n- 논의내용1\n- 논의내용2\n\n**결정 사항:**\n- 결정1\n- 결정2"
|
||||
* 사용자가 자유롭게 편집할 수 있도록 구조화된 텍스트로 작성
|
||||
- **pending**: 보류 사항 배열 (추가 논의 필요 사항)
|
||||
- **todos**: Todo 배열 (제목만, 담당자/마감일/우선순위 없음)
|
||||
- title: Todo 제목만 추출 (예: "시장 조사 보고서 작성")
|
||||
|
||||
---
|
||||
|
||||
# 출력 형식
|
||||
|
||||
반드시 아래 JSON 형식으로만 응답하세요. 다른 텍스트는 포함하지 마세요.
|
||||
|
||||
```json
|
||||
{{
|
||||
"keywords": ["키워드1", "키워드2", "키워드3"],
|
||||
"statistics": {{
|
||||
"agendas_count": 숫자,
|
||||
"todos_count": 숫자
|
||||
}},
|
||||
"decisions": "**안건1 결정사항:**\\n- 결정1\\n- 결정2\\n\\n**안건2 결정사항:**\\n- 결정3",
|
||||
"agenda_summaries": [
|
||||
{{
|
||||
"agenda_number": 1,
|
||||
"agenda_title": "안건 제목",
|
||||
"summary_short": "짧은 요약 (20자 이내)",
|
||||
"summary": "**논의 사항:**\\n- 논의내용1\\n- 논의내용2\\n\\n**결정 사항:**\\n- 결정1\\n- 결정2",
|
||||
"pending": ["보류사항"],
|
||||
"todos": [
|
||||
{{
|
||||
"title": "Todo 제목"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 중요 규칙
|
||||
|
||||
1. **정확성**: 참석자 회의록에 명시된 내용만 사용
|
||||
2. **객관성**: 추측이나 가정 없이 사실만 기록
|
||||
3. **완전성**: 모든 필드를 빠짐없이 작성
|
||||
4. **구조화**: 안건별로 명확히 분리
|
||||
5. **결정사항 추출**:
|
||||
- 회의 전체 결정사항(decisions)은 모든 안건의 결정사항을 포함
|
||||
- 안건별 summary에도 결정사항을 포함하여 사용자가 수정 가능하도록 작성
|
||||
6. **summary 작성**:
|
||||
- summary_short: AI가 자동 생성한 1줄 요약 (사용자 수정 불가)
|
||||
- summary: 논의사항과 결정사항을 포함한 전체 요약 (사용자 수정 가능)
|
||||
7. **Todo 추출**: 제목만 추출 (담당자나 마감일 없어도 됨)
|
||||
8. **JSON만 출력**: 추가 설명 없이 JSON만 반환
|
||||
|
||||
이제 위 회의록들을 분석하여 통합 요약을 JSON 형식으로 생성해주세요.
|
||||
"""
|
||||
|
||||
return prompt
|
||||
BIN
ai-python/app/services/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
ai-python/app/services/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
ai-python/app/services/__pycache__/redis_service.cpython-313.pyc
Normal file
BIN
ai-python/app/services/__pycache__/redis_service.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
90
ai-python/app/services/claude_service.py
Normal file
90
ai-python/app/services/claude_service.py
Normal file
@ -0,0 +1,90 @@
|
||||
"""Claude API Service"""
|
||||
import anthropic
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
from app.config import get_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
class ClaudeService:
|
||||
"""Claude API 호출 서비스"""
|
||||
|
||||
def __init__(self):
|
||||
self.client = anthropic.Anthropic(api_key=settings.claude_api_key)
|
||||
self.model = settings.claude_model
|
||||
self.max_tokens = settings.claude_max_tokens
|
||||
self.temperature = settings.claude_temperature
|
||||
|
||||
async def generate_completion(
|
||||
self,
|
||||
prompt: str,
|
||||
system_prompt: str = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Claude API 호출하여 응답 생성
|
||||
|
||||
Args:
|
||||
prompt: 사용자 프롬프트
|
||||
system_prompt: 시스템 프롬프트 (선택)
|
||||
|
||||
Returns:
|
||||
Claude API 응답 (JSON 파싱)
|
||||
"""
|
||||
try:
|
||||
# 메시지 구성
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt
|
||||
}
|
||||
]
|
||||
|
||||
# API 호출
|
||||
logger.info(f"Claude API 호출 시작 - Model: {self.model}")
|
||||
|
||||
if system_prompt:
|
||||
response = self.client.messages.create(
|
||||
model=self.model,
|
||||
max_tokens=self.max_tokens,
|
||||
temperature=self.temperature,
|
||||
system=system_prompt,
|
||||
messages=messages
|
||||
)
|
||||
else:
|
||||
response = self.client.messages.create(
|
||||
model=self.model,
|
||||
max_tokens=self.max_tokens,
|
||||
temperature=self.temperature,
|
||||
messages=messages
|
||||
)
|
||||
|
||||
# 응답 텍스트 추출
|
||||
response_text = response.content[0].text
|
||||
logger.info(f"Claude API 응답 수신 완료 - Tokens: {response.usage.input_tokens + response.usage.output_tokens}")
|
||||
|
||||
# JSON 파싱
|
||||
# ```json ... ``` 블록 제거
|
||||
if "```json" in response_text:
|
||||
response_text = response_text.split("```json")[1].split("```")[0].strip()
|
||||
elif "```" in response_text:
|
||||
response_text = response_text.split("```")[1].split("```")[0].strip()
|
||||
|
||||
result = json.loads(response_text)
|
||||
|
||||
return result
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON 파싱 실패: {e}")
|
||||
logger.error(f"응답 텍스트: {response_text[:500]}...")
|
||||
raise ValueError(f"Claude API 응답을 JSON으로 파싱할 수 없습니다: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Claude API 호출 실패: {e}")
|
||||
raise
|
||||
|
||||
|
||||
# 싱글톤 인스턴스
|
||||
claude_service = ClaudeService()
|
||||
122
ai-python/app/services/transcript_service.py
Normal file
122
ai-python/app/services/transcript_service.py
Normal file
@ -0,0 +1,122 @@
|
||||
"""Transcript Service - 회의록 통합 처리"""
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from app.models.transcript import (
|
||||
ConsolidateRequest,
|
||||
ConsolidateResponse,
|
||||
AgendaSummary,
|
||||
ExtractedTodo
|
||||
)
|
||||
from app.services.claude_service import claude_service
|
||||
from app.prompts.consolidate_prompt import get_consolidate_prompt
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TranscriptService:
|
||||
"""회의록 통합 서비스"""
|
||||
|
||||
async def consolidate_minutes(
|
||||
self,
|
||||
request: ConsolidateRequest
|
||||
) -> ConsolidateResponse:
|
||||
"""
|
||||
참석자별 회의록을 통합하여 AI 요약 생성
|
||||
"""
|
||||
logger.info(f"회의록 통합 시작 - Meeting ID: {request.meeting_id}")
|
||||
logger.info(f"참석자 수: {len(request.participant_minutes)}")
|
||||
|
||||
try:
|
||||
# 1. 프롬프트 생성
|
||||
participant_data = [
|
||||
{
|
||||
"user_name": pm.user_name,
|
||||
"content": pm.content
|
||||
}
|
||||
for pm in request.participant_minutes
|
||||
]
|
||||
|
||||
prompt = get_consolidate_prompt(
|
||||
participant_minutes=participant_data,
|
||||
agendas=request.agendas
|
||||
)
|
||||
|
||||
# 입력 데이터 로깅
|
||||
logger.info("=" * 80)
|
||||
logger.info("INPUT - 참석자별 회의록:")
|
||||
for pm in participant_data:
|
||||
logger.info(f"\n[{pm['user_name']}]")
|
||||
logger.info(f"{pm['content'][:500]}..." if len(pm['content']) > 500 else pm['content'])
|
||||
logger.info("=" * 80)
|
||||
|
||||
# 2. Claude API 호출
|
||||
start_time = datetime.utcnow()
|
||||
ai_result = await claude_service.generate_completion(prompt)
|
||||
processing_time = (datetime.utcnow() - start_time).total_seconds() * 1000
|
||||
|
||||
logger.info(f"AI 처리 완료 - {processing_time:.0f}ms")
|
||||
|
||||
# 3. 응답 구성
|
||||
response = self._build_response(
|
||||
meeting_id=request.meeting_id,
|
||||
ai_result=ai_result,
|
||||
participants_count=len(request.participant_minutes),
|
||||
duration_minutes=request.duration_minutes
|
||||
)
|
||||
|
||||
logger.info(f"통합 요약 완료 - 안건 수: {len(response.agenda_summaries)}, Todo 수: {response.statistics['todos_count']}")
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"회의록 통합 실패: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _build_response(
|
||||
self,
|
||||
meeting_id: str,
|
||||
ai_result: dict,
|
||||
participants_count: int,
|
||||
duration_minutes: int = None
|
||||
) -> ConsolidateResponse:
|
||||
"""AI 응답을 ConsolidateResponse로 변환"""
|
||||
|
||||
# 안건별 요약 변환
|
||||
agenda_summaries = []
|
||||
for agenda_data in ai_result.get("agenda_summaries", []):
|
||||
# Todo 변환 (제목만)
|
||||
todos = [
|
||||
ExtractedTodo(title=todo.get("title", ""))
|
||||
for todo in agenda_data.get("todos", [])
|
||||
]
|
||||
|
||||
agenda_summaries.append(
|
||||
AgendaSummary(
|
||||
agenda_number=agenda_data.get("agenda_number", 0),
|
||||
agenda_title=agenda_data.get("agenda_title", ""),
|
||||
summary_short=agenda_data.get("summary_short", ""),
|
||||
summary=agenda_data.get("summary", ""),
|
||||
pending=agenda_data.get("pending", []),
|
||||
todos=todos
|
||||
)
|
||||
)
|
||||
|
||||
# 통계 정보
|
||||
statistics = ai_result.get("statistics", {})
|
||||
statistics["participants_count"] = participants_count
|
||||
if duration_minutes:
|
||||
statistics["duration_minutes"] = duration_minutes
|
||||
|
||||
# 응답 생성
|
||||
return ConsolidateResponse(
|
||||
meeting_id=meeting_id,
|
||||
keywords=ai_result.get("keywords", []),
|
||||
statistics=statistics,
|
||||
decisions=ai_result.get("decisions", ""),
|
||||
agenda_summaries=agenda_summaries,
|
||||
generated_at=datetime.utcnow()
|
||||
)
|
||||
|
||||
|
||||
# 싱글톤 인스턴스
|
||||
transcript_service = TranscriptService()
|
||||
2
ai-python/logs/ai-python.log
Normal file
2
ai-python/logs/ai-python.log
Normal file
@ -0,0 +1,2 @@
|
||||
INFO: Will watch for changes in these directories: ['/Users/jominseo/HGZero/ai-python']
|
||||
ERROR: [Errno 48] Address already in use
|
||||
1577
ai-python/logs/ai-service.log
Normal file
1577
ai-python/logs/ai-service.log
Normal file
File diff suppressed because it is too large
Load Diff
58
ai-python/main.py
Normal file
58
ai-python/main.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""AI Service FastAPI Application"""
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from app.config import get_settings
|
||||
from app.api.v1 import router as api_v1_router
|
||||
import logging
|
||||
|
||||
# 로깅 설정
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Settings
|
||||
settings = get_settings()
|
||||
|
||||
# FastAPI 앱 생성
|
||||
app = FastAPI(
|
||||
title=settings.app_name,
|
||||
description="AI-powered meeting minutes analysis service",
|
||||
version="1.0.0",
|
||||
docs_url="/api/docs",
|
||||
redoc_url="/api/redoc",
|
||||
openapi_url="/api/openapi.json"
|
||||
)
|
||||
|
||||
# CORS 미들웨어 설정
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# API 라우터 등록
|
||||
app.include_router(api_v1_router, prefix="/api")
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""헬스 체크"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": settings.app_name
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host=settings.host,
|
||||
port=settings.port,
|
||||
reload=True,
|
||||
log_level=settings.log_level.lower()
|
||||
)
|
||||
11
ai-python/requirements.txt
Normal file
11
ai-python/requirements.txt
Normal file
@ -0,0 +1,11 @@
|
||||
# FastAPI
|
||||
fastapi==0.115.0
|
||||
uvicorn[standard]==0.32.0
|
||||
pydantic==2.9.2
|
||||
pydantic-settings==2.6.0
|
||||
|
||||
# Claude API
|
||||
anthropic==0.39.0
|
||||
|
||||
# Utilities
|
||||
python-dotenv==1.0.1
|
||||
File diff suppressed because it is too large
Load Diff
780
ai/logs/ai.log
780
ai/logs/ai.log
@ -1,780 +0,0 @@
|
||||
2025-10-23 16:24:30 [main] INFO com.unicorn.hgzero.ai.AiApplication - Starting AiApplication using Java 23.0.2 with PID 33322 (/Users/jominseo/HGZero/ai/build/classes/java/main started by jominseo in /Users/jominseo/HGZero/ai)
|
||||
2025-10-23 16:24:30 [main] DEBUG com.unicorn.hgzero.ai.AiApplication - Running with Spring Boot v3.3.0, Spring v6.1.8
|
||||
2025-10-23 16:24:30 [main] INFO com.unicorn.hgzero.ai.AiApplication - The following 1 profile is active: "local"
|
||||
2025-10-23 16:24:31 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||
2025-10-23 16:24:31 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in DEFAULT mode.
|
||||
2025-10-23 16:24:31 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 4 ms. Found 0 JPA repository interfaces.
|
||||
2025-10-23 16:24:31 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||
2025-10-23 16:24:31 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
||||
2025-10-23 16:24:31 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 0 ms. Found 0 Redis repository interfaces.
|
||||
2025-10-23 16:24:31 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port 8084 (http)
|
||||
2025-10-23 16:24:31 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
|
||||
2025-10-23 16:24:31 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.24]
|
||||
2025-10-23 16:24:31 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
|
||||
2025-10-23 16:24:31 [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 694 ms
|
||||
2025-10-23 16:24:31 [main] INFO o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
|
||||
2025-10-23 16:24:31 [main] INFO org.hibernate.Version - HHH000412: Hibernate ORM core version 6.5.2.Final
|
||||
2025-10-23 16:24:31 [main] INFO o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration boolean -> org.hibernate.type.BasicTypeReference@5c5f12e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration boolean -> org.hibernate.type.BasicTypeReference@5c5f12e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Boolean -> org.hibernate.type.BasicTypeReference@5c5f12e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration numeric_boolean -> org.hibernate.type.BasicTypeReference@23f8036d
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.NumericBooleanConverter -> org.hibernate.type.BasicTypeReference@23f8036d
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration true_false -> org.hibernate.type.BasicTypeReference@68f69ca3
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.TrueFalseConverter -> org.hibernate.type.BasicTypeReference@68f69ca3
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration yes_no -> org.hibernate.type.BasicTypeReference@1e3566e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.YesNoConverter -> org.hibernate.type.BasicTypeReference@1e3566e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte -> org.hibernate.type.BasicTypeReference@2b058bfd
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte -> org.hibernate.type.BasicTypeReference@2b058bfd
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Byte -> org.hibernate.type.BasicTypeReference@2b058bfd
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration binary -> org.hibernate.type.BasicTypeReference@4805069b
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte[] -> org.hibernate.type.BasicTypeReference@4805069b
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration [B -> org.hibernate.type.BasicTypeReference@4805069b
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration binary_wrapper -> org.hibernate.type.BasicTypeReference@14ca88bc
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration wrapper-binary -> org.hibernate.type.BasicTypeReference@14ca88bc
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration image -> org.hibernate.type.BasicTypeReference@f01fc6d
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration blob -> org.hibernate.type.BasicTypeReference@85cd413
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Blob -> org.hibernate.type.BasicTypeReference@85cd413
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_blob -> org.hibernate.type.BasicTypeReference@688d2a5d
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_blob_wrapper -> org.hibernate.type.BasicTypeReference@2842c098
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration short -> org.hibernate.type.BasicTypeReference@2820b369
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration short -> org.hibernate.type.BasicTypeReference@2820b369
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Short -> org.hibernate.type.BasicTypeReference@2820b369
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration integer -> org.hibernate.type.BasicTypeReference@46b21632
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration int -> org.hibernate.type.BasicTypeReference@46b21632
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Integer -> org.hibernate.type.BasicTypeReference@46b21632
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration long -> org.hibernate.type.BasicTypeReference@476c137b
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration long -> org.hibernate.type.BasicTypeReference@476c137b
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Long -> org.hibernate.type.BasicTypeReference@476c137b
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration float -> org.hibernate.type.BasicTypeReference@79144d0e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration float -> org.hibernate.type.BasicTypeReference@79144d0e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Float -> org.hibernate.type.BasicTypeReference@79144d0e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration double -> org.hibernate.type.BasicTypeReference@540212be
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration double -> org.hibernate.type.BasicTypeReference@540212be
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Double -> org.hibernate.type.BasicTypeReference@540212be
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration big_integer -> org.hibernate.type.BasicTypeReference@2579d8a
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.math.BigInteger -> org.hibernate.type.BasicTypeReference@2579d8a
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration big_decimal -> org.hibernate.type.BasicTypeReference@2507a170
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.math.BigDecimal -> org.hibernate.type.BasicTypeReference@2507a170
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration character -> org.hibernate.type.BasicTypeReference@7e20f4e3
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration char -> org.hibernate.type.BasicTypeReference@7e20f4e3
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Character -> org.hibernate.type.BasicTypeReference@7e20f4e3
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration character_nchar -> org.hibernate.type.BasicTypeReference@3af39e7b
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration string -> org.hibernate.type.BasicTypeReference@4f6ff62
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.String -> org.hibernate.type.BasicTypeReference@4f6ff62
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration nstring -> org.hibernate.type.BasicTypeReference@1c62d2ad
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration characters -> org.hibernate.type.BasicTypeReference@651caa2e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration char[] -> org.hibernate.type.BasicTypeReference@651caa2e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration [C -> org.hibernate.type.BasicTypeReference@651caa2e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration wrapper-characters -> org.hibernate.type.BasicTypeReference@433ae0b0
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration text -> org.hibernate.type.BasicTypeReference@70840a5a
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ntext -> org.hibernate.type.BasicTypeReference@7af9595d
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration clob -> org.hibernate.type.BasicTypeReference@7a34c1f6
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Clob -> org.hibernate.type.BasicTypeReference@7a34c1f6
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration nclob -> org.hibernate.type.BasicTypeReference@6e9f8160
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.NClob -> org.hibernate.type.BasicTypeReference@6e9f8160
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob -> org.hibernate.type.BasicTypeReference@3e998033
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob_char_array -> org.hibernate.type.BasicTypeReference@e1a150c
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob_character_array -> org.hibernate.type.BasicTypeReference@527d5e48
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob -> org.hibernate.type.BasicTypeReference@407b41e6
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob_character_array -> org.hibernate.type.BasicTypeReference@3291d9c2
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob_char_array -> org.hibernate.type.BasicTypeReference@6cfd08e9
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration Duration -> org.hibernate.type.BasicTypeReference@54ca9420
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.Duration -> org.hibernate.type.BasicTypeReference@54ca9420
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalDateTime -> org.hibernate.type.BasicTypeReference@4ea48b2e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalDateTime -> org.hibernate.type.BasicTypeReference@4ea48b2e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalDate -> org.hibernate.type.BasicTypeReference@72c704f1
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalDate -> org.hibernate.type.BasicTypeReference@72c704f1
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalTime -> org.hibernate.type.BasicTypeReference@76f9e000
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalTime -> org.hibernate.type.BasicTypeReference@76f9e000
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTime -> org.hibernate.type.BasicTypeReference@7612116b
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetDateTime -> org.hibernate.type.BasicTypeReference@7612116b
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTimeWithTimezone -> org.hibernate.type.BasicTypeReference@1c05097c
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@562f6681
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTime -> org.hibernate.type.BasicTypeReference@6f6f65a4
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetTime -> org.hibernate.type.BasicTypeReference@6f6f65a4
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeUtc -> org.hibernate.type.BasicTypeReference@990b86b
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeWithTimezone -> org.hibernate.type.BasicTypeReference@3dea1ecc
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@105c6c9e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTime -> org.hibernate.type.BasicTypeReference@40a7974
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.ZonedDateTime -> org.hibernate.type.BasicTypeReference@40a7974
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTimeWithTimezone -> org.hibernate.type.BasicTypeReference@8d5da7e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@65a4b9d6
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration date -> org.hibernate.type.BasicTypeReference@16ef1160
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Date -> org.hibernate.type.BasicTypeReference@16ef1160
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration time -> org.hibernate.type.BasicTypeReference@41f90b10
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Time -> org.hibernate.type.BasicTypeReference@41f90b10
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration timestamp -> org.hibernate.type.BasicTypeReference@67593f7b
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Timestamp -> org.hibernate.type.BasicTypeReference@67593f7b
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Date -> org.hibernate.type.BasicTypeReference@67593f7b
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar -> org.hibernate.type.BasicTypeReference@2773504f
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Calendar -> org.hibernate.type.BasicTypeReference@2773504f
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.GregorianCalendar -> org.hibernate.type.BasicTypeReference@2773504f
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar_date -> org.hibernate.type.BasicTypeReference@497921d0
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar_time -> org.hibernate.type.BasicTypeReference@40d10264
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration instant -> org.hibernate.type.BasicTypeReference@6edd4fe2
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.Instant -> org.hibernate.type.BasicTypeReference@6edd4fe2
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid -> org.hibernate.type.BasicTypeReference@53918b5e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.UUID -> org.hibernate.type.BasicTypeReference@53918b5e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration pg-uuid -> org.hibernate.type.BasicTypeReference@53918b5e
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid-binary -> org.hibernate.type.BasicTypeReference@5366575d
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid-char -> org.hibernate.type.BasicTypeReference@1b6cad77
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration class -> org.hibernate.type.BasicTypeReference@1fca53a7
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Class -> org.hibernate.type.BasicTypeReference@1fca53a7
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration currency -> org.hibernate.type.BasicTypeReference@40dee07b
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration Currency -> org.hibernate.type.BasicTypeReference@40dee07b
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Currency -> org.hibernate.type.BasicTypeReference@40dee07b
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration locale -> org.hibernate.type.BasicTypeReference@21e39b82
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Locale -> org.hibernate.type.BasicTypeReference@21e39b82
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration serializable -> org.hibernate.type.BasicTypeReference@5f9a8ddc
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.io.Serializable -> org.hibernate.type.BasicTypeReference@5f9a8ddc
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration timezone -> org.hibernate.type.BasicTypeReference@1280bae3
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.TimeZone -> org.hibernate.type.BasicTypeReference@1280bae3
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZoneOffset -> org.hibernate.type.BasicTypeReference@256a5df0
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.ZoneOffset -> org.hibernate.type.BasicTypeReference@256a5df0
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration url -> org.hibernate.type.BasicTypeReference@1868ed54
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.net.URL -> org.hibernate.type.BasicTypeReference@1868ed54
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration vector -> org.hibernate.type.BasicTypeReference@131777e8
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration row_version -> org.hibernate.type.BasicTypeReference@45790cb
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration object -> org.hibernate.type.JavaObjectType@2bc2e022
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Object -> org.hibernate.type.JavaObjectType@2bc2e022
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration null -> org.hibernate.type.NullType@1ff81b0d
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_date -> org.hibernate.type.BasicTypeReference@1c610f
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_time -> org.hibernate.type.BasicTypeReference@5abc5854
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_timestamp -> org.hibernate.type.BasicTypeReference@5c3007d
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar -> org.hibernate.type.BasicTypeReference@66b40dd3
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar_date -> org.hibernate.type.BasicTypeReference@7296fe0b
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar_time -> org.hibernate.type.BasicTypeReference@4a5066f5
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_binary -> org.hibernate.type.BasicTypeReference@578d472a
|
||||
2025-10-23 16:24:31 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_serializable -> org.hibernate.type.BasicTypeReference@1191029d
|
||||
2025-10-23 16:24:31 [main] INFO o.s.o.j.p.SpringPersistenceUnitInfo - No LoadTimeWeaver setup: ignoring JPA class transformer
|
||||
2025-10-23 16:24:31 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
|
||||
2025-10-23 16:24:32 [main] WARN o.h.e.j.e.i.JdbcEnvironmentInitiator - HHH000342: Could not obtain connection to query metadata
|
||||
java.lang.NullPointerException: Cannot invoke "org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(java.sql.SQLException, String)" because the return value of "org.hibernate.resource.transaction.backend.jdbc.internal.JdbcIsolationDelegate.sqlExceptionHelper()" is null
|
||||
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcIsolationDelegate.delegateWork(JdbcIsolationDelegate.java:116)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentUsingJdbcMetadata(JdbcEnvironmentInitiator.java:290)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:123)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:77)
|
||||
at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:130)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:263)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:238)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:215)
|
||||
at org.hibernate.boot.model.relational.Database.<init>(Database.java:45)
|
||||
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.getDatabase(InFlightMetadataCollectorImpl.java:221)
|
||||
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.<init>(InFlightMetadataCollectorImpl.java:189)
|
||||
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:171)
|
||||
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1431)
|
||||
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1502)
|
||||
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:75)
|
||||
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:390)
|
||||
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409)
|
||||
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396)
|
||||
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:366)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1835)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1784)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205)
|
||||
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:952)
|
||||
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
|
||||
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
|
||||
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:335)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352)
|
||||
at com.unicorn.hgzero.ai.AiApplication.main(AiApplication.java:20)
|
||||
2025-10-23 16:24:32 [main] ERROR o.s.o.j.LocalContainerEntityManagerFactoryBean - Failed to initialize JPA EntityManagerFactory: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||
2025-10-23 16:24:32 [main] WARN o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||
2025-10-23 16:24:32 [main] INFO o.a.catalina.core.StandardService - Stopping service [Tomcat]
|
||||
2025-10-23 16:24:32 [main] INFO o.s.b.a.l.ConditionEvaluationReportLogger -
|
||||
|
||||
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
|
||||
2025-10-23 16:24:32 [main] ERROR o.s.boot.SpringApplication - Application run failed
|
||||
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1788)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205)
|
||||
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:952)
|
||||
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
|
||||
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
|
||||
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:335)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352)
|
||||
at com.unicorn.hgzero.ai.AiApplication.main(AiApplication.java:20)
|
||||
Caused by: org.hibernate.service.spi.ServiceException: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:276)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:238)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:215)
|
||||
at org.hibernate.boot.model.relational.Database.<init>(Database.java:45)
|
||||
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.getDatabase(InFlightMetadataCollectorImpl.java:221)
|
||||
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.<init>(InFlightMetadataCollectorImpl.java:189)
|
||||
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:171)
|
||||
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1431)
|
||||
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1502)
|
||||
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:75)
|
||||
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:390)
|
||||
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409)
|
||||
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396)
|
||||
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:366)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1835)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1784)
|
||||
... 15 common frames omitted
|
||||
Caused by: org.hibernate.HibernateException: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||
at org.hibernate.engine.jdbc.dialect.internal.DialectFactoryImpl.determineDialect(DialectFactoryImpl.java:191)
|
||||
at org.hibernate.engine.jdbc.dialect.internal.DialectFactoryImpl.buildDialect(DialectFactoryImpl.java:87)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentWithDefaults(JdbcEnvironmentInitiator.java:152)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentUsingJdbcMetadata(JdbcEnvironmentInitiator.java:362)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:123)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:77)
|
||||
at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:130)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:263)
|
||||
... 30 common frames omitted
|
||||
2025-10-23 16:25:55 [main] INFO com.unicorn.hgzero.ai.AiApplication - Starting AiApplication using Java 23.0.2 with PID 33935 (/Users/jominseo/HGZero/ai/build/classes/java/main started by jominseo in /Users/jominseo/HGZero/ai)
|
||||
2025-10-23 16:25:55 [main] DEBUG com.unicorn.hgzero.ai.AiApplication - Running with Spring Boot v3.3.0, Spring v6.1.8
|
||||
2025-10-23 16:25:55 [main] INFO com.unicorn.hgzero.ai.AiApplication - The following 1 profile is active: "local"
|
||||
2025-10-23 16:25:55 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||
2025-10-23 16:25:55 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in DEFAULT mode.
|
||||
2025-10-23 16:25:56 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 3 ms. Found 0 JPA repository interfaces.
|
||||
2025-10-23 16:25:56 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||
2025-10-23 16:25:56 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
||||
2025-10-23 16:25:56 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 0 ms. Found 0 Redis repository interfaces.
|
||||
2025-10-23 16:25:56 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port 8084 (http)
|
||||
2025-10-23 16:25:56 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
|
||||
2025-10-23 16:25:56 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.24]
|
||||
2025-10-23 16:25:56 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
|
||||
2025-10-23 16:25:56 [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 608 ms
|
||||
2025-10-23 16:25:56 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
|
||||
2025-10-23 16:25:56 [main] INFO com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:testdb user=SA
|
||||
2025-10-23 16:25:56 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
|
||||
2025-10-23 16:25:56 [main] INFO o.s.b.a.h.H2ConsoleAutoConfiguration - H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:testdb'
|
||||
2025-10-23 16:25:56 [main] INFO o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
|
||||
2025-10-23 16:25:56 [main] INFO org.hibernate.Version - HHH000412: Hibernate ORM core version 6.5.2.Final
|
||||
2025-10-23 16:25:56 [main] INFO o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
|
||||
2025-10-23 16:25:56 [main] INFO o.s.o.j.p.SpringPersistenceUnitInfo - No LoadTimeWeaver setup: ignoring JPA class transformer
|
||||
2025-10-23 16:25:56 [main] WARN org.hibernate.orm.deprecation - HHH90000025: H2Dialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default)
|
||||
2025-10-23 16:25:56 [main] INFO o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
|
||||
2025-10-23 16:25:56 [main] INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Initialized JPA EntityManagerFactory for persistence unit 'default'
|
||||
2025-10-23 16:25:56 [main] WARN o.s.b.a.o.j.JpaBaseConfiguration$JpaWebConfiguration - spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
|
||||
2025-10-23 16:25:56 [main] ERROR i.n.r.d.DnsServerAddressStreamProviders - Unable to load io.netty.resolver.dns.macos.MacOSDnsServerAddressStreamProvider, fallback to system defaults. This may result in incorrect DNS resolutions on MacOS. Check whether you have a dependency on 'io.netty:netty-resolver-dns-native-macos'. Use DEBUG level to see the full stack: java.lang.UnsatisfiedLinkError: failed to load the required native library
|
||||
2025-10-23 16:25:57 [main] WARN o.s.b.a.s.s.UserDetailsServiceAutoConfiguration -
|
||||
|
||||
Using generated security password: 1665e64f-a0ac-49dc-806e-846f88237e7c
|
||||
|
||||
This generated password is for development use only. Your security configuration must be updated before running your application in production.
|
||||
|
||||
2025-10-23 16:25:57 [main] INFO o.s.s.c.a.a.c.InitializeUserDetailsBeanManagerConfigurer$InitializeUserDetailsManagerConfigurer - Global AuthenticationManager configured with UserDetailsService bean with name inMemoryUserDetailsManager
|
||||
2025-10-23 16:25:57 [main] INFO o.s.b.a.e.web.EndpointLinksResolver - Exposing 3 endpoints beneath base path '/actuator'
|
||||
2025-10-23 16:25:57 [main] INFO o.s.s.web.DefaultSecurityFilterChain - Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@54ad9ff9, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@3b6b9981, org.springframework.security.web.context.SecurityContextHolderFilter@3ce34b92, org.springframework.security.web.header.HeaderWriterFilter@4a89722e, org.springframework.web.filter.CorsFilter@2eb6e166, org.springframework.security.web.csrf.CsrfFilter@2c3762c7, org.springframework.security.web.authentication.logout.LogoutFilter@751e7d99, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@331b0bfd, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@20894afb, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@43b1fdb7, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@61d4e070, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@f511a8e, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@1b52f723, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@7bdbf06f, org.springframework.security.web.access.ExceptionTranslationFilter@64c009b8, org.springframework.security.web.access.intercept.AuthorizationFilter@7dfbdcfe]
|
||||
2025-10-23 16:25:57 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat started on port 8084 (http) with context path '/'
|
||||
2025-10-23 16:25:57 [main] INFO com.unicorn.hgzero.ai.AiApplication - Started AiApplication in 1.733 seconds (process running for 1.843)
|
||||
2025-10-23 16:26:47 [http-nio-8084-exec-1] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
|
||||
2025-10-23 16:26:47 [http-nio-8084-exec-1] INFO o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
|
||||
2025-10-23 16:26:47 [http-nio-8084-exec-1] INFO o.s.web.servlet.DispatcherServlet - Completed initialization in 0 ms
|
||||
2025-10-23 16:26:47 [http-nio-8084-exec-1] DEBUG o.s.security.web.FilterChainProxy - Securing GET /
|
||||
2025-10-23 16:26:47 [http-nio-8084-exec-1] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext
|
||||
2025-10-23 16:26:47 [http-nio-8084-exec-1] DEBUG o.s.s.w.s.HttpSessionRequestCache - Saved request http://localhost:8084/?continue to session
|
||||
2025-10-23 16:26:47 [http-nio-8084-exec-1] DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint - Trying to match using And [Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@32da6cef, matchingMediaTypes=[application/xhtml+xml, image/*, text/html, text/plain], useEquals=false, ignoredMediaTypes=[*/*]]]
|
||||
2025-10-23 16:26:47 [http-nio-8084-exec-1] DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint - Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint@104972a0
|
||||
2025-10-23 16:26:47 [http-nio-8084-exec-1] DEBUG o.s.s.web.DefaultRedirectStrategy - Redirecting to http://localhost:8084/login
|
||||
2025-10-23 16:26:47 [http-nio-8084-exec-2] DEBUG o.s.security.web.FilterChainProxy - Securing GET /login
|
||||
2025-10-23 16:26:47 [http-nio-8084-exec-3] DEBUG o.s.security.web.FilterChainProxy - Securing GET /favicon.ico
|
||||
2025-10-23 16:26:47 [http-nio-8084-exec-3] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext
|
||||
2025-10-23 16:26:47 [http-nio-8084-exec-3] DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint - Trying to match using And [Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@32da6cef, matchingMediaTypes=[application/xhtml+xml, image/*, text/html, text/plain], useEquals=false, ignoredMediaTypes=[*/*]]]
|
||||
2025-10-23 16:26:47 [http-nio-8084-exec-3] DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint - Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint@104972a0
|
||||
2025-10-23 16:26:47 [http-nio-8084-exec-3] DEBUG o.s.s.web.DefaultRedirectStrategy - Redirecting to http://localhost:8084/login
|
||||
2025-10-23 16:26:47 [http-nio-8084-exec-4] DEBUG o.s.security.web.FilterChainProxy - Securing GET /login
|
||||
2025-10-23 16:27:13 [SpringApplicationShutdownHook] INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Closing JPA EntityManagerFactory for persistence unit 'default'
|
||||
2025-10-23 16:27:13 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
|
||||
2025-10-23 16:27:13 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
|
||||
2025-10-23 17:10:12 [main] INFO com.unicorn.hgzero.ai.AiApplication - Starting AiApplication using Java 23.0.2 with PID 43825 (/Users/jominseo/HGZero/ai/build/classes/java/main started by jominseo in /Users/jominseo/HGZero/ai)
|
||||
2025-10-23 17:10:12 [main] DEBUG com.unicorn.hgzero.ai.AiApplication - Running with Spring Boot v3.3.0, Spring v6.1.8
|
||||
2025-10-23 17:10:12 [main] INFO com.unicorn.hgzero.ai.AiApplication - The following 1 profile is active: "dev"
|
||||
2025-10-23 17:10:12 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||
2025-10-23 17:10:12 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in DEFAULT mode.
|
||||
2025-10-23 17:10:12 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 3 ms. Found 0 JPA repository interfaces.
|
||||
2025-10-23 17:10:12 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||
2025-10-23 17:10:12 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
||||
2025-10-23 17:10:12 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 0 ms. Found 0 Redis repository interfaces.
|
||||
2025-10-23 17:10:13 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port 8083 (http)
|
||||
2025-10-23 17:10:13 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
|
||||
2025-10-23 17:10:13 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.24]
|
||||
2025-10-23 17:10:13 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
|
||||
2025-10-23 17:10:13 [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 668 ms
|
||||
2025-10-23 17:10:13 [main] INFO o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
|
||||
2025-10-23 17:10:13 [main] INFO org.hibernate.Version - HHH000412: Hibernate ORM core version 6.5.2.Final
|
||||
2025-10-23 17:10:13 [main] INFO o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration boolean -> org.hibernate.type.BasicTypeReference@66716959
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration boolean -> org.hibernate.type.BasicTypeReference@66716959
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Boolean -> org.hibernate.type.BasicTypeReference@66716959
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration numeric_boolean -> org.hibernate.type.BasicTypeReference@34e07e65
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.NumericBooleanConverter -> org.hibernate.type.BasicTypeReference@34e07e65
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration true_false -> org.hibernate.type.BasicTypeReference@7ca0166c
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.TrueFalseConverter -> org.hibernate.type.BasicTypeReference@7ca0166c
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration yes_no -> org.hibernate.type.BasicTypeReference@1dcad16f
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.YesNoConverter -> org.hibernate.type.BasicTypeReference@1dcad16f
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte -> org.hibernate.type.BasicTypeReference@701c482e
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte -> org.hibernate.type.BasicTypeReference@701c482e
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Byte -> org.hibernate.type.BasicTypeReference@701c482e
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration binary -> org.hibernate.type.BasicTypeReference@4738131e
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte[] -> org.hibernate.type.BasicTypeReference@4738131e
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration [B -> org.hibernate.type.BasicTypeReference@4738131e
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration binary_wrapper -> org.hibernate.type.BasicTypeReference@3b576ee3
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration wrapper-binary -> org.hibernate.type.BasicTypeReference@3b576ee3
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration image -> org.hibernate.type.BasicTypeReference@705d914f
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration blob -> org.hibernate.type.BasicTypeReference@6212ea52
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Blob -> org.hibernate.type.BasicTypeReference@6212ea52
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_blob -> org.hibernate.type.BasicTypeReference@65b5b5ed
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_blob_wrapper -> org.hibernate.type.BasicTypeReference@6595ffce
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration short -> org.hibernate.type.BasicTypeReference@795eddda
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration short -> org.hibernate.type.BasicTypeReference@795eddda
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Short -> org.hibernate.type.BasicTypeReference@795eddda
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration integer -> org.hibernate.type.BasicTypeReference@c6bf8d9
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration int -> org.hibernate.type.BasicTypeReference@c6bf8d9
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Integer -> org.hibernate.type.BasicTypeReference@c6bf8d9
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration long -> org.hibernate.type.BasicTypeReference@44392e64
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration long -> org.hibernate.type.BasicTypeReference@44392e64
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Long -> org.hibernate.type.BasicTypeReference@44392e64
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration float -> org.hibernate.type.BasicTypeReference@e18d2a2
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration float -> org.hibernate.type.BasicTypeReference@e18d2a2
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Float -> org.hibernate.type.BasicTypeReference@e18d2a2
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration double -> org.hibernate.type.BasicTypeReference@1a77eb6
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration double -> org.hibernate.type.BasicTypeReference@1a77eb6
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Double -> org.hibernate.type.BasicTypeReference@1a77eb6
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration big_integer -> org.hibernate.type.BasicTypeReference@52d9f36b
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.math.BigInteger -> org.hibernate.type.BasicTypeReference@52d9f36b
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration big_decimal -> org.hibernate.type.BasicTypeReference@5f9ebd5a
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.math.BigDecimal -> org.hibernate.type.BasicTypeReference@5f9ebd5a
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration character -> org.hibernate.type.BasicTypeReference@175bf9c9
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration char -> org.hibernate.type.BasicTypeReference@175bf9c9
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Character -> org.hibernate.type.BasicTypeReference@175bf9c9
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration character_nchar -> org.hibernate.type.BasicTypeReference@2db3675a
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration string -> org.hibernate.type.BasicTypeReference@306c9b2c
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.String -> org.hibernate.type.BasicTypeReference@306c9b2c
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration nstring -> org.hibernate.type.BasicTypeReference@1ab28416
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration characters -> org.hibernate.type.BasicTypeReference@52efb338
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration char[] -> org.hibernate.type.BasicTypeReference@52efb338
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration [C -> org.hibernate.type.BasicTypeReference@52efb338
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration wrapper-characters -> org.hibernate.type.BasicTypeReference@64508788
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration text -> org.hibernate.type.BasicTypeReference@30b1c5d5
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ntext -> org.hibernate.type.BasicTypeReference@3e2d65e1
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration clob -> org.hibernate.type.BasicTypeReference@1174676f
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Clob -> org.hibernate.type.BasicTypeReference@1174676f
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration nclob -> org.hibernate.type.BasicTypeReference@71f8ce0e
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.NClob -> org.hibernate.type.BasicTypeReference@71f8ce0e
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob -> org.hibernate.type.BasicTypeReference@4fd92289
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob_char_array -> org.hibernate.type.BasicTypeReference@1a8e44fe
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob_character_array -> org.hibernate.type.BasicTypeReference@287317df
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob -> org.hibernate.type.BasicTypeReference@1fcc3461
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob_character_array -> org.hibernate.type.BasicTypeReference@1987807b
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob_char_array -> org.hibernate.type.BasicTypeReference@71469e01
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration Duration -> org.hibernate.type.BasicTypeReference@41bbb219
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.Duration -> org.hibernate.type.BasicTypeReference@41bbb219
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalDateTime -> org.hibernate.type.BasicTypeReference@3f2ae973
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalDateTime -> org.hibernate.type.BasicTypeReference@3f2ae973
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalDate -> org.hibernate.type.BasicTypeReference@1a8b22b5
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalDate -> org.hibernate.type.BasicTypeReference@1a8b22b5
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalTime -> org.hibernate.type.BasicTypeReference@5f781173
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalTime -> org.hibernate.type.BasicTypeReference@5f781173
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTime -> org.hibernate.type.BasicTypeReference@43cf5bff
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetDateTime -> org.hibernate.type.BasicTypeReference@43cf5bff
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTimeWithTimezone -> org.hibernate.type.BasicTypeReference@2b464384
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@681b42d3
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTime -> org.hibernate.type.BasicTypeReference@77f7352a
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetTime -> org.hibernate.type.BasicTypeReference@77f7352a
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeUtc -> org.hibernate.type.BasicTypeReference@4ede8888
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeWithTimezone -> org.hibernate.type.BasicTypeReference@571db8b4
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@65a2755e
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTime -> org.hibernate.type.BasicTypeReference@2b3242a5
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.ZonedDateTime -> org.hibernate.type.BasicTypeReference@2b3242a5
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTimeWithTimezone -> org.hibernate.type.BasicTypeReference@11120583
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@2bf0c70d
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration date -> org.hibernate.type.BasicTypeReference@5d8e4fa8
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Date -> org.hibernate.type.BasicTypeReference@5d8e4fa8
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration time -> org.hibernate.type.BasicTypeReference@649009d6
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Time -> org.hibernate.type.BasicTypeReference@649009d6
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration timestamp -> org.hibernate.type.BasicTypeReference@652f26da
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Timestamp -> org.hibernate.type.BasicTypeReference@652f26da
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Date -> org.hibernate.type.BasicTypeReference@652f26da
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar -> org.hibernate.type.BasicTypeReference@484a5ddd
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Calendar -> org.hibernate.type.BasicTypeReference@484a5ddd
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.GregorianCalendar -> org.hibernate.type.BasicTypeReference@484a5ddd
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar_date -> org.hibernate.type.BasicTypeReference@6796a873
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar_time -> org.hibernate.type.BasicTypeReference@3acc3ee
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration instant -> org.hibernate.type.BasicTypeReference@1f293cb7
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.Instant -> org.hibernate.type.BasicTypeReference@1f293cb7
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid -> org.hibernate.type.BasicTypeReference@5972e3a
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.UUID -> org.hibernate.type.BasicTypeReference@5972e3a
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration pg-uuid -> org.hibernate.type.BasicTypeReference@5972e3a
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid-binary -> org.hibernate.type.BasicTypeReference@5790cbcb
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid-char -> org.hibernate.type.BasicTypeReference@32c6d164
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration class -> org.hibernate.type.BasicTypeReference@645c9f0f
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Class -> org.hibernate.type.BasicTypeReference@645c9f0f
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration currency -> org.hibernate.type.BasicTypeReference@58068b40
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration Currency -> org.hibernate.type.BasicTypeReference@58068b40
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Currency -> org.hibernate.type.BasicTypeReference@58068b40
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration locale -> org.hibernate.type.BasicTypeReference@999cd18
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Locale -> org.hibernate.type.BasicTypeReference@999cd18
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration serializable -> org.hibernate.type.BasicTypeReference@dd060be
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.io.Serializable -> org.hibernate.type.BasicTypeReference@dd060be
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration timezone -> org.hibernate.type.BasicTypeReference@df432ec
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.TimeZone -> org.hibernate.type.BasicTypeReference@df432ec
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZoneOffset -> org.hibernate.type.BasicTypeReference@6144e499
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.ZoneOffset -> org.hibernate.type.BasicTypeReference@6144e499
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration url -> org.hibernate.type.BasicTypeReference@26f204a4
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.net.URL -> org.hibernate.type.BasicTypeReference@26f204a4
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration vector -> org.hibernate.type.BasicTypeReference@28295554
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration row_version -> org.hibernate.type.BasicTypeReference@4e671ef
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration object -> org.hibernate.type.JavaObjectType@2aac6fa7
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Object -> org.hibernate.type.JavaObjectType@2aac6fa7
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration null -> org.hibernate.type.NullType@2358443e
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_date -> org.hibernate.type.BasicTypeReference@25e796fe
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_time -> org.hibernate.type.BasicTypeReference@29ba63f0
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_timestamp -> org.hibernate.type.BasicTypeReference@4822ab4d
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar -> org.hibernate.type.BasicTypeReference@516b84d1
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar_date -> org.hibernate.type.BasicTypeReference@1ad1f167
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar_time -> org.hibernate.type.BasicTypeReference@608eb42e
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_binary -> org.hibernate.type.BasicTypeReference@3d2b13b1
|
||||
2025-10-23 17:10:13 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_serializable -> org.hibernate.type.BasicTypeReference@30eb55c9
|
||||
2025-10-23 17:10:13 [main] INFO o.s.o.j.p.SpringPersistenceUnitInfo - No LoadTimeWeaver setup: ignoring JPA class transformer
|
||||
2025-10-23 17:10:13 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
|
||||
2025-10-23 17:10:14 [main] WARN o.h.e.j.e.i.JdbcEnvironmentInitiator - HHH000342: Could not obtain connection to query metadata
|
||||
java.lang.NullPointerException: Cannot invoke "org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(java.sql.SQLException, String)" because the return value of "org.hibernate.resource.transaction.backend.jdbc.internal.JdbcIsolationDelegate.sqlExceptionHelper()" is null
|
||||
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcIsolationDelegate.delegateWork(JdbcIsolationDelegate.java:116)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentUsingJdbcMetadata(JdbcEnvironmentInitiator.java:290)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:123)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:77)
|
||||
at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:130)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:263)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:238)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:215)
|
||||
at org.hibernate.boot.model.relational.Database.<init>(Database.java:45)
|
||||
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.getDatabase(InFlightMetadataCollectorImpl.java:221)
|
||||
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.<init>(InFlightMetadataCollectorImpl.java:189)
|
||||
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:171)
|
||||
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1431)
|
||||
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1502)
|
||||
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:75)
|
||||
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:390)
|
||||
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409)
|
||||
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396)
|
||||
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:366)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1835)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1784)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205)
|
||||
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:952)
|
||||
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
|
||||
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
|
||||
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:335)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352)
|
||||
at com.unicorn.hgzero.ai.AiApplication.main(AiApplication.java:20)
|
||||
2025-10-23 17:10:14 [main] ERROR o.s.o.j.LocalContainerEntityManagerFactoryBean - Failed to initialize JPA EntityManagerFactory: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||
2025-10-23 17:10:14 [main] WARN o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||
2025-10-23 17:10:14 [main] INFO o.a.catalina.core.StandardService - Stopping service [Tomcat]
|
||||
2025-10-23 17:10:14 [main] INFO o.s.b.a.l.ConditionEvaluationReportLogger -
|
||||
|
||||
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
|
||||
2025-10-23 17:10:14 [main] ERROR o.s.boot.SpringApplication - Application run failed
|
||||
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1788)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205)
|
||||
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:952)
|
||||
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
|
||||
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
|
||||
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:335)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352)
|
||||
at com.unicorn.hgzero.ai.AiApplication.main(AiApplication.java:20)
|
||||
Caused by: org.hibernate.service.spi.ServiceException: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:276)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:238)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:215)
|
||||
at org.hibernate.boot.model.relational.Database.<init>(Database.java:45)
|
||||
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.getDatabase(InFlightMetadataCollectorImpl.java:221)
|
||||
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.<init>(InFlightMetadataCollectorImpl.java:189)
|
||||
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:171)
|
||||
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1431)
|
||||
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1502)
|
||||
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:75)
|
||||
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:390)
|
||||
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409)
|
||||
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396)
|
||||
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:366)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1835)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1784)
|
||||
... 15 common frames omitted
|
||||
Caused by: org.hibernate.HibernateException: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||
at org.hibernate.engine.jdbc.dialect.internal.DialectFactoryImpl.determineDialect(DialectFactoryImpl.java:191)
|
||||
at org.hibernate.engine.jdbc.dialect.internal.DialectFactoryImpl.buildDialect(DialectFactoryImpl.java:87)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentWithDefaults(JdbcEnvironmentInitiator.java:152)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentUsingJdbcMetadata(JdbcEnvironmentInitiator.java:362)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:123)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:77)
|
||||
at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:130)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:263)
|
||||
... 30 common frames omitted
|
||||
2025-10-23 17:38:09 [main] INFO com.unicorn.hgzero.ai.AiApplication - Starting AiApplication using Java 23.0.2 with PID 49971 (/Users/jominseo/HGZero/ai/build/classes/java/main started by jominseo in /Users/jominseo/HGZero/ai)
|
||||
2025-10-23 17:38:09 [main] DEBUG com.unicorn.hgzero.ai.AiApplication - Running with Spring Boot v3.3.0, Spring v6.1.8
|
||||
2025-10-23 17:38:09 [main] INFO com.unicorn.hgzero.ai.AiApplication - The following 1 profile is active: "dev"
|
||||
2025-10-23 17:38:09 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||
2025-10-23 17:38:09 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in DEFAULT mode.
|
||||
2025-10-23 17:38:09 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 4 ms. Found 0 JPA repository interfaces.
|
||||
2025-10-23 17:38:09 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||
2025-10-23 17:38:09 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
||||
2025-10-23 17:38:09 [main] INFO o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 0 ms. Found 0 Redis repository interfaces.
|
||||
2025-10-23 17:38:09 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port 8083 (http)
|
||||
2025-10-23 17:38:09 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
|
||||
2025-10-23 17:38:09 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.24]
|
||||
2025-10-23 17:38:09 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
|
||||
2025-10-23 17:38:09 [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 679 ms
|
||||
2025-10-23 17:38:10 [main] INFO o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
|
||||
2025-10-23 17:38:10 [main] INFO org.hibernate.Version - HHH000412: Hibernate ORM core version 6.5.2.Final
|
||||
2025-10-23 17:38:10 [main] INFO o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration boolean -> org.hibernate.type.BasicTypeReference@306c9b2c
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration boolean -> org.hibernate.type.BasicTypeReference@306c9b2c
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Boolean -> org.hibernate.type.BasicTypeReference@306c9b2c
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration numeric_boolean -> org.hibernate.type.BasicTypeReference@1ab28416
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.NumericBooleanConverter -> org.hibernate.type.BasicTypeReference@1ab28416
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration true_false -> org.hibernate.type.BasicTypeReference@52efb338
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.TrueFalseConverter -> org.hibernate.type.BasicTypeReference@52efb338
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration yes_no -> org.hibernate.type.BasicTypeReference@64508788
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration org.hibernate.type.YesNoConverter -> org.hibernate.type.BasicTypeReference@64508788
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte -> org.hibernate.type.BasicTypeReference@30b1c5d5
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte -> org.hibernate.type.BasicTypeReference@30b1c5d5
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Byte -> org.hibernate.type.BasicTypeReference@30b1c5d5
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration binary -> org.hibernate.type.BasicTypeReference@3e2d65e1
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration byte[] -> org.hibernate.type.BasicTypeReference@3e2d65e1
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration [B -> org.hibernate.type.BasicTypeReference@3e2d65e1
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration binary_wrapper -> org.hibernate.type.BasicTypeReference@1174676f
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration wrapper-binary -> org.hibernate.type.BasicTypeReference@1174676f
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration image -> org.hibernate.type.BasicTypeReference@71f8ce0e
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration blob -> org.hibernate.type.BasicTypeReference@4fd92289
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Blob -> org.hibernate.type.BasicTypeReference@4fd92289
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_blob -> org.hibernate.type.BasicTypeReference@1a8e44fe
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_blob_wrapper -> org.hibernate.type.BasicTypeReference@287317df
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration short -> org.hibernate.type.BasicTypeReference@1fcc3461
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration short -> org.hibernate.type.BasicTypeReference@1fcc3461
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Short -> org.hibernate.type.BasicTypeReference@1fcc3461
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration integer -> org.hibernate.type.BasicTypeReference@1987807b
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration int -> org.hibernate.type.BasicTypeReference@1987807b
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Integer -> org.hibernate.type.BasicTypeReference@1987807b
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration long -> org.hibernate.type.BasicTypeReference@71469e01
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration long -> org.hibernate.type.BasicTypeReference@71469e01
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Long -> org.hibernate.type.BasicTypeReference@71469e01
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration float -> org.hibernate.type.BasicTypeReference@41bbb219
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration float -> org.hibernate.type.BasicTypeReference@41bbb219
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Float -> org.hibernate.type.BasicTypeReference@41bbb219
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration double -> org.hibernate.type.BasicTypeReference@3f2ae973
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration double -> org.hibernate.type.BasicTypeReference@3f2ae973
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Double -> org.hibernate.type.BasicTypeReference@3f2ae973
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration big_integer -> org.hibernate.type.BasicTypeReference@1a8b22b5
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.math.BigInteger -> org.hibernate.type.BasicTypeReference@1a8b22b5
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration big_decimal -> org.hibernate.type.BasicTypeReference@5f781173
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.math.BigDecimal -> org.hibernate.type.BasicTypeReference@5f781173
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration character -> org.hibernate.type.BasicTypeReference@43cf5bff
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration char -> org.hibernate.type.BasicTypeReference@43cf5bff
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Character -> org.hibernate.type.BasicTypeReference@43cf5bff
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration character_nchar -> org.hibernate.type.BasicTypeReference@2b464384
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration string -> org.hibernate.type.BasicTypeReference@681b42d3
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.String -> org.hibernate.type.BasicTypeReference@681b42d3
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration nstring -> org.hibernate.type.BasicTypeReference@77f7352a
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration characters -> org.hibernate.type.BasicTypeReference@4ede8888
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration char[] -> org.hibernate.type.BasicTypeReference@4ede8888
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration [C -> org.hibernate.type.BasicTypeReference@4ede8888
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration wrapper-characters -> org.hibernate.type.BasicTypeReference@571db8b4
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration text -> org.hibernate.type.BasicTypeReference@65a2755e
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ntext -> org.hibernate.type.BasicTypeReference@2b3242a5
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration clob -> org.hibernate.type.BasicTypeReference@11120583
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Clob -> org.hibernate.type.BasicTypeReference@11120583
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration nclob -> org.hibernate.type.BasicTypeReference@2bf0c70d
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.NClob -> org.hibernate.type.BasicTypeReference@2bf0c70d
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob -> org.hibernate.type.BasicTypeReference@5d8e4fa8
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob_char_array -> org.hibernate.type.BasicTypeReference@649009d6
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_clob_character_array -> org.hibernate.type.BasicTypeReference@652f26da
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob -> org.hibernate.type.BasicTypeReference@484a5ddd
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob_character_array -> org.hibernate.type.BasicTypeReference@6796a873
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration materialized_nclob_char_array -> org.hibernate.type.BasicTypeReference@3acc3ee
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration Duration -> org.hibernate.type.BasicTypeReference@1f293cb7
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.Duration -> org.hibernate.type.BasicTypeReference@1f293cb7
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalDateTime -> org.hibernate.type.BasicTypeReference@5972e3a
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalDateTime -> org.hibernate.type.BasicTypeReference@5972e3a
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalDate -> org.hibernate.type.BasicTypeReference@5790cbcb
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalDate -> org.hibernate.type.BasicTypeReference@5790cbcb
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration LocalTime -> org.hibernate.type.BasicTypeReference@32c6d164
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.LocalTime -> org.hibernate.type.BasicTypeReference@32c6d164
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTime -> org.hibernate.type.BasicTypeReference@645c9f0f
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetDateTime -> org.hibernate.type.BasicTypeReference@645c9f0f
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTimeWithTimezone -> org.hibernate.type.BasicTypeReference@58068b40
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetDateTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@999cd18
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTime -> org.hibernate.type.BasicTypeReference@dd060be
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.OffsetTime -> org.hibernate.type.BasicTypeReference@dd060be
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeUtc -> org.hibernate.type.BasicTypeReference@df432ec
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeWithTimezone -> org.hibernate.type.BasicTypeReference@6144e499
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration OffsetTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@26f204a4
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTime -> org.hibernate.type.BasicTypeReference@28295554
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.ZonedDateTime -> org.hibernate.type.BasicTypeReference@28295554
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTimeWithTimezone -> org.hibernate.type.BasicTypeReference@4e671ef
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZonedDateTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@42403dc6
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration date -> org.hibernate.type.BasicTypeReference@74a1d60e
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Date -> org.hibernate.type.BasicTypeReference@74a1d60e
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration time -> org.hibernate.type.BasicTypeReference@16c0be3b
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Time -> org.hibernate.type.BasicTypeReference@16c0be3b
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration timestamp -> org.hibernate.type.BasicTypeReference@219edc05
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.sql.Timestamp -> org.hibernate.type.BasicTypeReference@219edc05
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Date -> org.hibernate.type.BasicTypeReference@219edc05
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar -> org.hibernate.type.BasicTypeReference@62f37bfd
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Calendar -> org.hibernate.type.BasicTypeReference@62f37bfd
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.GregorianCalendar -> org.hibernate.type.BasicTypeReference@62f37bfd
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar_date -> org.hibernate.type.BasicTypeReference@1818d00b
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration calendar_time -> org.hibernate.type.BasicTypeReference@b3a8455
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration instant -> org.hibernate.type.BasicTypeReference@5c930fc3
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.Instant -> org.hibernate.type.BasicTypeReference@5c930fc3
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid -> org.hibernate.type.BasicTypeReference@25c6ab3f
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.UUID -> org.hibernate.type.BasicTypeReference@25c6ab3f
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration pg-uuid -> org.hibernate.type.BasicTypeReference@25c6ab3f
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid-binary -> org.hibernate.type.BasicTypeReference@7b80af04
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration uuid-char -> org.hibernate.type.BasicTypeReference@2447940d
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration class -> org.hibernate.type.BasicTypeReference@60ee7a51
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Class -> org.hibernate.type.BasicTypeReference@60ee7a51
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration currency -> org.hibernate.type.BasicTypeReference@70e1aa20
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration Currency -> org.hibernate.type.BasicTypeReference@70e1aa20
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Currency -> org.hibernate.type.BasicTypeReference@70e1aa20
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration locale -> org.hibernate.type.BasicTypeReference@e67d3b7
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.Locale -> org.hibernate.type.BasicTypeReference@e67d3b7
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration serializable -> org.hibernate.type.BasicTypeReference@1618c98a
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.io.Serializable -> org.hibernate.type.BasicTypeReference@1618c98a
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration timezone -> org.hibernate.type.BasicTypeReference@5b715ea
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.util.TimeZone -> org.hibernate.type.BasicTypeReference@5b715ea
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration ZoneOffset -> org.hibernate.type.BasicTypeReference@787a0fd6
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.time.ZoneOffset -> org.hibernate.type.BasicTypeReference@787a0fd6
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration url -> org.hibernate.type.BasicTypeReference@48b09105
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.net.URL -> org.hibernate.type.BasicTypeReference@48b09105
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration vector -> org.hibernate.type.BasicTypeReference@18b45500
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration row_version -> org.hibernate.type.BasicTypeReference@25110bb9
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration object -> org.hibernate.type.JavaObjectType@30eb55c9
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration java.lang.Object -> org.hibernate.type.JavaObjectType@30eb55c9
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration null -> org.hibernate.type.NullType@5badeda0
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_date -> org.hibernate.type.BasicTypeReference@56a9a7b5
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_time -> org.hibernate.type.BasicTypeReference@338270ea
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_timestamp -> org.hibernate.type.BasicTypeReference@7f64bd7
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar -> org.hibernate.type.BasicTypeReference@1c79d093
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar_date -> org.hibernate.type.BasicTypeReference@746fd19b
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_calendar_time -> org.hibernate.type.BasicTypeReference@54caeadc
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_binary -> org.hibernate.type.BasicTypeReference@61d7bb61
|
||||
2025-10-23 17:38:10 [main] DEBUG o.hibernate.type.BasicTypeRegistry - Adding type registration imm_serializable -> org.hibernate.type.BasicTypeReference@33f81280
|
||||
2025-10-23 17:38:10 [main] INFO o.s.o.j.p.SpringPersistenceUnitInfo - No LoadTimeWeaver setup: ignoring JPA class transformer
|
||||
2025-10-23 17:38:10 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
|
||||
2025-10-23 17:38:11 [main] WARN o.h.e.j.e.i.JdbcEnvironmentInitiator - HHH000342: Could not obtain connection to query metadata
|
||||
java.lang.NullPointerException: Cannot invoke "org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(java.sql.SQLException, String)" because the return value of "org.hibernate.resource.transaction.backend.jdbc.internal.JdbcIsolationDelegate.sqlExceptionHelper()" is null
|
||||
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcIsolationDelegate.delegateWork(JdbcIsolationDelegate.java:116)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentUsingJdbcMetadata(JdbcEnvironmentInitiator.java:290)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:123)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:77)
|
||||
at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:130)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:263)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:238)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:215)
|
||||
at org.hibernate.boot.model.relational.Database.<init>(Database.java:45)
|
||||
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.getDatabase(InFlightMetadataCollectorImpl.java:221)
|
||||
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.<init>(InFlightMetadataCollectorImpl.java:189)
|
||||
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:171)
|
||||
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1431)
|
||||
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1502)
|
||||
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:75)
|
||||
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:390)
|
||||
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409)
|
||||
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396)
|
||||
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:366)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1835)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1784)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205)
|
||||
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:952)
|
||||
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
|
||||
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
|
||||
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:335)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352)
|
||||
at com.unicorn.hgzero.ai.AiApplication.main(AiApplication.java:20)
|
||||
2025-10-23 17:38:11 [main] ERROR o.s.o.j.LocalContainerEntityManagerFactoryBean - Failed to initialize JPA EntityManagerFactory: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||
2025-10-23 17:38:11 [main] WARN o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||
2025-10-23 17:38:11 [main] INFO o.a.catalina.core.StandardService - Stopping service [Tomcat]
|
||||
2025-10-23 17:38:11 [main] INFO o.s.b.a.l.ConditionEvaluationReportLogger -
|
||||
|
||||
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
|
||||
2025-10-23 17:38:11 [main] ERROR o.s.boot.SpringApplication - Application run failed
|
||||
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1788)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205)
|
||||
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:952)
|
||||
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
|
||||
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
|
||||
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:335)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352)
|
||||
at com.unicorn.hgzero.ai.AiApplication.main(AiApplication.java:20)
|
||||
Caused by: org.hibernate.service.spi.ServiceException: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment] due to: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:276)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:238)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:215)
|
||||
at org.hibernate.boot.model.relational.Database.<init>(Database.java:45)
|
||||
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.getDatabase(InFlightMetadataCollectorImpl.java:221)
|
||||
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.<init>(InFlightMetadataCollectorImpl.java:189)
|
||||
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:171)
|
||||
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1431)
|
||||
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1502)
|
||||
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:75)
|
||||
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:390)
|
||||
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409)
|
||||
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396)
|
||||
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:366)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1835)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1784)
|
||||
... 15 common frames omitted
|
||||
Caused by: org.hibernate.HibernateException: Unable to determine Dialect without JDBC metadata (please set 'jakarta.persistence.jdbc.url' for common cases or 'hibernate.dialect' when a custom Dialect implementation must be provided)
|
||||
at org.hibernate.engine.jdbc.dialect.internal.DialectFactoryImpl.determineDialect(DialectFactoryImpl.java:191)
|
||||
at org.hibernate.engine.jdbc.dialect.internal.DialectFactoryImpl.buildDialect(DialectFactoryImpl.java:87)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentWithDefaults(JdbcEnvironmentInitiator.java:152)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentUsingJdbcMetadata(JdbcEnvironmentInitiator.java:362)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:123)
|
||||
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:77)
|
||||
at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:130)
|
||||
at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:263)
|
||||
... 30 common frames omitted
|
||||
392
claude/MEETING-AI-TEST-GUIDE.md
Normal file
392
claude/MEETING-AI-TEST-GUIDE.md
Normal file
@ -0,0 +1,392 @@
|
||||
# Meeting AI 통합 실행 및 테스트 가이드
|
||||
|
||||
작성일: 2025-10-28
|
||||
작성자: 이동욱 (Backend Developer)
|
||||
|
||||
## 📋 목차
|
||||
1. [사전 준비](#사전-준비)
|
||||
2. [AI Python Service 실행](#ai-python-service-실행)
|
||||
3. [Meeting Service 실행](#meeting-service-실행)
|
||||
4. [통합 테스트](#통합-테스트)
|
||||
5. [트러블슈팅](#트러블슈팅)
|
||||
|
||||
---
|
||||
|
||||
## 사전 준비
|
||||
|
||||
### 1. 포트 확인
|
||||
```bash
|
||||
# 포트 사용 확인
|
||||
lsof -i :8082 # Meeting Service
|
||||
lsof -i :8087 # AI Python Service
|
||||
```
|
||||
|
||||
### 2. 데이터베이스 확인
|
||||
```sql
|
||||
-- PostgreSQL 연결 확인
|
||||
psql -h 4.230.48.72 -U hgzerouser -d meetingdb
|
||||
|
||||
-- 필요한 테이블 확인
|
||||
\dt meeting_analysis
|
||||
\dt todos
|
||||
\dt meetings
|
||||
\dt agenda_sections
|
||||
```
|
||||
|
||||
### 3. Redis 확인
|
||||
```bash
|
||||
# Redis 연결 테스트
|
||||
redis-cli -h 20.249.177.114 -p 6379 -a Hi5Jessica! ping
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AI Python Service 실행
|
||||
|
||||
### 1. 디렉토리 이동
|
||||
```bash
|
||||
cd /Users/jominseo/HGZero/ai-python
|
||||
```
|
||||
|
||||
### 2. 환경 변수 확인
|
||||
```bash
|
||||
# .env 파일 확인 (없으면 .env.example에서 복사)
|
||||
cat .env
|
||||
|
||||
# 필수 환경 변수:
|
||||
# - PORT=8087
|
||||
# - CLAUDE_API_KEY=sk-ant-api03-...
|
||||
# - REDIS_HOST=20.249.177.114
|
||||
# - REDIS_PORT=6379
|
||||
```
|
||||
|
||||
### 3. 의존성 설치
|
||||
```bash
|
||||
# Python 가상환경 활성화 (선택사항)
|
||||
source venv/bin/activate # 또는 python3 -m venv venv
|
||||
|
||||
# 의존성 설치
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 4. 서비스 실행
|
||||
```bash
|
||||
# 방법 1: 직접 실행
|
||||
python3 main.py
|
||||
|
||||
# 방법 2: uvicorn으로 실행
|
||||
uvicorn main:app --host 0.0.0.0 --port 8087 --reload
|
||||
|
||||
# 방법 3: 백그라운드 실행
|
||||
nohup python3 main.py > logs/ai-python.log 2>&1 & echo "Started AI Python Service with PID: $!"
|
||||
```
|
||||
|
||||
### 5. 상태 확인
|
||||
```bash
|
||||
# Health Check
|
||||
curl http://localhost:8087/health
|
||||
|
||||
# 기대 응답:
|
||||
# {"status":"healthy","service":"AI Service (Python)"}
|
||||
|
||||
# API 문서 확인
|
||||
open http://localhost:8087/docs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Meeting Service 실행
|
||||
|
||||
### 1. 디렉토리 이동
|
||||
```bash
|
||||
cd /Users/jominseo/HGZero
|
||||
```
|
||||
|
||||
### 2. 빌드
|
||||
```bash
|
||||
# Java 21 사용
|
||||
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-21.jdk/Contents/Home
|
||||
|
||||
# 빌드
|
||||
./gradlew :meeting:clean :meeting:build -x test
|
||||
```
|
||||
|
||||
### 3. 실행
|
||||
```bash
|
||||
# 방법 1: Gradle로 실행
|
||||
./gradlew :meeting:bootRun
|
||||
|
||||
# 방법 2: JAR 실행
|
||||
java -jar meeting/build/libs/meeting-0.0.1-SNAPSHOT.jar
|
||||
|
||||
# 방법 3: IntelliJ 실행 프로파일 사용
|
||||
python3 tools/run-intellij-service-profile.py meeting
|
||||
```
|
||||
|
||||
### 4. 상태 확인
|
||||
```bash
|
||||
# Health Check
|
||||
curl http://localhost:8082/actuator/health
|
||||
|
||||
# Swagger UI
|
||||
open http://localhost:8082/swagger-ui.html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 통합 테스트
|
||||
|
||||
### 테스트 시나리오
|
||||
|
||||
#### 1. 회의 생성 (사전 작업)
|
||||
```bash
|
||||
curl -X POST http://localhost:8082/api/meetings \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-User-Id: user123" \
|
||||
-H "X-User-Name: 홍길동" \
|
||||
-H "X-User-Email: hong@example.com" \
|
||||
-d '{
|
||||
"title": "AI 통합 테스트 회의",
|
||||
"purpose": "Meeting AI 기능 테스트",
|
||||
"scheduledAt": "2025-10-28T14:00:00",
|
||||
"endTime": "2025-10-28T15:00:00",
|
||||
"location": "회의실 A",
|
||||
"participantIds": ["user123", "user456"]
|
||||
}'
|
||||
```
|
||||
|
||||
응답에서 `meetingId` 저장
|
||||
|
||||
|
||||
#### 2. 회의 시작
|
||||
```bash
|
||||
MEETING_ID="위에서 받은 meetingId"
|
||||
|
||||
curl -X POST http://localhost:8082/api/meetings/${MEETING_ID}/start \
|
||||
-H "X-User-Id: user123" \
|
||||
-H "X-User-Name: 홍길동" \
|
||||
-H "X-User-Email: hong@example.com"
|
||||
```
|
||||
|
||||
|
||||
#### 3. 안건 섹션 생성 (테스트 데이터)
|
||||
```sql
|
||||
-- PostgreSQL에서 직접 실행
|
||||
INSERT INTO agenda_sections (
|
||||
id, minutes_id, meeting_id, agenda_number, agenda_title,
|
||||
ai_summary_short, discussions,
|
||||
decisions, pending_items, opinions, todos,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
'agenda-001', 'minutes-001', '위의_meetingId', 1, '신제품 기획',
|
||||
NULL,
|
||||
'타겟 고객층을 20-30대로 설정하고 UI/UX 개선에 집중하기로 논의했습니다.',
|
||||
'["타겟 고객: 20-30대 직장인", "UI/UX 개선 최우선"]'::json,
|
||||
'["가격 정책 추가 검토 필요"]'::json,
|
||||
'[]'::json,
|
||||
'[]'::json,
|
||||
NOW(), NOW()
|
||||
),
|
||||
(
|
||||
'agenda-002', 'minutes-001', '위의_meetingId', 2, '마케팅 전략',
|
||||
NULL,
|
||||
'SNS 마케팅과 인플루언서 협업을 통한 브랜드 인지도 제고 방안을 논의했습니다.',
|
||||
'["SNS 광고 집행", "인플루언서 3명과 계약"]'::json,
|
||||
'["예산 승인 대기"]'::json,
|
||||
'[]'::json,
|
||||
'[]'::json,
|
||||
NOW(), NOW()
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
#### 4. **핵심 테스트: 회의 종료 API 호출**
|
||||
```bash
|
||||
curl -X POST http://localhost:8082/api/meetings/${MEETING_ID}/end \
|
||||
-H "X-User-Id: user123" \
|
||||
-H "X-User-Name: 홍길동" \
|
||||
-H "X-User-Email: hong@example.com" \
|
||||
-v
|
||||
```
|
||||
|
||||
**기대 응답:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"title": "AI 통합 테스트 회의",
|
||||
"participantCount": 2,
|
||||
"durationMinutes": 60,
|
||||
"agendaCount": 2,
|
||||
"todoCount": 5,
|
||||
"keywords": ["신제품", "UI/UX", "마케팅", "SNS", "인플루언서"],
|
||||
"agendaSummaries": [
|
||||
{
|
||||
"title": "안건 1: 신제품 기획",
|
||||
"aiSummaryShort": "타겟 고객 설정 및 UI/UX 개선 방향 논의",
|
||||
"details": {
|
||||
"discussion": "타겟 고객층을 20-30대로 설정...",
|
||||
"decisions": ["타겟 고객: 20-30대 직장인", "UI/UX 개선 최우선"],
|
||||
"pending": ["가격 정책 추가 검토 필요"]
|
||||
},
|
||||
"todos": [
|
||||
{"title": "시장 조사 보고서 작성"},
|
||||
{"title": "UI/UX 개선안 프로토타입 제작"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "안건 2: 마케팅 전략",
|
||||
"aiSummaryShort": "SNS 마케팅 및 인플루언서 협업 계획",
|
||||
"details": {
|
||||
"discussion": "SNS 마케팅과 인플루언서 협업...",
|
||||
"decisions": ["SNS 광고 집행", "인플루언서 3명과 계약"],
|
||||
"pending": ["예산 승인 대기"]
|
||||
},
|
||||
"todos": [
|
||||
{"title": "인플루언서 계약서 작성"},
|
||||
{"title": "SNS 광고 컨텐츠 제작"},
|
||||
{"title": "예산안 제출"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### 5. 결과 확인
|
||||
|
||||
**데이터베이스 확인:**
|
||||
```sql
|
||||
-- 회의 상태 확인
|
||||
SELECT meeting_id, title, status, ended_at
|
||||
FROM meetings
|
||||
WHERE meeting_id = '위의_meetingId';
|
||||
-- 기대: status = 'COMPLETED'
|
||||
|
||||
-- AI 분석 결과 확인
|
||||
SELECT analysis_id, meeting_id, keywords, status, completed_at
|
||||
FROM meeting_analysis
|
||||
WHERE meeting_id = '위의_meetingId';
|
||||
|
||||
-- Todo 확인
|
||||
SELECT todo_id, title, status
|
||||
FROM todos
|
||||
WHERE meeting_id = '위의_meetingId';
|
||||
-- 기대: 5개의 Todo 생성
|
||||
```
|
||||
|
||||
**로그 확인:**
|
||||
```bash
|
||||
# AI Python Service 로그
|
||||
tail -f logs/ai-python.log
|
||||
|
||||
# Meeting Service 로그
|
||||
tail -f meeting/logs/meeting-service.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 트러블슈팅
|
||||
|
||||
### 1. AI Python Service 연결 실패
|
||||
```
|
||||
에러: Connection refused (8087)
|
||||
|
||||
해결:
|
||||
1. AI Python Service가 실행 중인지 확인
|
||||
ps aux | grep python | grep main.py
|
||||
2. 포트 확인
|
||||
lsof -i :8087
|
||||
3. 로그 확인
|
||||
tail -f logs/ai-python.log
|
||||
```
|
||||
|
||||
### 2. Claude API 오류
|
||||
```
|
||||
에러: Invalid API key
|
||||
|
||||
해결:
|
||||
1. .env 파일의 CLAUDE_API_KEY 확인
|
||||
2. API 키 유효성 확인
|
||||
curl https://api.anthropic.com/v1/messages \
|
||||
-H "x-api-key: $CLAUDE_API_KEY" \
|
||||
-H "anthropic-version: 2023-06-01"
|
||||
```
|
||||
|
||||
### 3. 데이터베이스 연결 실패
|
||||
```
|
||||
에러: Connection to 4.230.48.72:5432 refused
|
||||
|
||||
해결:
|
||||
1. PostgreSQL 서버 상태 확인
|
||||
2. 방화벽 규칙 확인
|
||||
3. application.yml의 DB 설정 확인
|
||||
```
|
||||
|
||||
### 4. 타임아웃 오류
|
||||
```
|
||||
에러: Read timeout (30초)
|
||||
|
||||
해결:
|
||||
1. application.yml에서 타임아웃 증가
|
||||
ai.service.timeout=60000
|
||||
2. Claude API 응답 시간 확인
|
||||
3. 네트워크 상태 확인
|
||||
```
|
||||
|
||||
### 5. 안건 데이터 없음
|
||||
```
|
||||
에러: No agenda sections found
|
||||
|
||||
해결:
|
||||
1. agenda_sections 테이블에 데이터 확인
|
||||
SELECT * FROM agenda_sections WHERE meeting_id = '해당ID';
|
||||
2. 테스트 데이터 삽입 (위 SQL 참조)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 성능 측정
|
||||
|
||||
### 응답 시간 측정
|
||||
```bash
|
||||
# 회의 종료 API 응답 시간
|
||||
time curl -X POST http://localhost:8082/api/meetings/${MEETING_ID}/end \
|
||||
-H "X-User-Id: user123" \
|
||||
-H "X-User-Name: 홍길동" \
|
||||
-H "X-User-Email: hong@example.com"
|
||||
|
||||
# 기대 시간: 5-15초 (Claude API 호출 포함)
|
||||
```
|
||||
|
||||
### 동시성 테스트
|
||||
```bash
|
||||
# Apache Bench로 부하 테스트 (선택사항)
|
||||
ab -n 10 -c 2 -H "X-User-Id: user123" \
|
||||
http://localhost:8087/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 체크리스트
|
||||
|
||||
- [ ] AI Python Service 실행 (8087)
|
||||
- [ ] Meeting Service 실행 (8082)
|
||||
- [ ] 데이터베이스 연결 확인
|
||||
- [ ] Redis 연결 확인
|
||||
- [ ] 회의 생성 API 성공
|
||||
- [ ] 회의 시작 API 성공
|
||||
- [ ] 안건 데이터 삽입
|
||||
- [ ] **회의 종료 API 성공**
|
||||
- [ ] AI 분석 결과 저장 확인
|
||||
- [ ] Todo 자동 생성 확인
|
||||
- [ ] 회의 상태 COMPLETED 확인
|
||||
|
||||
---
|
||||
|
||||
## 참고 링크
|
||||
|
||||
- AI Python Service: http://localhost:8087/docs
|
||||
- Meeting Service Swagger: http://localhost:8082/swagger-ui.html
|
||||
- Claude API 문서: https://docs.anthropic.com/claude/reference
|
||||
322
claude/README-SCHEMA-ANALYSIS.md
Normal file
322
claude/README-SCHEMA-ANALYSIS.md
Normal file
@ -0,0 +1,322 @@
|
||||
# Meeting Service 데이터베이스 스키마 분석 문서
|
||||
|
||||
## 생성된 문서 목록
|
||||
|
||||
본 분석은 Meeting Service의 데이터베이스 스키마를 전방위적으로 분석한 결과입니다.
|
||||
|
||||
### 1. SCHEMA-REPORT-SUMMARY.md (메인 보고서)
|
||||
**파일**: `/Users/jominseo/HGZero/claude/SCHEMA-REPORT-SUMMARY.md`
|
||||
**내용**:
|
||||
- Executive Summary (핵심 발견사항)
|
||||
- 데이터베이스 구조 개요
|
||||
- 테이블별 상세 분석 (1.1~1.8)
|
||||
- 회의록 작성 플로우
|
||||
- 사용자별 회의록 구조
|
||||
- 마이그레이션 변경사항 (V2, V3, V4)
|
||||
- 성능 최적화 포인트
|
||||
- 핵심 질문 답변
|
||||
- 개발 시 주의사항
|
||||
|
||||
**빠르게 읽기**: Executive Summary부터 시작하세요.
|
||||
|
||||
---
|
||||
|
||||
### 2. database-schema-analysis.md (상세 분석)
|
||||
**파일**: `/Users/jominseo/HGZero/claude/database-schema-analysis.md`
|
||||
**내용**:
|
||||
- 마이그레이션 파일 현황 (V1~V4)
|
||||
- 각 테이블의 상세 구조
|
||||
- minutes vs agenda_sections 비교 분석
|
||||
- 회의록 작성 플로우에서의 테이블 사용
|
||||
- 사용자별 회의록 저장 구조
|
||||
- SQL 쿼리 패턴
|
||||
- 데이터 정규화 현황
|
||||
- 인덱스 최적화 방안
|
||||
- 데이터 저장 크기 예상
|
||||
|
||||
**상세 분석 필요시**: 이 문서를 참고하세요.
|
||||
|
||||
---
|
||||
|
||||
### 3. data-flow-diagram.md (흐름도)
|
||||
**파일**: `/Users/jominseo/HGZero/claude/data-flow-diagram.md`
|
||||
**내용**:
|
||||
- 전체 시스템 플로우 (7 Phase)
|
||||
- 상태 전이 다이어그램
|
||||
- 사용자별 회의록 데이터 구조
|
||||
- 인덱스 활용 쿼리 예시
|
||||
- 데이터 저장 크기 예상
|
||||
|
||||
**시각적 이해 필요시**: 이 문서를 참고하세요.
|
||||
|
||||
---
|
||||
|
||||
### 4. database-diagram.puml (ER 다이어그램)
|
||||
**파일**: `/Users/jominseo/HGZero/claude/database-diagram.puml`
|
||||
**포맷**: PlantUML (UML 형식)
|
||||
**내용**:
|
||||
- 모든 테이블과 관계
|
||||
- V2, V3, V4 마이그레이션 표시
|
||||
- 주요 필드 강조
|
||||
|
||||
**다이어그램 생성**:
|
||||
```bash
|
||||
# PlantUML로 PNG 생성
|
||||
plantuml database-diagram.puml -o database-diagram.png
|
||||
|
||||
# 또는 온라인 에디터
|
||||
https://www.plantuml.com/plantuml/uml/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 핵심 발견사항 한눈에 보기
|
||||
|
||||
### 1. Minutes 테이블 구조
|
||||
```
|
||||
잘못된 이해: minutes.content ← 회의록 내용
|
||||
올바른 구조: minutes_sections.content ← 회의록 내용
|
||||
minutes ← 메타데이터만 (title, status, version)
|
||||
```
|
||||
|
||||
### 2. 사용자별 회의록 (V3)
|
||||
```
|
||||
minutes.user_id = NULL → AI 통합 회의록
|
||||
minutes.user_id = 'user@.com' → 개인 회의록
|
||||
인덱스: idx_minutes_meeting_user(meeting_id, user_id)
|
||||
```
|
||||
|
||||
### 3. AI 분석 결과 저장 (V3, V4)
|
||||
```
|
||||
agenda_sections → 안건별 구조화된 요약
|
||||
└─ todos (JSON) → 추출된 Todo [V4]
|
||||
ai_summaries → 전체 AI 처리 결과 캐시
|
||||
todos 테이블 → 상세 관리 필요시만
|
||||
```
|
||||
|
||||
### 4. 정규화 (V2)
|
||||
```
|
||||
이전: meetings.participants = "user1,user2,user3"
|
||||
현재: meeting_participants (테이블, 복합PK)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 빠른 참조표
|
||||
|
||||
### 회의록 작성 플로우
|
||||
| 단계 | API | 데이터베이스 변화 |
|
||||
|------|-----|-----------------|
|
||||
| 1 | CreateMeeting | meetings INSERT |
|
||||
| 2 | StartMeeting | meetings.status = IN_PROGRESS |
|
||||
| 3 | CreateMinutes | minutes INSERT (통합 + 개인) |
|
||||
| 4 | UpdateMinutes | minutes_sections.content UPDATE |
|
||||
| 5 | EndMeeting | meetings.status = COMPLETED, ended_at [V3] |
|
||||
| 6 | FinalizeMinutes | minutes.status = FINALIZED, sections locked |
|
||||
| 7 | AI 분석 | agenda_sections, ai_summaries, todos INSERT |
|
||||
|
||||
### 테이블별 핵심 필드
|
||||
```
|
||||
meetings : meeting_id, status, ended_at [V3]
|
||||
minutes : id, meeting_id, user_id [V3], status
|
||||
minutes_sections : id, minutes_id, content ★
|
||||
agenda_sections : id, minutes_id, agenda_number, todos [V4]
|
||||
ai_summaries : id, meeting_id, result (JSON)
|
||||
todos : todo_id, extracted_by [V3], extraction_confidence [V3]
|
||||
```
|
||||
|
||||
### 인덱스
|
||||
```
|
||||
PRIMARY:
|
||||
idx_minutes_meeting_user (meeting_id, user_id) [V3]
|
||||
idx_sections_meeting (meeting_id) [V3]
|
||||
idx_sections_agenda (meeting_id, agenda_number) [V3]
|
||||
|
||||
SECONDARY:
|
||||
idx_todos_extracted (extracted_by) [V3]
|
||||
idx_todos_meeting (meeting_id) [V3]
|
||||
idx_summaries_type (meeting_id, summary_type) [V3]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 마이그레이션 타임라인
|
||||
|
||||
```
|
||||
V1 (초기)
|
||||
├─ meetings, minutes, minutes_sections
|
||||
├─ todos, meeting_analysis
|
||||
└─ JPA Hibernate로 자동 생성
|
||||
|
||||
V2 (2025-10-27)
|
||||
├─ meeting_participants 테이블 생성
|
||||
├─ meetings.participants (CSV) 마이그레이션
|
||||
└─ 정규화 완료
|
||||
|
||||
V3 (2025-10-28) ★ 주요 변경
|
||||
├─ minutes.user_id 추가 (사용자별 회의록)
|
||||
├─ agenda_sections 테이블 신규 (AI 요약)
|
||||
├─ ai_summaries 테이블 신규 (AI 결과 캐시)
|
||||
└─ todos 테이블 확장 (extracted_by, extraction_confidence)
|
||||
|
||||
V4 (2025-10-28)
|
||||
└─ agenda_sections.todos JSON 필드 추가
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 자주 묻는 질문
|
||||
|
||||
### Q: minutes 테이블에 content 필드가 있나요?
|
||||
**A**: 없습니다. 실제 회의록 내용은 `minutes_sections.content`에 저장됩니다.
|
||||
`minutes` 테이블은 메타데이터만 보유합니다 (title, status, version 등).
|
||||
|
||||
### Q: 사용자별 회의록은 어떻게 구분되나요?
|
||||
**A**: `minutes.user_id` 컬럼으로 구분됩니다.
|
||||
- NULL: AI 통합 회의록
|
||||
- NOT NULL: 개인별 회의록 (각 참석자마다 생성)
|
||||
|
||||
### Q: AI 분석은 모든 회의록을 처리하나요?
|
||||
**A**: 아니요. 통합 회의록(`user_id=NULL`)만 분석합니다.
|
||||
개인별 회의록(`user_id NOT NULL`)은 개인 기록용이며 AI 분석 대상이 아닙니다.
|
||||
|
||||
### Q: agenda_sections와 minutes_sections의 차이는?
|
||||
**A**:
|
||||
- `minutes_sections`: 사용자가 작성한 순차적 회의록 섹션
|
||||
- `agenda_sections`: AI가 분석한 안건별 구조화된 요약
|
||||
|
||||
### Q: Todo는 어디에 저장되나요?
|
||||
**A**: 두 곳에 저장 가능합니다.
|
||||
1. `agenda_sections.todos` (JSON): 안건별 요약의 일부
|
||||
2. `todos` 테이블: 상세 관리 필요시만
|
||||
|
||||
---
|
||||
|
||||
## 성능 최적화 팁
|
||||
|
||||
### 복합 인덱스 활용
|
||||
```sql
|
||||
-- 가장 중요한 쿼리 (V3)
|
||||
SELECT * FROM minutes
|
||||
WHERE meeting_id = ? AND user_id = ?;
|
||||
└─ 인덱스: idx_minutes_meeting_user (meeting_id, user_id)
|
||||
```
|
||||
|
||||
### 추천 추가 인덱스
|
||||
```sql
|
||||
CREATE INDEX idx_minutes_status_created
|
||||
ON minutes(status, created_at DESC);
|
||||
|
||||
CREATE INDEX idx_agenda_meeting_created
|
||||
ON agenda_sections(meeting_id, created_at DESC);
|
||||
```
|
||||
|
||||
### 쿼리 패턴
|
||||
```sql
|
||||
-- 통합 회의록 조회 (가장 흔함)
|
||||
SELECT m.*, ms.* FROM minutes m
|
||||
LEFT JOIN minutes_sections ms ON m.id = ms.minutes_id
|
||||
WHERE m.meeting_id = ? AND m.user_id IS NULL
|
||||
|
||||
-- 개인 회의록 조회
|
||||
SELECT m.*, ms.* FROM minutes m
|
||||
LEFT JOIN minutes_sections ms ON m.id = ms.minutes_id
|
||||
WHERE m.meeting_id = ? AND m.user_id = ?
|
||||
|
||||
-- AI 분석 결과 조회
|
||||
SELECT * FROM agenda_sections
|
||||
WHERE meeting_id = ? ORDER BY agenda_number
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 문서 읽기 순서 추천
|
||||
|
||||
### 1단계: 빠른 이해 (5분)
|
||||
→ `SCHEMA-REPORT-SUMMARY.md`의 Executive Summary만 읽기
|
||||
|
||||
### 2단계: 구조 이해 (15분)
|
||||
→ `database-diagram.puml` (다이어그램 확인)
|
||||
→ `data-flow-diagram.md`의 Phase 1~7 읽기
|
||||
|
||||
### 3단계: 상세 이해 (30분)
|
||||
→ `SCHEMA-REPORT-SUMMARY.md` 전체 읽기
|
||||
→ `database-schema-analysis.md`의 핵심 섹션 읽기
|
||||
|
||||
### 4단계: 개발 참고 (필요시)
|
||||
→ `database-schema-analysis.md`의 쿼리 예시
|
||||
→ `data-flow-diagram.md`의 인덱스 활용 섹션
|
||||
|
||||
---
|
||||
|
||||
## 개발 체크리스트
|
||||
|
||||
회의록 작성 기능 개발시:
|
||||
|
||||
### 데이터 저장
|
||||
- [ ] 회의록 내용은 `minutes_sections.content`에 저장
|
||||
- [ ] `minutes` 테이블에는 메타데이터만 저장 (title, status)
|
||||
- [ ] 회의 종료시 `minutes.user_id` 값 확인 (NULL vs 사용자ID)
|
||||
|
||||
### AI 분석
|
||||
- [ ] 통합 회의록(`user_id=NULL`)만 AI 분석 대상으로 처리
|
||||
- [ ] `agenda_sections`은 통합 회의록에만 생성
|
||||
- [ ] `ai_summaries`에 전체 결과 캐싱
|
||||
|
||||
### 쿼리 성능
|
||||
- [ ] 복합 인덱스 활용: `idx_minutes_meeting_user`
|
||||
- [ ] 조회시 `WHERE meeting_id AND user_id` 조건 사용
|
||||
- [ ] 기존 인덱스 모두 생성 확인
|
||||
|
||||
### 데이터 무결성
|
||||
- [ ] 회의 종료시 `ended_at` 기록 (V3)
|
||||
- [ ] 최종화시 `minutes_sections` locked 처리
|
||||
- [ ] AI 추출 Todo의 `extraction_confidence` 값 확인
|
||||
|
||||
---
|
||||
|
||||
## 관련 파일 위치
|
||||
|
||||
**마이그레이션**:
|
||||
```
|
||||
/Users/jominseo/HGZero/meeting/src/main/resources/db/migration/
|
||||
├─ V2__create_meeting_participants_table.sql
|
||||
├─ V3__add_meeting_end_support.sql
|
||||
└─ V4__add_todos_to_agenda_sections.sql
|
||||
```
|
||||
|
||||
**엔티티**:
|
||||
```
|
||||
/Users/jominseo/HGZero/meeting/src/main/java/.../entity/
|
||||
├─ MeetingEntity.java
|
||||
├─ MinutesEntity.java
|
||||
├─ MinutesSectionEntity.java
|
||||
├─ AgendaSectionEntity.java [V3]
|
||||
├─ TodoEntity.java
|
||||
└─ MeetingParticipantEntity.java [V2]
|
||||
```
|
||||
|
||||
**서비스**:
|
||||
```
|
||||
/Users/jominseo/HGZero/meeting/src/main/java/.../service/
|
||||
├─ MinutesService.java
|
||||
├─ MinutesSectionService.java
|
||||
└─ MinutesAnalysisEventConsumer.java (비동기 AI 분석)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 지원
|
||||
|
||||
이 문서에 대한 추가 질문이나 불명확한 부분이 있으면:
|
||||
|
||||
1. `SCHEMA-REPORT-SUMMARY.md`의 "핵심 질문 답변" 섹션 확인
|
||||
2. `database-schema-analysis.md`에서 상세 내용 검색
|
||||
3. `data-flow-diagram.md`에서 흐름도 재확인
|
||||
|
||||
---
|
||||
|
||||
**문서 작성일**: 2025-10-28
|
||||
**분석 대상**: Meeting Service (feat/meeting-ai 브랜치)
|
||||
**마이그레이션 버전**: V1~V4
|
||||
**상태**: 완료 및 검증됨
|
||||
607
claude/SCHEMA-REPORT-SUMMARY.md
Normal file
607
claude/SCHEMA-REPORT-SUMMARY.md
Normal file
@ -0,0 +1,607 @@
|
||||
# Meeting Service 데이터베이스 스키마 분석 최종 보고서
|
||||
|
||||
**작성일**: 2025-10-28
|
||||
**분석 대상**: Meeting Service (feat/meeting-ai 브랜치)
|
||||
**분석 범위**: 마이그레이션 V1~V4, 엔티티 구조, 데이터 플로우
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
### 핵심 발견사항
|
||||
|
||||
1. **minutes 테이블에 content 필드가 없음**
|
||||
- 실제 회의록 내용은 `minutes_sections.content`에 저장
|
||||
- minutes 테이블은 메타데이터만 보유 (title, status, version 등)
|
||||
|
||||
2. **사용자별 회의록 완벽하게 지원 (V3)**
|
||||
- `minutes.user_id = NULL`: AI 통합 회의록
|
||||
- `minutes.user_id = 참석자ID`: 개인별 회의록
|
||||
- 인덱스: `idx_minutes_meeting_user` (meeting_id, user_id)
|
||||
|
||||
3. **AI 분석 결과 구조화 저장 (V3, V4)**
|
||||
- `agenda_sections`: 안건별 구조화된 요약
|
||||
- `ai_summaries`: AI 처리 결과 캐싱
|
||||
- `todos` (V4): 각 안건의 JSON으로 저장
|
||||
|
||||
4. **정규화 완료 (V2)**
|
||||
- `meetings.participants` (CSV) → `meeting_participants` (테이블)
|
||||
- 복합 PK: (meeting_id, user_id)
|
||||
|
||||
---
|
||||
|
||||
## 데이터베이스 구조 개요
|
||||
|
||||
### 테이블 분류
|
||||
|
||||
**핵심 테이블** (V1):
|
||||
- `meetings`: 회의 기본 정보
|
||||
- `minutes`: 회의록 메타데이터
|
||||
- `minutes_sections`: 회의록 섹션 (실제 내용)
|
||||
|
||||
**참석자 관리** (V2):
|
||||
- `meeting_participants`: 회의 참석자 정보
|
||||
|
||||
**AI 분석** (V3):
|
||||
- `agenda_sections`: 안건별 AI 요약
|
||||
- `ai_summaries`: AI 처리 결과 캐시
|
||||
- `todos`: Todo 아이템 (expanded)
|
||||
|
||||
---
|
||||
|
||||
## 1. 핵심 테이블별 상세 분석
|
||||
|
||||
### 1.1 meetings (회의 기본 정보)
|
||||
|
||||
**구성**:
|
||||
- PK: meeting_id (VARCHAR(50))
|
||||
- 주요 필드: title, purpose, description
|
||||
- 상태: SCHEDULED → IN_PROGRESS → COMPLETED
|
||||
- 시간: scheduled_at, started_at, ended_at (V3)
|
||||
|
||||
**중요 변경**:
|
||||
- V3에서 `ended_at` 추가
|
||||
- 회의 정확한 종료 시간 기록
|
||||
|
||||
```sql
|
||||
-- 조회 예시
|
||||
SELECT * FROM meetings
|
||||
WHERE status = 'COMPLETED'
|
||||
AND ended_at >= NOW() - INTERVAL '7 days'
|
||||
ORDER BY ended_at DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.2 minutes (회의록 메타데이터)
|
||||
|
||||
**구성**:
|
||||
```
|
||||
minutes_id (PK)
|
||||
├─ meeting_id (FK)
|
||||
├─ user_id (V3) ← NULL: AI 통합 회의록 / NOT NULL: 개인 회의록
|
||||
├─ title
|
||||
├─ status (DRAFT, FINALIZED)
|
||||
├─ version
|
||||
├─ created_by, finalized_by
|
||||
└─ created_at, finalized_at
|
||||
```
|
||||
|
||||
**중요**:
|
||||
- **content 필드 없음** → minutes_sections에 저장
|
||||
- 메타데이터만 관리 (생성자, 확정자, 버전 등)
|
||||
|
||||
**쿼리 패턴**:
|
||||
```sql
|
||||
-- AI 통합 회의록
|
||||
SELECT * FROM minutes
|
||||
WHERE meeting_id = ? AND user_id IS NULL;
|
||||
|
||||
-- 특정 사용자의 회의록
|
||||
SELECT * FROM minutes
|
||||
WHERE meeting_id = ? AND user_id = ?;
|
||||
|
||||
-- 복합 인덱스 활용: idx_minutes_meeting_user
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.3 minutes_sections (회의록 섹션 - 실제 내용)
|
||||
|
||||
**구성**:
|
||||
```
|
||||
section_id (PK)
|
||||
├─ minutes_id (FK) ← 어느 회의록에 속하는가
|
||||
├─ type (AGENDA, DISCUSSION, DECISION, ACTION_ITEM)
|
||||
├─ title
|
||||
├─ content ← ★ 실제 회의록 내용
|
||||
├─ order
|
||||
├─ verified (검증 완료)
|
||||
├─ locked (수정 불가)
|
||||
└─ locked_by
|
||||
```
|
||||
|
||||
**핵심 특성**:
|
||||
- **content**: 사용자가 작성한 실제 내용
|
||||
- **locked**: finalize_minutes 호출시 잠금
|
||||
- **verified**: 확정시 TRUE로 설정
|
||||
|
||||
**데이터 흐름**:
|
||||
```
|
||||
1. CreateMinutes → minutes_sections 초기 생성
|
||||
2. UpdateMinutes → content 저장 (여러 번)
|
||||
3. FinalizeMinutes → locked=TRUE, verified=TRUE
|
||||
4. (locked 상태에서 수정 불가)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.4 agenda_sections (AI 요약 - V3)
|
||||
|
||||
**구성**:
|
||||
```
|
||||
id (PK, UUID)
|
||||
├─ minutes_id (FK) ← 통합 회의록만 (user_id=NULL)
|
||||
├─ meeting_id (FK)
|
||||
├─ agenda_number (1, 2, 3...)
|
||||
├─ agenda_title
|
||||
├─ ai_summary_short (1줄 요약)
|
||||
├─ discussions (3-5문장 논의)
|
||||
├─ decisions (JSON 배열)
|
||||
├─ pending_items (JSON 배열)
|
||||
├─ opinions (JSON 배열: {speaker, opinion})
|
||||
└─ todos (JSON 배열 [V4])
|
||||
```
|
||||
|
||||
**V4 추가 사항**:
|
||||
- `todos` JSON 필드 추가
|
||||
- 안건별 추출된 Todo 저장
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "시장 조사 보고서 작성",
|
||||
"assignee": "김민준",
|
||||
"dueDate": "2025-02-15",
|
||||
"description": "20-30대 타겟 시장 조사",
|
||||
"priority": "HIGH"
|
||||
}
|
||||
```
|
||||
|
||||
**중요**:
|
||||
- **통합 회의록만 분석** (user_id=NULL인 것)
|
||||
- 참석자별 회의록(user_id NOT NULL)은 AI 분석 대상 아님
|
||||
- minutes_id로 통합 회의록 참조
|
||||
|
||||
---
|
||||
|
||||
### 1.5 minutes_sections vs agenda_sections
|
||||
|
||||
| 항목 | minutes_sections | agenda_sections |
|
||||
|------|-----------------|-----------------|
|
||||
| **용도** | 사용자 작성 | AI 요약 |
|
||||
| **모든 회의록** | ✓ 통합 + 개인 | ✗ 통합만 |
|
||||
| **구조** | 순차적 섹션 | 안건별 구조화 |
|
||||
| **내용 저장** | content (TEXT) | JSON 필드들 |
|
||||
| **관계** | 1:N (minutes과) | N:1 (minutes과) |
|
||||
| **목적** | 기록 | 분석/요약 |
|
||||
|
||||
**생성 흐름**:
|
||||
```
|
||||
회의 시작
|
||||
↓
|
||||
minutes 생성 (통합 + 개인)
|
||||
↓
|
||||
minutes_sections 생성 (4개 그룹)
|
||||
↓
|
||||
사용자 작성 중...
|
||||
↓
|
||||
회의 종료 → FinalizeMinutes
|
||||
↓
|
||||
minutes_sections locked
|
||||
↓
|
||||
AI 분석 (비동기)
|
||||
↓
|
||||
agenda_sections 생성 (통합 회의록 기반)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.6 ai_summaries (AI 처리 결과 - V3)
|
||||
|
||||
**구성**:
|
||||
```
|
||||
id (PK, UUID)
|
||||
├─ meeting_id (FK)
|
||||
├─ summary_type (CONSOLIDATED, TODO_EXTRACTION)
|
||||
├─ source_minutes_ids (JSON: 사용된 회의록 ID 배열)
|
||||
├─ result (JSON: AI 응답 전체)
|
||||
├─ processing_time_ms (처리 시간)
|
||||
├─ model_version (claude-3.5-sonnet)
|
||||
├─ keywords (JSON: 키워드 배열)
|
||||
└─ statistics (JSON: {participants, agendas, todos})
|
||||
```
|
||||
|
||||
**용도**:
|
||||
- AI 처리 결과 캐싱
|
||||
- 재처리 필요시 참조
|
||||
- 성능 통계 기록
|
||||
|
||||
---
|
||||
|
||||
### 1.7 todos (Todo 아이템)
|
||||
|
||||
**기본 구조**:
|
||||
```
|
||||
todo_id (PK)
|
||||
├─ meeting_id (FK)
|
||||
├─ minutes_id (FK)
|
||||
├─ title
|
||||
├─ description
|
||||
├─ assignee_id
|
||||
├─ due_date
|
||||
├─ status (PENDING, COMPLETED)
|
||||
├─ priority (HIGH, MEDIUM, LOW)
|
||||
└─ completed_at
|
||||
```
|
||||
|
||||
**V3 추가 필드**:
|
||||
```
|
||||
├─ extracted_by (AI / MANUAL) ← AI 자동 추출 vs 수동
|
||||
├─ section_reference (안건 참조)
|
||||
└─ extraction_confidence (0.00~1.00) ← AI 신뢰도
|
||||
```
|
||||
|
||||
**저장 전략**:
|
||||
1. `agenda_sections.todos` (JSON): 간단한 Todo, 기본 저장 위치
|
||||
2. `todos` 테이블: 상세 관리 필요시만 추가 저장
|
||||
|
||||
---
|
||||
|
||||
### 1.8 meeting_participants (참석자 관리 - V2)
|
||||
|
||||
**구성**:
|
||||
```
|
||||
PK: (meeting_id, user_id)
|
||||
├─ invitation_status (PENDING, ACCEPTED, DECLINED)
|
||||
├─ attended (BOOLEAN)
|
||||
└─ created_at, updated_at
|
||||
```
|
||||
|
||||
**V2 개선**:
|
||||
- 이전: meetings.participants (CSV 문자열)
|
||||
- 현재: 별도 테이블 (정규화)
|
||||
- 복합 PK로 중복 방지
|
||||
|
||||
---
|
||||
|
||||
## 2. 회의록 작성 플로우 (전체)
|
||||
|
||||
### 단계별 데이터 변화
|
||||
|
||||
```
|
||||
PHASE 1: 회의 준비
|
||||
═════════════════════════════════════════════
|
||||
1. CreateMeeting
|
||||
→ INSERT meetings (status='SCHEDULED')
|
||||
→ INSERT meeting_participants (5명)
|
||||
|
||||
PHASE 2: 회의 진행
|
||||
═════════════════════════════════════════════
|
||||
2. StartMeeting
|
||||
→ UPDATE meetings SET status='IN_PROGRESS'
|
||||
|
||||
3. CreateMinutes (회의 중)
|
||||
→ INSERT minutes (user_id=NULL) × 1 (통합)
|
||||
→ INSERT minutes (user_id=user_id) × 5 (개인)
|
||||
→ INSERT minutes_sections (초기 생성)
|
||||
|
||||
4. UpdateMinutes (여러 번)
|
||||
→ UPDATE minutes_sections SET content='...'
|
||||
|
||||
PHASE 3: 회의 종료
|
||||
═════════════════════════════════════════════
|
||||
5. EndMeeting
|
||||
→ UPDATE meetings SET
|
||||
status='COMPLETED',
|
||||
ended_at=NOW() [V3]
|
||||
|
||||
PHASE 4: 회의록 최종화
|
||||
═════════════════════════════════════════════
|
||||
6. FinalizeMinutes
|
||||
→ UPDATE minutes SET
|
||||
status='FINALIZED'
|
||||
→ UPDATE minutes_sections SET
|
||||
locked=TRUE,
|
||||
verified=TRUE
|
||||
|
||||
PHASE 5: AI 분석 (비동기)
|
||||
═════════════════════════════════════════════
|
||||
7. MinutesAnalysisEventConsumer
|
||||
→ Read minutes (user_id=NULL)
|
||||
→ Read minutes_sections
|
||||
→ Call AI Service
|
||||
→ INSERT agenda_sections [V3]
|
||||
→ INSERT ai_summaries [V3]
|
||||
→ INSERT todos [V3 확장]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 사용자별 회의록 구조
|
||||
|
||||
### 데이터 분리 방식
|
||||
|
||||
**1개 회의 (참석자 5명)**:
|
||||
|
||||
```
|
||||
meetings: 1개
|
||||
├─ meeting_id = 'meeting-001'
|
||||
└─ status = COMPLETED
|
||||
|
||||
meeting_participants: 5개
|
||||
├─ (meeting-001, user1@example.com)
|
||||
├─ (meeting-001, user2@example.com)
|
||||
├─ (meeting-001, user3@example.com)
|
||||
├─ (meeting-001, user4@example.com)
|
||||
└─ (meeting-001, user5@example.com)
|
||||
|
||||
minutes: 6개 [V3]
|
||||
├─ (id=consol-1, meeting_id=meeting-001, user_id=NULL)
|
||||
│ → 통합 회의록 (AI 분석 대상)
|
||||
├─ (id=user1-min, meeting_id=meeting-001, user_id=user1@example.com)
|
||||
│ → 사용자1 개인 회의록
|
||||
├─ (id=user2-min, meeting_id=meeting-001, user_id=user2@example.com)
|
||||
│ → 사용자2 개인 회의록
|
||||
├─ ... (user3, user4, user5)
|
||||
└─
|
||||
|
||||
minutes_sections: 수십 개 (6개 회의록 × N개 섹션)
|
||||
├─ Group 1: consol-1의 섹션들 (AI 작성)
|
||||
├─ Group 2: user1-min의 섹션들 (사용자1 작성)
|
||||
├─ Group 3: user2-min의 섹션들 (사용자2 작성)
|
||||
└─ ... (user3, user4, user5)
|
||||
|
||||
agenda_sections: 5개 [V3]
|
||||
├─ (id=ag-1, minutes_id=consol-1, agenda_number=1)
|
||||
├─ (id=ag-2, minutes_id=consol-1, agenda_number=2)
|
||||
└─ ... (3, 4, 5)
|
||||
```
|
||||
|
||||
**핵심**:
|
||||
- 참석자별 회의록은 minutes.user_id로 구분
|
||||
- 인덱스 활용: `idx_minutes_meeting_user`
|
||||
- AI 분석: 통합 회의록만 (user_id=NULL)
|
||||
|
||||
---
|
||||
|
||||
## 4. 마이그레이션 변경사항 요약
|
||||
|
||||
### V2 (2025-10-27)
|
||||
|
||||
```sql
|
||||
-- meeting_participants 테이블 생성
|
||||
CREATE TABLE meeting_participants (
|
||||
meeting_id, user_id (복합 PK),
|
||||
invitation_status, attended
|
||||
)
|
||||
|
||||
-- 데이터 마이그레이션
|
||||
SELECT TRIM(participant) FROM meetings.participants (CSV)
|
||||
→ INSERT INTO meeting_participants
|
||||
|
||||
-- meetings.participants 컬럼 삭제
|
||||
ALTER TABLE meetings DROP COLUMN participants
|
||||
```
|
||||
|
||||
**영향**: 정규화 완료, 중복 데이터 제거
|
||||
|
||||
---
|
||||
|
||||
### V3 (2025-10-28)
|
||||
|
||||
#### 3-1. minutes 테이블 확장
|
||||
```sql
|
||||
ALTER TABLE minutes ADD COLUMN user_id VARCHAR(100);
|
||||
CREATE INDEX idx_minutes_meeting_user ON minutes(meeting_id, user_id);
|
||||
```
|
||||
|
||||
**의미**: 사용자별 회의록 지원
|
||||
|
||||
---
|
||||
|
||||
#### 3-2. agenda_sections 테이블 신규
|
||||
```sql
|
||||
CREATE TABLE agenda_sections (
|
||||
id, minutes_id, meeting_id,
|
||||
agenda_number, agenda_title,
|
||||
ai_summary_short, discussions,
|
||||
decisions (JSON),
|
||||
pending_items (JSON),
|
||||
opinions (JSON)
|
||||
)
|
||||
```
|
||||
|
||||
**의미**: AI 요약을 구조화된 형식으로 저장
|
||||
|
||||
---
|
||||
|
||||
#### 3-3. ai_summaries 테이블 신규
|
||||
```sql
|
||||
CREATE TABLE ai_summaries (
|
||||
id, meeting_id, summary_type,
|
||||
source_minutes_ids (JSON),
|
||||
result (JSON),
|
||||
processing_time_ms,
|
||||
model_version,
|
||||
keywords (JSON),
|
||||
statistics (JSON)
|
||||
)
|
||||
```
|
||||
|
||||
**의미**: AI 처리 결과 캐싱
|
||||
|
||||
---
|
||||
|
||||
#### 3-4. todos 테이블 확장
|
||||
```sql
|
||||
ALTER TABLE todos ADD COLUMN extracted_by VARCHAR(50) DEFAULT 'AI';
|
||||
ALTER TABLE todos ADD COLUMN section_reference VARCHAR(200);
|
||||
ALTER TABLE todos ADD COLUMN extraction_confidence DECIMAL(3,2);
|
||||
```
|
||||
|
||||
**의미**: AI 자동 추출 추적
|
||||
|
||||
---
|
||||
|
||||
### V4 (2025-10-28)
|
||||
|
||||
```sql
|
||||
ALTER TABLE agenda_sections ADD COLUMN todos JSON;
|
||||
```
|
||||
|
||||
**의미**: 안건별 Todo를 JSON으로 저장
|
||||
|
||||
---
|
||||
|
||||
## 5. 성능 최적화
|
||||
|
||||
### 현재 인덱스
|
||||
|
||||
```
|
||||
meetings:
|
||||
├─ PK: meeting_id
|
||||
|
||||
minutes:
|
||||
├─ PK: id
|
||||
└─ idx_minutes_meeting_user (meeting_id, user_id) [V3]
|
||||
|
||||
minutes_sections:
|
||||
├─ PK: id
|
||||
└─ (minutes_id로 FK 지원)
|
||||
|
||||
agenda_sections: [V3]
|
||||
├─ PK: id
|
||||
├─ idx_sections_meeting (meeting_id)
|
||||
├─ idx_sections_agenda (meeting_id, agenda_number)
|
||||
└─ idx_sections_minutes (minutes_id)
|
||||
|
||||
ai_summaries: [V3]
|
||||
├─ PK: id
|
||||
├─ idx_summaries_meeting (meeting_id)
|
||||
├─ idx_summaries_type (meeting_id, summary_type)
|
||||
└─ idx_summaries_created (created_at)
|
||||
|
||||
todos:
|
||||
├─ PK: todo_id
|
||||
├─ idx_todos_extracted (extracted_by) [V3]
|
||||
└─ idx_todos_meeting (meeting_id) [V3]
|
||||
|
||||
meeting_participants: [V2]
|
||||
├─ PK: (meeting_id, user_id)
|
||||
├─ idx_user_id (user_id)
|
||||
└─ idx_invitation_status (invitation_status)
|
||||
```
|
||||
|
||||
### 추천 추가 인덱스
|
||||
|
||||
```sql
|
||||
-- 자주 조회하는 패턴
|
||||
CREATE INDEX idx_minutes_status_created
|
||||
ON minutes(status, created_at DESC);
|
||||
|
||||
CREATE INDEX idx_agenda_meeting_created
|
||||
ON agenda_sections(meeting_id, created_at DESC);
|
||||
|
||||
CREATE INDEX idx_todos_meeting_assignee
|
||||
ON todos(meeting_id, assignee_id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 핵심 질문 답변
|
||||
|
||||
### Q1: minutes 테이블에 content 필드가 있는가?
|
||||
**A**: **없음**
|
||||
- minutes: 메타데이터만 (title, status, version 등)
|
||||
- 실제 내용: minutes_sections.content
|
||||
|
||||
### Q2: minutes_section과 agenda_sections의 차이점?
|
||||
**A**:
|
||||
| 항목 | minutes_sections | agenda_sections |
|
||||
|------|-----------------|-----------------|
|
||||
| 목적 | 사용자 작성 | AI 요약 |
|
||||
| 모든 회의록 | O | X (통합만) |
|
||||
| 내용 저장 | content (TEXT) | JSON |
|
||||
|
||||
### Q3: 사용자별 회의록 저장 방식?
|
||||
**A**:
|
||||
- minutes.user_id로 구분
|
||||
- NULL: AI 통합회의록
|
||||
- NOT NULL: 개인별 회의록
|
||||
- 인덱스: idx_minutes_meeting_user
|
||||
|
||||
### Q4: V3, V4 주요 변경?
|
||||
**A**:
|
||||
- V3: user_id, agenda_sections, ai_summaries, todos 확장
|
||||
- V4: agenda_sections.todos JSON 추가
|
||||
|
||||
---
|
||||
|
||||
## 7. 개발 시 주의사항
|
||||
|
||||
### Do's ✓
|
||||
- minutes_sections.content에 실제 내용 저장
|
||||
- AI 분석시 user_id=NULL인 minutes만 처리
|
||||
- agenda_sections.todos와 todos 테이블 동시 저장 (필요시)
|
||||
- 복합 인덱스 활용 (meeting_id, user_id)
|
||||
|
||||
### Don'ts ✗
|
||||
- minutes 테이블에 content 저장 (없음)
|
||||
- 참석자별 회의록(user_id NOT NULL)을 AI 분석 (통합만)
|
||||
- agenda_sections를 모든 minutes에 생성 (통합만)
|
||||
- 인덱스 무시한 풀 스캔
|
||||
|
||||
---
|
||||
|
||||
## 8. 파일 위치 및 참조
|
||||
|
||||
**마이그레이션 파일**:
|
||||
- `/Users/jominseo/HGZero/meeting/src/main/resources/db/migration/V2__*.sql`
|
||||
- `/Users/jominseo/HGZero/meeting/src/main/resources/db/migration/V3__*.sql`
|
||||
- `/Users/jominseo/HGZero/meeting/src/main/resources/db/migration/V4__*.sql`
|
||||
|
||||
**엔티티**:
|
||||
- `MeetingEntity`, `MinutesEntity`, `MinutesSectionEntity`
|
||||
- `AgendaSectionEntity`, `TodoEntity`, `MeetingParticipantEntity`
|
||||
|
||||
**서비스**:
|
||||
- `MinutesService`, `MinutesSectionService`
|
||||
- `MinutesAnalysisEventConsumer` (비동기)
|
||||
|
||||
---
|
||||
|
||||
## 9. 결론
|
||||
|
||||
### 핵심 설계 원칙
|
||||
1. **메타데이터 vs 내용 분리**: minutes (메타) vs minutes_sections (내용)
|
||||
2. **사용자별 격리**: user_id 컬럼으로 개인 회의록 관리
|
||||
3. **AI 결과 구조화**: JSON으로 유연성과 성능 확보
|
||||
4. **정규화 완료**: 참석자 정보 테이블화
|
||||
|
||||
### 검증 사항
|
||||
- V3, V4 마이그레이션 정상 적용
|
||||
- 모든 인덱스 생성됨
|
||||
- 관계 설정 정상 (FK, 1:N)
|
||||
|
||||
### 다음 단계
|
||||
- 성능 모니터링 (쿼리 실행 계획)
|
||||
- 추가 인덱스 검토
|
||||
- AI 분석 결과 검증
|
||||
- 참석자별 회의록 사용성 테스트
|
||||
|
||||
---
|
||||
|
||||
**문서 정보**:
|
||||
- 작성자: Database Architecture Analysis
|
||||
- 대상 서비스: Meeting Service (AI 통합 회의록)
|
||||
- 최종 버전: 2025-10-28
|
||||
560
claude/data-flow-diagram.md
Normal file
560
claude/data-flow-diagram.md
Normal file
@ -0,0 +1,560 @@
|
||||
# Meeting Service 데이터 플로우 다이어그램
|
||||
|
||||
## 1. 전체 시스템 플로우
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 회의 생명주기 데이터 플로우 │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Phase 1: 회의 준비 단계
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
사용자가 회의 생성
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 1-1. CreateMeeting API │
|
||||
│ ────────────────────────────────────────────────────────────│
|
||||
│ INSERT INTO meetings ( │
|
||||
│ meeting_id, title, purpose, scheduled_at, │
|
||||
│ organizer_id, status, created_at │
|
||||
│ ) │
|
||||
│ VALUES (...) │
|
||||
│ │
|
||||
│ + INSERT INTO meeting_participants [V2] │
|
||||
│ (meeting_id, user_id, invitation_status) │
|
||||
│ FOR EACH participant │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
DB State:
|
||||
✓ meetings: SCHEDULED status
|
||||
✓ meeting_participants: PENDING status
|
||||
|
||||
|
||||
Phase 2: 회의 진행 중
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
회의 시작 (start_meeting API)
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 2-1. StartMeeting UseCase │
|
||||
│ ────────────────────────────────────────────────────────────│
|
||||
│ UPDATE meetings SET │
|
||||
│ status = 'IN_PROGRESS', │
|
||||
│ started_at = NOW() │
|
||||
│ WHERE meeting_id = ? │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
회의 중 회의록 작성
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 2-2. CreateMinutes API (회의 시작 후) │
|
||||
│ ────────────────────────────────────────────────────────────│
|
||||
│ INSERT INTO minutes ( │
|
||||
│ id, meeting_id, user_id, title, status, │
|
||||
│ created_by, version, created_at │
|
||||
│ ) VALUES ( │
|
||||
│ 'consolidated-minutes-1', 'meeting-001', │
|
||||
│ NULL, [V3] ← AI 통합 회의록 표시 │
|
||||
│ '2025년 1월 10일 회의', 'DRAFT', ... │
|
||||
│ ) │
|
||||
│ │
|
||||
│ + 각 참석자별 회의록도 동시 생성: │
|
||||
│ INSERT INTO minutes ( │
|
||||
│ id, meeting_id, user_id, ... │
|
||||
│ ) VALUES ( │
|
||||
│ 'user-minutes-user1', 'meeting-001', │
|
||||
│ 'user1@example.com', [V3] ← 참석자 구분 │
|
||||
│ ... │
|
||||
│ ) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
DB State:
|
||||
✓ meetings: IN_PROGRESS
|
||||
✓ minutes (multiple records):
|
||||
- 1개의 통합 회의록 (user_id=NULL)
|
||||
- N개의 참석자별 회의록 (user_id=참석자ID)
|
||||
✓ minutes_sections: 초기 섹션 생성
|
||||
|
||||
|
||||
Phase 3: 회의록 작성 중
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
사용자가 회의록 섹션 작성
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 3-1. UpdateMinutes API (여러 번) │
|
||||
│ ────────────────────────────────────────────────────────────│
|
||||
│ │
|
||||
│ 각 섹션별로: │
|
||||
│ INSERT INTO minutes_sections ( │
|
||||
│ id, minutes_id, type, title, content, order │
|
||||
│ ) VALUES ( │
|
||||
│ 'section-1', 'consolidated-minutes-1', │
|
||||
│ 'DISCUSSION', '신제품 기획 방향', │
|
||||
│ '신제품의 주요 타겟은 20-30대 직장인으로 설정...', │
|
||||
│ 1 │
|
||||
│ ) │
|
||||
│ │
|
||||
│ UPDATE minutes_sections SET │
|
||||
│ content = '...', │
|
||||
│ updated_at = NOW() │
|
||||
│ WHERE id = 'section-1' │
|
||||
│ │
|
||||
│ ★ 중요: content 컬럼에 실제 회의록 내용 저장! │
|
||||
│ minutes 테이블에는 content가 없음 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
DB State:
|
||||
✓ minutes: status='DRAFT'
|
||||
✓ minutes_sections: 사용자가 작성한 내용 축적
|
||||
✓ 각 참석자가 자신의 회의록을 독립적으로 작성
|
||||
|
||||
|
||||
Phase 4: 회의 종료
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
회의 종료 (end_meeting API)
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 4-1. EndMeeting UseCase [V3 추가] │
|
||||
│ ────────────────────────────────────────────────────────────│
|
||||
│ UPDATE meetings SET │
|
||||
│ status = 'COMPLETED', │
|
||||
│ ended_at = NOW() [V3] ← 종료 시간 기록 │
|
||||
│ WHERE meeting_id = ? │
|
||||
│ │
|
||||
│ ★ 중요: 회의 종료와 동시에 회의록 준비 시작 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
DB State:
|
||||
✓ meetings: status='COMPLETED', ended_at=현재시간
|
||||
✓ minutes: 계속 DRAFT (사용자 추가 편집 가능)
|
||||
|
||||
|
||||
Phase 5: 회의록 최종화
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
사용자가 회의록 최종화 요청
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 5-1. FinalizeMinutes API │
|
||||
│ ────────────────────────────────────────────────────────────│
|
||||
│ UPDATE minutes SET │
|
||||
│ status = 'FINALIZED', │
|
||||
│ finalized_by = ?, │
|
||||
│ finalized_at = NOW(), │
|
||||
│ version = version + 1 │
|
||||
│ WHERE id = 'consolidated-minutes-1' │
|
||||
│ │
|
||||
│ UPDATE minutes_sections SET │
|
||||
│ locked = TRUE, │
|
||||
│ locked_by = ?, │
|
||||
│ verified = TRUE │
|
||||
│ WHERE minutes_id = 'consolidated-minutes-1' │
|
||||
│ │
|
||||
│ ★ 중요: minutes_id를 통해 관련된 모든 섹션 잠금 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
Event 발생: MinutesAnalysisRequestEvent (Async)
|
||||
↓
|
||||
DB State:
|
||||
✓ minutes: status='FINALIZED'
|
||||
✓ minutes_sections: locked=TRUE, verified=TRUE
|
||||
✓ 모든 섹션이 수정 불가능
|
||||
|
||||
|
||||
Phase 6: AI 분석 처리 (비동기 - MinutesAnalysisEventConsumer)
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
이벤트 수신: MinutesAnalysisRequestEvent
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 6-1. 통합 회의록 조회 (user_id=NULL) │
|
||||
│ ────────────────────────────────────────────────────────────│
|
||||
│ SELECT m.*, GROUP_CONCAT(ms.content) AS full_content │
|
||||
│ FROM minutes m │
|
||||
│ LEFT JOIN minutes_sections ms ON m.id = ms.minutes_id │
|
||||
│ WHERE m.meeting_id = ? AND m.user_id IS NULL │
|
||||
│ ORDER BY ms.order │
|
||||
│ │
|
||||
│ ★ 참석자별 회의록은 AI 분석 대상이 아님 │
|
||||
│ user_id IS NOT NULL인 것들은 개인 기록용 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
AI Service 호출 (Claude API)
|
||||
↓
|
||||
AI가 회의록 분석
|
||||
- 안건별로 분리
|
||||
- 요약 생성
|
||||
- 결정사항 추출
|
||||
- 보류사항 추출
|
||||
- Todo 추출
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 6-2. agenda_sections 생성 [V3] │
|
||||
│ ────────────────────────────────────────────────────────────│
|
||||
│ INSERT INTO agenda_sections ( │
|
||||
│ id, minutes_id, meeting_id, agenda_number, │
|
||||
│ agenda_title, ai_summary_short, discussions, │
|
||||
│ decisions, pending_items, opinions, todos [V4] │
|
||||
│ ) VALUES ( │
|
||||
│ 'uuid-1', 'consolidated-minutes-1', 'meeting-001', │
|
||||
│ 1, '신제품 기획 방향성', │
|
||||
│ '타겟 고객을 20-30대로 설정...', │
|
||||
│ '신제품의 주요 타겟 고객층을 20-30대...', │
|
||||
│ ["타겟 고객: 20-30대 직장인", "UI 개선 최우선"], │
|
||||
│ [], │
|
||||
│ [{"speaker": "김민준", "opinion": "..."}], │
|
||||
│ [ │
|
||||
│ {"title": "시장 조사", "assignee": "김민준", │
|
||||
│ "dueDate": "2025-02-15", "priority": "HIGH"} │
|
||||
│ ] [V4] │
|
||||
│ ) │
|
||||
│ │
|
||||
│ FOR EACH agenda detected by AI │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 6-3. ai_summaries 저장 [V3] │
|
||||
│ ────────────────────────────────────────────────────────────│
|
||||
│ INSERT INTO ai_summaries ( │
|
||||
│ id, meeting_id, summary_type, │
|
||||
│ source_minutes_ids, result, processing_time_ms, │
|
||||
│ model_version, keywords, statistics, created_at │
|
||||
│ ) VALUES ( │
|
||||
│ 'summary-uuid-1', 'meeting-001', 'CONSOLIDATED', │
|
||||
│ ["consolidated-minutes-1"], │
|
||||
│ {AI 응답 전체 JSON}, │
|
||||
│ 2500, │
|
||||
│ 'claude-3.5-sonnet', │
|
||||
│ ["신제품", "타겟층", "UI개선"], │
|
||||
│ {"participants": 5, "agendas": 3, "todos": 8}, │
|
||||
│ NOW() │
|
||||
│ ) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 6-4. todos 저장 [V3 확장] │
|
||||
│ ────────────────────────────────────────────────────────────│
|
||||
│ INSERT INTO todos ( │
|
||||
│ todo_id, meeting_id, minutes_id, title, │
|
||||
│ assignee_id, due_date, status, priority, │
|
||||
│ extracted_by, section_reference, │
|
||||
│ extraction_confidence, created_at │
|
||||
│ ) VALUES ( │
|
||||
│ 'todo-uuid-1', 'meeting-001', │
|
||||
│ 'consolidated-minutes-1', '시장 조사 보고서 작성', │
|
||||
│ 'user1@example.com', '2025-02-15', 'PENDING', 'HIGH', │
|
||||
│ 'AI', '안건 1: 신제품 기획', [V3] │
|
||||
│ 0.95, [V3] 신뢰도 │
|
||||
│ NOW() │
|
||||
│ ) │
|
||||
│ │
|
||||
│ ★ 주의: agenda_sections.todos (JSON)에도 동시 저장 │
|
||||
│ 개별 관리 필요시만 todos 테이블에 저장 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
DB State:
|
||||
✓ agenda_sections: AI 요약 결과 저장됨 (안건별)
|
||||
✓ ai_summaries: AI 처리 결과 캐시
|
||||
✓ todos: AI 추출 Todo (extracted_by='AI')
|
||||
|
||||
|
||||
Phase 7: 회의록 및 분석 결과 조회
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Case 1: 통합 회의록 조회
|
||||
─────────────────────────────────────────────────────────────
|
||||
SELECT m.*, ms.*, ag.*, ai.* FROM minutes m
|
||||
LEFT JOIN minutes_sections ms ON m.id = ms.minutes_id
|
||||
LEFT JOIN agenda_sections ag ON m.id = ag.minutes_id
|
||||
LEFT JOIN ai_summaries ai ON m.meeting_id = ai.meeting_id
|
||||
WHERE m.meeting_id = 'meeting-001'
|
||||
AND m.user_id IS NULL [V3]
|
||||
ORDER BY ms.order
|
||||
|
||||
|
||||
Case 2: 특정 사용자의 개인 회의록 조회
|
||||
─────────────────────────────────────────────────────────────
|
||||
SELECT m.*, ms.* FROM minutes m
|
||||
LEFT JOIN minutes_sections ms ON m.id = ms.minutes_id
|
||||
WHERE m.meeting_id = 'meeting-001'
|
||||
AND m.user_id = 'user1@example.com' [V3]
|
||||
ORDER BY ms.order
|
||||
|
||||
→ 개인이 작성한 회의록만 조회
|
||||
→ AI 분석 결과(agenda_sections) 미포함
|
||||
|
||||
|
||||
Case 3: AI 분석 결과만 조회
|
||||
─────────────────────────────────────────────────────────────
|
||||
SELECT ag.* FROM agenda_sections ag
|
||||
WHERE ag.meeting_id = 'meeting-001'
|
||||
ORDER BY ag.agenda_number
|
||||
|
||||
→ 안건별 AI 요약
|
||||
→ todos JSON 필드 포함 (V4)
|
||||
|
||||
|
||||
Case 4: 추출된 Todo 조회
|
||||
─────────────────────────────────────────────────────────────
|
||||
SELECT * FROM todos
|
||||
WHERE meeting_id = 'meeting-001'
|
||||
AND extracted_by = 'AI' [V3]
|
||||
ORDER BY priority DESC, due_date ASC
|
||||
|
||||
또는 agenda_sections의 JSON todos 필드 사용
|
||||
|
||||
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 상태 전이 다이어그램 (State Transition)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ meetings 테이블 상태 │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[생성]
|
||||
│
|
||||
├─────────────────────────┐
|
||||
▼ │
|
||||
SCHEDULED │ (시간 경과)
|
||||
(scheduled_at 설정) │
|
||||
│ │
|
||||
│ start_meeting API │
|
||||
▼ │
|
||||
IN_PROGRESS │
|
||||
(started_at 설정) │
|
||||
│ │
|
||||
│ end_meeting API [V3] │
|
||||
▼ │
|
||||
COMPLETED │
|
||||
(ended_at 설정) [V3 추가] ├─────────────────────────┐
|
||||
│ │ │
|
||||
└─────────────────────────┘ │
|
||||
│ 회의록 최종화 │
|
||||
│ (finalize_minutes API) │
|
||||
▼ │
|
||||
minutes: FINALIZED │
|
||||
(status='FINALIZED') │
|
||||
│ │
|
||||
│ (비동기 이벤트) │
|
||||
▼ │
|
||||
AI 분석 완료 │
|
||||
agenda_sections 생성 │
|
||||
ai_summaries 생성 │
|
||||
todos 추출 │
|
||||
│ │
|
||||
└─────────────────────────┘
|
||||
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ minutes 테이블 상태 │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
CREATE DRAFT
|
||||
(minutes 생성) ───────────► (사용자 작성 중)
|
||||
│
|
||||
update_minutes API
|
||||
│
|
||||
(섹션 추가/수정)
|
||||
│
|
||||
│
|
||||
finalize_minutes API
|
||||
│
|
||||
▼
|
||||
FINALIZED
|
||||
(AI 분석 대기 중)
|
||||
│
|
||||
(비동기 처리 완료)
|
||||
│
|
||||
▼
|
||||
분석 완료 (상태 유지)
|
||||
agenda_sections 생성됨
|
||||
ai_summaries 생성됨
|
||||
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ minutes_sections 잠금 상태 │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
편집 가능
|
||||
(locked=FALSE)
|
||||
│
|
||||
│ finalize_minutes
|
||||
│
|
||||
▼
|
||||
잠금됨
|
||||
(locked=TRUE, locked_by=user_id)
|
||||
│
|
||||
└─────► 수정 불가
|
||||
verified=TRUE
|
||||
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ todos 완료 상태 │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
PENDING
|
||||
(생성됨)
|
||||
│
|
||||
│ todo 완료 API
|
||||
│
|
||||
▼
|
||||
COMPLETED
|
||||
(completed_at 설정)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 사용자별 회의록 데이터 구조
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 1개 회의 (meetings: meeting-001)
|
||||
│ ├─ 참석자: user1, user2, user3
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
회의 종료 → minutes 테이블에 여러 레코드 생성
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ minutes 테이블 (3개 레코드 생성) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ id │ meeting_id │ user_id │ status
|
||||
├─────────────────────┼─────────────┼──────────────────────┼────────
|
||||
│ consol-minutes-001 │ meeting-001 │ NULL [V3] │ DRAFT
|
||||
│ user1-minutes-001 │ meeting-001 │ user1@example.com │ DRAFT
|
||||
│ user2-minutes-001 │ meeting-001 │ user2@example.com │ DRAFT
|
||||
│ user3-minutes-001 │ meeting-001 │ user3@example.com │ DRAFT
|
||||
└─────────────────────┴─────────────┴──────────────────────┴────────
|
||||
|
||||
↓ (각각 minutes_sections 참조)
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ minutes_sections 테이블 (4그룹 × N개 섹션) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ id │ minutes_id │ type │ title │ content
|
||||
├────────┼────────────────────┼─────────────┼──────────┼─────────
|
||||
│ sec-1 │ consol-minutes-001 │ DISCUSSION │ 안건1 │ "AI가..."
|
||||
│ sec-2 │ consol-minutes-001 │ DECISION │ 결정1 │ "..."
|
||||
│ │ │ │ │
|
||||
│ sec-3 │ user1-minutes-001 │ DISCUSSION │ 안건1 │ "사용자1..."
|
||||
│ sec-4 │ user1-minutes-001 │ DISCUSSION │ 안건2 │ "..."
|
||||
│ │ │ │ │
|
||||
│ sec-5 │ user2-minutes-001 │ DISCUSSION │ 안건1 │ "사용자2..."
|
||||
│ sec-6 │ user2-minutes-001 │ DECISION │ 결정1 │ "..."
|
||||
│ │ │ │ │
|
||||
│ sec-7 │ user3-minutes-001 │ DISCUSSION │ 안건1 │ "사용자3..."
|
||||
└────────┴────────────────────┴─────────────┴──────────┴─────────
|
||||
|
||||
|
||||
각 사용자가 독립적으로 작성:
|
||||
- User1: consol-minutes-001의 sec-3, sec-4 편집
|
||||
- User2: user2-minutes-001의 sec-5, sec-6 편집
|
||||
- User3: user3-minutes-001의 sec-7 편집
|
||||
|
||||
AI 분석 (user_id=NULL인 것만):
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ agenda_sections 테이블 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ id │ minutes_id │ meeting_id │ agenda_number
|
||||
├────────┼────────────────────┼─────────────┼──────────────────
|
||||
│ ag-1 │ consol-minutes-001 │ meeting-001 │ 1
|
||||
│ ag-2 │ consol-minutes-001 │ meeting-001 │ 2
|
||||
└────────┴────────────────────┴─────────────┴──────────────────
|
||||
|
||||
→ minutes_id를 통해 통합 회의록만 참조
|
||||
→ user_id='user1@example.com'인 회의록은 참조하지 않음
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 인덱스 활용 쿼리 예시
|
||||
|
||||
```sql
|
||||
-- 쿼리 1: 특정 회의의 통합 회의록 조회 (V3 인덱스 활용)
|
||||
SELECT * FROM minutes
|
||||
WHERE meeting_id = 'meeting-001' AND user_id IS NULL
|
||||
ORDER BY created_at DESC;
|
||||
└─► 인덱스: idx_minutes_meeting_user (meeting_id, user_id)
|
||||
|
||||
|
||||
-- 쿼리 2: 특정 사용자의 회의록 조회 (복합 인덱스 활용)
|
||||
SELECT * FROM minutes
|
||||
WHERE meeting_id = 'meeting-001' AND user_id = 'user1@example.com'
|
||||
ORDER BY created_at DESC;
|
||||
└─► 인덱스: idx_minutes_meeting_user (meeting_id, user_id)
|
||||
|
||||
|
||||
-- 쿼리 3: 안건별 AI 요약 조회 (V3 인덱스 활용)
|
||||
SELECT * FROM agenda_sections
|
||||
WHERE meeting_id = 'meeting-001'
|
||||
ORDER BY agenda_number ASC;
|
||||
└─► 인덱스: idx_sections_meeting (meeting_id)
|
||||
|
||||
|
||||
-- 쿼리 4: 특정 안건의 세부 요약 (복합 인덱스 활용)
|
||||
SELECT * FROM agenda_sections
|
||||
WHERE meeting_id = 'meeting-001' AND agenda_number = 1;
|
||||
└─► 인덱스: idx_sections_agenda (meeting_id, agenda_number)
|
||||
|
||||
|
||||
-- 쿼리 5: AI 추출 Todo 조회 (V3 인덱스 활용)
|
||||
SELECT * FROM todos
|
||||
WHERE meeting_id = 'meeting-001' AND extracted_by = 'AI'
|
||||
ORDER BY priority DESC, due_date ASC;
|
||||
└─► 인덱스: idx_todos_extracted (extracted_by)
|
||||
└─► 인덱스: idx_todos_meeting (meeting_id)
|
||||
|
||||
|
||||
-- 쿼리 6: 특정 회의의 모든 데이터 조회 (JOIN)
|
||||
SELECT
|
||||
m.*,
|
||||
ms.content,
|
||||
ag.ai_summary_short,
|
||||
ag.todos,
|
||||
ai.keywords
|
||||
FROM minutes m
|
||||
LEFT JOIN minutes_sections ms ON m.id = ms.minutes_id
|
||||
LEFT JOIN agenda_sections ag ON m.id = ag.minutes_id
|
||||
LEFT JOIN ai_summaries ai ON m.meeting_id = ai.meeting_id
|
||||
WHERE m.meeting_id = 'meeting-001' AND m.user_id IS NULL
|
||||
ORDER BY ms.order ASC, ag.agenda_number ASC;
|
||||
└─► 인덱스: idx_minutes_meeting_user (meeting_id, user_id)
|
||||
└─► 인덱스: idx_sections_minutes (minutes_id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 데이터 저장 크기 예상
|
||||
|
||||
```
|
||||
1개 회의 (참석자 5명) 데이터 크기:
|
||||
|
||||
├─ meetings: ~500 bytes
|
||||
├─ meeting_participants (5명): ~5 × 150 = 750 bytes
|
||||
├─ minutes (6개: 1 통합 + 5 개인): ~6 × 400 = 2.4 KB
|
||||
├─ minutes_sections (30개 섹션): ~30 × 2 KB = 60 KB
|
||||
├─ agenda_sections (5개 안건): ~5 × 4 KB = 20 KB
|
||||
├─ ai_summaries: ~10 KB
|
||||
└─ todos (8개): ~8 × 800 bytes = 6.4 KB
|
||||
|
||||
Total: ~100 KB/회의
|
||||
|
||||
1년 (250개 회의) 예상:
|
||||
└─► 25 MB + 인덱스 ~5 MB = ~30 MB
|
||||
|
||||
|
||||
JSON 필드 데이터 크기:
|
||||
├─ agenda_sections.decisions: ~200 bytes/건
|
||||
├─ agenda_sections.opinions: ~300 bytes/건
|
||||
├─ agenda_sections.todos: ~500 bytes/건 [V4]
|
||||
├─ ai_summaries.result: ~5-10 KB/건
|
||||
└─ ai_summaries.statistics: ~200 bytes/건
|
||||
```
|
||||
130
claude/database-diagram.puml
Normal file
130
claude/database-diagram.puml
Normal file
@ -0,0 +1,130 @@
|
||||
@startuml Meeting Service Database Schema
|
||||
!theme mono
|
||||
|
||||
'=== Core Tables ===
|
||||
entity "meetings" {
|
||||
* **meeting_id : VARCHAR(50)
|
||||
--
|
||||
title : VARCHAR(200) NOT NULL
|
||||
purpose : VARCHAR(500)
|
||||
description : TEXT
|
||||
scheduled_at : TIMESTAMP NOT NULL
|
||||
started_at : TIMESTAMP
|
||||
ended_at : TIMESTAMP [V3]
|
||||
status : VARCHAR(20) NOT NULL
|
||||
organizer_id : VARCHAR(50) NOT NULL
|
||||
created_at : TIMESTAMP
|
||||
updated_at : TIMESTAMP
|
||||
template_id : VARCHAR(50)
|
||||
}
|
||||
|
||||
entity "meeting_participants" {
|
||||
* **meeting_id : VARCHAR(50) [FK]
|
||||
* **user_id : VARCHAR(100)
|
||||
--
|
||||
invitation_status : VARCHAR(20)
|
||||
attended : BOOLEAN
|
||||
created_at : TIMESTAMP
|
||||
updated_at : TIMESTAMP
|
||||
}
|
||||
|
||||
entity "minutes" {
|
||||
* **id : VARCHAR(50)
|
||||
--
|
||||
meeting_id : VARCHAR(50) [FK] NOT NULL
|
||||
user_id : VARCHAR(100) [V3]
|
||||
title : VARCHAR(200) NOT NULL
|
||||
status : VARCHAR(20) NOT NULL
|
||||
version : INT NOT NULL
|
||||
created_by : VARCHAR(50) NOT NULL
|
||||
finalized_by : VARCHAR(50)
|
||||
finalized_at : TIMESTAMP
|
||||
created_at : TIMESTAMP
|
||||
updated_at : TIMESTAMP
|
||||
}
|
||||
|
||||
entity "minutes_sections" {
|
||||
* **id : VARCHAR(50)
|
||||
--
|
||||
minutes_id : VARCHAR(50) [FK] NOT NULL
|
||||
type : VARCHAR(50) NOT NULL
|
||||
title : VARCHAR(200) NOT NULL
|
||||
**content : TEXT
|
||||
order : INT
|
||||
verified : BOOLEAN
|
||||
locked : BOOLEAN
|
||||
locked_by : VARCHAR(50)
|
||||
created_at : TIMESTAMP
|
||||
updated_at : TIMESTAMP
|
||||
}
|
||||
|
||||
'=== V3 New Tables ===
|
||||
entity "agenda_sections" {
|
||||
* **id : VARCHAR(36)
|
||||
--
|
||||
minutes_id : VARCHAR(36) [FK] NOT NULL
|
||||
meeting_id : VARCHAR(50) [FK] NOT NULL
|
||||
agenda_number : INT NOT NULL
|
||||
agenda_title : VARCHAR(200) NOT NULL
|
||||
ai_summary_short : TEXT
|
||||
discussions : TEXT
|
||||
decisions : JSON
|
||||
pending_items : JSON
|
||||
opinions : JSON
|
||||
**todos : JSON [V4]
|
||||
created_at : TIMESTAMP
|
||||
updated_at : TIMESTAMP
|
||||
}
|
||||
|
||||
entity "ai_summaries" {
|
||||
* **id : VARCHAR(36)
|
||||
--
|
||||
meeting_id : VARCHAR(50) [FK] NOT NULL
|
||||
summary_type : VARCHAR(50) NOT NULL
|
||||
source_minutes_ids : JSON NOT NULL
|
||||
result : JSON NOT NULL
|
||||
processing_time_ms : INT
|
||||
model_version : VARCHAR(50)
|
||||
keywords : JSON
|
||||
statistics : JSON
|
||||
created_at : TIMESTAMP
|
||||
}
|
||||
|
||||
entity "todos" {
|
||||
* **todo_id : VARCHAR(50)
|
||||
--
|
||||
meeting_id : VARCHAR(50) [FK] NOT NULL
|
||||
minutes_id : VARCHAR(50) [FK]
|
||||
title : VARCHAR(200) NOT NULL
|
||||
description : TEXT
|
||||
assignee_id : VARCHAR(50) NOT NULL
|
||||
due_date : DATE
|
||||
status : VARCHAR(20) NOT NULL
|
||||
priority : VARCHAR(20)
|
||||
extracted_by : VARCHAR(50) [V3]
|
||||
section_reference : VARCHAR(200) [V3]
|
||||
extraction_confidence : DECIMAL(3,2) [V3]
|
||||
completed_at : TIMESTAMP
|
||||
created_at : TIMESTAMP
|
||||
updated_at : TIMESTAMP
|
||||
}
|
||||
|
||||
'=== Relationships ===
|
||||
meetings ||--o{ meeting_participants : "1:N [V2]"
|
||||
meetings ||--o{ minutes : "1:N"
|
||||
meetings ||--o{ agenda_sections : "1:N [V3]"
|
||||
meetings ||--o{ ai_summaries : "1:N [V3]"
|
||||
meetings ||--o{ todos : "1:N"
|
||||
minutes ||--o{ minutes_sections : "1:N"
|
||||
minutes ||--o{ agenda_sections : "1:N [V3]"
|
||||
|
||||
'=== Legend ===
|
||||
legend right
|
||||
V2 = Migration 2 (2025-10-27)
|
||||
V3 = Migration 3 (2025-10-28)
|
||||
V4 = Migration 4 (2025-10-28)
|
||||
[FK] = Foreign Key
|
||||
**bold** = Important fields
|
||||
end legend
|
||||
|
||||
@enduml
|
||||
675
claude/database-schema-analysis.md
Normal file
675
claude/database-schema-analysis.md
Normal file
@ -0,0 +1,675 @@
|
||||
# Meeting Service 데이터베이스 스키마 전체 분석
|
||||
|
||||
## 1. 마이그레이션 파일 현황
|
||||
|
||||
### 마이그레이션 체인
|
||||
```
|
||||
V1 (초기) → V2 (회의 참석자) → V3 (회의종료) → V4 (todos)
|
||||
```
|
||||
|
||||
### 각 마이그레이션 내용
|
||||
- **V1**: 초기 스키마 (meetings, minutes, minutes_sections 등 - JPA로 자동 생성)
|
||||
- **V2**: `meeting_participants` 테이블 분리 (2025-10-27)
|
||||
- **V3**: 회의종료 기능 지원 (2025-10-28) - **주요 변경**
|
||||
- **V4**: `agenda_sections` 테이블에 `todos` 컬럼 추가 (2025-10-28)
|
||||
|
||||
---
|
||||
|
||||
## 2. 핵심 테이블 구조 분석
|
||||
|
||||
### 2.1 meetings 테이블
|
||||
**용도**: 회의 기본 정보 저장
|
||||
|
||||
| 컬럼명 | 타입 | 설명 | 용도 |
|
||||
|--------|------|------|------|
|
||||
| meeting_id | VARCHAR(50) | PK | 회의 고유 식별자 |
|
||||
| title | VARCHAR(200) | NOT NULL | 회의 제목 |
|
||||
| purpose | VARCHAR(500) | | 회의 목적 |
|
||||
| description | TEXT | | 상세 설명 |
|
||||
| scheduled_at | TIMESTAMP | NOT NULL | 예정된 시간 |
|
||||
| started_at | TIMESTAMP | | 실제 시작 시간 |
|
||||
| ended_at | TIMESTAMP | | **V3 추가**: 실제 종료 시간 |
|
||||
| status | VARCHAR(20) | NOT NULL | 상태: SCHEDULED, IN_PROGRESS, COMPLETED |
|
||||
| organizer_id | VARCHAR(50) | NOT NULL | 회의 주최자 |
|
||||
| created_at | TIMESTAMP | | 생성 시간 |
|
||||
| updated_at | TIMESTAMP | | 수정 시간 |
|
||||
|
||||
**관계**:
|
||||
- 1:N with `meeting_participants` (V2에서 분리)
|
||||
- 1:N with `minutes`
|
||||
|
||||
---
|
||||
|
||||
### 2.2 minutes 테이블
|
||||
**용도**: 회의록 기본 정보 + 사용자별 회의록 구분
|
||||
|
||||
| 컬럼명 | 타입 | 설명 | 용도 |
|
||||
|--------|------|------|------|
|
||||
| id/minutes_id | VARCHAR(50) | PK | 회의록 고유 식별자 |
|
||||
| meeting_id | VARCHAR(50) | FK | 해당 회의 ID |
|
||||
| user_id | VARCHAR(100) | **V3 추가** | NULL: AI 통합 회의록 / NOT NULL: 참석자별 회의록 |
|
||||
| title | VARCHAR(200) | NOT NULL | 회의록 제목 |
|
||||
| status | VARCHAR(20) | NOT NULL | DRAFT, FINALIZED |
|
||||
| version | INT | NOT NULL | 버전 관리 |
|
||||
| created_by | VARCHAR(50) | NOT NULL | 작성자 |
|
||||
| finalized_by | VARCHAR(50) | | 확정자 |
|
||||
| finalized_at | TIMESTAMP | | 확정 시간 |
|
||||
| created_at | TIMESTAMP | | 생성 시간 |
|
||||
| updated_at | TIMESTAMP | | 수정 시간 |
|
||||
|
||||
**중요**: `minutes` 테이블에는 `content` 컬럼이 **없음**
|
||||
- 실제 회의록 내용은 `minutes_sections`의 `content`에 저장됨
|
||||
- minutes는 메타데이터만 저장
|
||||
|
||||
**인덱스 (V3)**: `idx_minutes_meeting_user` on (meeting_id, user_id)
|
||||
|
||||
**관계**:
|
||||
- N:1 with `meetings`
|
||||
- 1:N with `minutes_sections`
|
||||
- 1:N with `agenda_sections` (V3 추가)
|
||||
|
||||
---
|
||||
|
||||
### 2.3 minutes_sections 테이블
|
||||
**용도**: 회의록 섹션별 상세 내용
|
||||
|
||||
| 컬럼명 | 타입 | 설명 |
|
||||
|--------|------|------|
|
||||
| id | VARCHAR(50) | PK |
|
||||
| minutes_id | VARCHAR(50) | FK to minutes |
|
||||
| type | VARCHAR(50) | AGENDA, DISCUSSION, DECISION, ACTION_ITEM |
|
||||
| title | VARCHAR(200) | 섹션 제목 |
|
||||
| **content** | TEXT | **섹션 상세 내용 저장** |
|
||||
| order | INT | 섹션 순서 |
|
||||
| verified | BOOLEAN | 검증 완료 여부 |
|
||||
| locked | BOOLEAN | 잠금 여부 |
|
||||
| locked_by | VARCHAR(50) | 잠금 사용자 |
|
||||
|
||||
**중요 사항**:
|
||||
- 회의록 실제 내용은 여기에 저장됨
|
||||
- `minutes`와 N:1 관계 (1개 회의록에 다중 섹션)
|
||||
- 사용자별 회의록도 각각 섹션을 가짐
|
||||
|
||||
---
|
||||
|
||||
### 2.4 agenda_sections 테이블 (V3 신규)
|
||||
**용도**: 안건별 AI 요약 결과 저장 (구조화된 형식)
|
||||
|
||||
| 컬럼명 | 타입 | 설명 | 포함 데이터 |
|
||||
|--------|------|------|-----------|
|
||||
| id | VARCHAR(36) | PK | UUID |
|
||||
| minutes_id | VARCHAR(36) | FK | 통합 회의록 참조 |
|
||||
| meeting_id | VARCHAR(50) | FK | 회의 ID |
|
||||
| agenda_number | INT | | 안건 번호 (1, 2, 3...) |
|
||||
| agenda_title | VARCHAR(200) | | 안건 제목 |
|
||||
| ai_summary_short | TEXT | | 짧은 요약 (1줄, 20자 이내) |
|
||||
| discussions | TEXT | | 논의 사항 (3-5문장) |
|
||||
| decisions | JSON | | 결정 사항 배열 |
|
||||
| pending_items | JSON | | 보류 사항 배열 |
|
||||
| opinions | JSON | | 참석자별 의견: [{speaker, opinion}] |
|
||||
| **todos** | JSON | **V4 추가** | 추출된 Todo: [{title, assignee, dueDate, description, priority}] |
|
||||
|
||||
**V4 추가 구조** (todos JSON):
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": "시장 조사 보고서 작성",
|
||||
"assignee": "김민준",
|
||||
"dueDate": "2025-02-15",
|
||||
"description": "20-30대 타겟 시장 조사",
|
||||
"priority": "HIGH"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**인덱스**:
|
||||
- `idx_sections_meeting` on meeting_id
|
||||
- `idx_sections_agenda` on (meeting_id, agenda_number)
|
||||
- `idx_sections_minutes` on minutes_id
|
||||
|
||||
**관계**:
|
||||
- N:1 with `minutes` (통합 회의록만 참조)
|
||||
- N:1 with `meetings`
|
||||
|
||||
---
|
||||
|
||||
### 2.5 minutes_section vs agenda_sections 차이점
|
||||
|
||||
| 특성 | minutes_sections | agenda_sections |
|
||||
|------|------------------|-----------------|
|
||||
| **용도** | 회의록 작성용 | AI 요약 결과 저장용 |
|
||||
| **구조** | 순차적 섹션 (type: AGENDA, DISCUSSION, DECISION) | 안건별 구조화된 데이터 |
|
||||
| **내용 저장** | content (TEXT) | 구조화된 필드 + JSON |
|
||||
| **소유 관계** | 모든 회의록 (사용자별 포함) | 통합 회의록만 (user_id=NULL) |
|
||||
| **목적** | 사용자 작성 | AI 자동 생성 |
|
||||
| **JSON 필드** | 없음 | decisions, pending_items, opinions, todos |
|
||||
|
||||
**생성 흐름**:
|
||||
```
|
||||
회의 종료 → 통합 회의록 (minutes, user_id=NULL)
|
||||
→ minutes_sections 생성 (사용자가 내용 작성)
|
||||
→ AI 분석 → agenda_sections 생성 (AI 요약 결과 저장)
|
||||
|
||||
동시에:
|
||||
→ 참석자별 회의록 (minutes, user_id NOT NULL)
|
||||
→ 참석자별 minutes_sections 생성
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.6 ai_summaries 테이블 (V3 신규)
|
||||
**용도**: AI 요약 결과 캐싱
|
||||
|
||||
| 컬럼명 | 타입 | 설명 |
|
||||
|--------|------|------|
|
||||
| id | VARCHAR(36) | PK |
|
||||
| meeting_id | VARCHAR(50) | FK |
|
||||
| summary_type | VARCHAR(50) | CONSOLIDATED (통합 요약) / TODO_EXTRACTION (Todo 추출) |
|
||||
| source_minutes_ids | JSON | 통합에 사용된 회의록 ID 배열 |
|
||||
| result | JSON | **AI 응답 전체 결과** |
|
||||
| processing_time_ms | INT | AI 처리 시간 |
|
||||
| model_version | VARCHAR(50) | 사용 모델 (claude-3.5-sonnet) |
|
||||
| keywords | JSON | 주요 키워드 배열 |
|
||||
| statistics | JSON | 통계 (참석자 수, 안건 수 등) |
|
||||
|
||||
---
|
||||
|
||||
### 2.7 todos 테이블
|
||||
**용도**: Todo 아이템 저장
|
||||
|
||||
| 컬럼명 | 타입 | 설명 |
|
||||
|--------|------|------|
|
||||
| todo_id | VARCHAR(50) | PK |
|
||||
| minutes_id | VARCHAR(50) | FK | 관련 회의록 |
|
||||
| meeting_id | VARCHAR(50) | FK | 회의 ID |
|
||||
| title | VARCHAR(200) | 제목 |
|
||||
| description | TEXT | 상세 설명 |
|
||||
| assignee_id | VARCHAR(50) | 담당자 |
|
||||
| due_date | DATE | 마감일 |
|
||||
| status | VARCHAR(20) | PENDING, COMPLETED |
|
||||
| priority | VARCHAR(20) | HIGH, MEDIUM, LOW |
|
||||
| completed_at | TIMESTAMP | 완료 시간 |
|
||||
|
||||
**V3에서 추가된 컬럼**:
|
||||
```sql
|
||||
extracted_by VARCHAR(50) -- AI 또는 MANUAL
|
||||
section_reference VARCHAR(200) -- 관련 회의록 섹션 참조
|
||||
extraction_confidence DECIMAL(3,2) -- AI 신뢰도 (0.00~1.00)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.8 meeting_participants 테이블 (V2 신규)
|
||||
**용도**: 회의 참석자 정보 분리
|
||||
|
||||
| 컬럼명 | 타입 | 설명 |
|
||||
|--------|------|------|
|
||||
| meeting_id | VARCHAR(50) | PK1, FK |
|
||||
| user_id | VARCHAR(100) | PK2 |
|
||||
| invitation_status | VARCHAR(20) | PENDING, ACCEPTED, DECLINED |
|
||||
| attended | BOOLEAN | 참석 여부 |
|
||||
| created_at | TIMESTAMP | |
|
||||
| updated_at | TIMESTAMP | |
|
||||
|
||||
**변경 배경 (V2)**:
|
||||
- 이전: meetings.participants (CSV 문자열)
|
||||
- 현재: meeting_participants (별도 테이블, 정규화)
|
||||
|
||||
---
|
||||
|
||||
## 3. 회의록 작성 플로우에서의 테이블 사용
|
||||
|
||||
### 3.1 회의 시작 (StartMeeting)
|
||||
```
|
||||
meetings 테이블 UPDATE
|
||||
└─ status: SCHEDULED → IN_PROGRESS
|
||||
└─ started_at 기록
|
||||
```
|
||||
|
||||
### 3.2 회의 종료 (EndMeeting)
|
||||
```
|
||||
meetings 테이블 UPDATE
|
||||
├─ status: IN_PROGRESS → COMPLETED
|
||||
└─ ended_at 기록 (V3 신규)
|
||||
|
||||
↓
|
||||
|
||||
minutes 테이블 생성 (AI 통합 회의록)
|
||||
├─ user_id = NULL
|
||||
├─ status = DRAFT
|
||||
└─ 각 참석자별 회의록도 동시 생성
|
||||
└─ user_id = 참석자ID
|
||||
|
||||
↓
|
||||
|
||||
minutes_sections 테이블 초기 생성
|
||||
├─ 통합 회의록용 섹션
|
||||
└─ 각 참석자별 섹션
|
||||
```
|
||||
|
||||
### 3.3 회의록 작성 (CreateMinutes / UpdateMinutes)
|
||||
```
|
||||
minutes 테이블 UPDATE
|
||||
├─ title 작성
|
||||
└─ status 유지 (DRAFT)
|
||||
|
||||
↓
|
||||
|
||||
minutes_sections 테이블 INSERT/UPDATE
|
||||
├─ type: AGENDA, DISCUSSION, DECISION 등
|
||||
├─ title: 섹션 제목
|
||||
├─ content: 실제 회의록 내용 ← **여기에 사용자가 입력한 내용 저장**
|
||||
└─ order: 순서
|
||||
|
||||
사용자가 작성한 내용 저장 경로:
|
||||
minutes_sections.content (TEXT 컬럼)
|
||||
```
|
||||
|
||||
### 3.4 AI 분석 (FinializeMinutes + AI Processing)
|
||||
```
|
||||
minutes 테이블 UPDATE
|
||||
├─ status: DRAFT → FINALIZED
|
||||
└─ finalized_at 기록
|
||||
|
||||
↓
|
||||
|
||||
agenda_sections 테이블 INSERT
|
||||
├─ minutesId = 통합 회의록 ID (user_id=NULL)
|
||||
├─ AI 요약: aiSummaryShort, discussions
|
||||
├─ 구조화된 데이터: decisions, pendingItems, opinions (JSON)
|
||||
└─ todos (V4): AI 추출 Todo (JSON)
|
||||
|
||||
↓
|
||||
|
||||
ai_summaries 테이블 INSERT
|
||||
├─ summary_type: CONSOLIDATED
|
||||
├─ result: AI 응답 전체 결과
|
||||
└─ keywords, statistics
|
||||
|
||||
↓
|
||||
|
||||
todos 테이블 INSERT (선택)
|
||||
├─ 간단한 Todo는 agenda_sections.todos에만 저장
|
||||
└─ 상세 관리 필요한 경우 별도 테이블 저장
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 사용자별 회의록 저장 구조
|
||||
|
||||
### 4.1 회의 종료 시 자동 생성
|
||||
|
||||
```
|
||||
1개의 회의 → 여러 회의록
|
||||
├─ AI 통합 회의록 (minutes.user_id = NULL)
|
||||
│ ├─ minutes_sections (AI/시스템이 생성)
|
||||
│ └─ agenda_sections (AI 분석 결과)
|
||||
│
|
||||
└─ 각 참석자별 회의록 (minutes.user_id = 참석자ID)
|
||||
├─ User1의 회의록 (minutes.user_id = 'user1@example.com')
|
||||
│ └─ minutes_sections (User1이 작성)
|
||||
│
|
||||
├─ User2의 회의록 (minutes.user_id = 'user2@example.com')
|
||||
│ └─ minutes_sections (User2이 작성)
|
||||
│
|
||||
└─ ...
|
||||
```
|
||||
|
||||
### 4.2 minutes 테이블 쿼리 예시
|
||||
|
||||
```sql
|
||||
-- 특정 회의의 AI 통합 회의록
|
||||
SELECT * FROM minutes
|
||||
WHERE meeting_id = 'meeting-001' AND user_id IS NULL;
|
||||
|
||||
-- 특정 회의의 참석자별 회의록
|
||||
SELECT * FROM minutes
|
||||
WHERE meeting_id = 'meeting-001' AND user_id IS NOT NULL;
|
||||
|
||||
-- 특정 사용자의 회의록
|
||||
SELECT * FROM minutes
|
||||
WHERE user_id = 'user1@example.com';
|
||||
|
||||
-- 참석자별로 회의록 조회 (복합 인덱스 활용)
|
||||
SELECT * FROM minutes
|
||||
WHERE meeting_id = 'meeting-001' AND user_id = 'user1@example.com';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. V3 마이그레이션의 주요 변경사항
|
||||
|
||||
### 5.1 minutes 테이블 확장
|
||||
```sql
|
||||
ALTER TABLE minutes ADD COLUMN IF NOT EXISTS user_id VARCHAR(100);
|
||||
CREATE INDEX IF NOT EXISTS idx_minutes_meeting_user ON minutes(meeting_id, user_id);
|
||||
```
|
||||
|
||||
**영향**:
|
||||
- 기존 회의록: `user_id = NULL` (AI 통합 회의록)
|
||||
- 새 회의록: `user_id = 참석자ID` (참석자별)
|
||||
- 쿼리 성능: 복합 인덱스로 빠른 검색
|
||||
|
||||
### 5.2 agenda_sections 테이블 신규 생성
|
||||
- AI 요약을 구조화된 형식으로 저장
|
||||
- JSON 필드로 결정사항, 보류사항, 의견, Todo 저장
|
||||
- minutes_id로 통합 회의록과 연결
|
||||
|
||||
### 5.3 ai_summaries 테이블 신규 생성
|
||||
- AI 처리 결과 캐싱
|
||||
- 처리 시간, 모델 버전 기록
|
||||
- 재처리 필요 시 참조 가능
|
||||
|
||||
### 5.4 todos 테이블 확장
|
||||
```sql
|
||||
ALTER TABLE todos ADD COLUMN extracted_by VARCHAR(50) DEFAULT 'AI';
|
||||
ALTER TABLE todos ADD COLUMN section_reference VARCHAR(200);
|
||||
ALTER TABLE todos ADD COLUMN extraction_confidence DECIMAL(3,2) DEFAULT 0.00;
|
||||
```
|
||||
|
||||
**목적**:
|
||||
- AI 자동 추출 vs 수동 작성 구분
|
||||
- Todo의 출처 추적
|
||||
- AI 신뢰도 관리
|
||||
|
||||
---
|
||||
|
||||
## 6. V4 마이그레이션의 변경사항
|
||||
|
||||
### 6.1 agenda_sections 테이블에 todos 컬럼 추가
|
||||
```sql
|
||||
ALTER TABLE agenda_sections ADD COLUMN IF NOT EXISTS todos JSON;
|
||||
```
|
||||
|
||||
**구조**:
|
||||
```json
|
||||
{
|
||||
"title": "시장 조사 보고서 작성",
|
||||
"assignee": "김민준",
|
||||
"dueDate": "2025-02-15",
|
||||
"description": "20-30대 타겟 시장 조사",
|
||||
"priority": "HIGH"
|
||||
}
|
||||
```
|
||||
|
||||
**저장 경로**:
|
||||
- **안건별 요약의 Todo**: `agenda_sections.todos` (JSON)
|
||||
- **개별 Todo 관리**: `todos` 테이블 (필요시)
|
||||
|
||||
---
|
||||
|
||||
## 7. 데이터 정규화 현황
|
||||
|
||||
### 7.1 정규화 수행 (V2)
|
||||
```
|
||||
meetings (이전):
|
||||
participants: "user1@example.com,user2@example.com"
|
||||
|
||||
↓ 정규화 (V2 마이그레이션)
|
||||
|
||||
meetings_participants (별도 테이블):
|
||||
[meeting_id, user_id] (복합 PK)
|
||||
invitation_status
|
||||
attended
|
||||
```
|
||||
|
||||
### 7.2 JSON 필드 사용 (V3, V4)
|
||||
- `decisions`, `pending_items`, `opinions`, `todos` (agenda_sections)
|
||||
- `keywords`, `statistics` (ai_summaries)
|
||||
- `source_minutes_ids` (ai_summaries)
|
||||
|
||||
**사용 이유**:
|
||||
- 변동적인 구조 데이터
|
||||
- AI 응답의 유연한 저장
|
||||
- 쿼리 패턴이 검색보다 전체 조회
|
||||
|
||||
---
|
||||
|
||||
## 8. 핵심 질문 답변
|
||||
|
||||
### Q1: minutes 테이블에 content 필드가 있는가?
|
||||
**A**: **없음**. 회의록 실제 내용은 `minutes_sections.content`에 저장됨.
|
||||
|
||||
### Q2: minutes_section과 agenda_sections의 차이점?
|
||||
| 항목 | minutes_sections | agenda_sections |
|
||||
|------|-----------------|-----------------|
|
||||
| 목적 | 사용자 작성 | AI 요약 |
|
||||
| 모든 회의록 | O | X (통합만) |
|
||||
| 구조 | 순차적 | 안건별 |
|
||||
| 내용 저장 | content (TEXT) | JSON |
|
||||
|
||||
### Q3: 사용자별 회의록을 저장할 적절한 구조는?
|
||||
**A**:
|
||||
- `minutes` 테이블: `user_id` 컬럼으로 구분
|
||||
- `minutes_sections`: 각 회의록의 섹션
|
||||
- 인덱스: `idx_minutes_meeting_user` (meeting_id, user_id)
|
||||
|
||||
### Q4: V3, V4 주요 변경사항은?
|
||||
- **V3**: user_id 추가, agenda_sections 신규, ai_summaries 신규, todos 확장
|
||||
- **V4**: agenda_sections.todos JSON 필드 추가
|
||||
|
||||
---
|
||||
|
||||
## 9. 데이터베이스 구조도 (PlantUML)
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
!theme mono
|
||||
|
||||
entity "meetings" as meetings {
|
||||
* meeting_id: VARCHAR(50)
|
||||
--
|
||||
title: VARCHAR(200)
|
||||
status: VARCHAR(20)
|
||||
organizer_id: VARCHAR(50)
|
||||
started_at: TIMESTAMP
|
||||
ended_at: TIMESTAMP [V3]
|
||||
created_at: TIMESTAMP
|
||||
updated_at: TIMESTAMP
|
||||
}
|
||||
|
||||
entity "meeting_participants" as participants {
|
||||
* meeting_id: VARCHAR(50) [FK]
|
||||
* user_id: VARCHAR(100)
|
||||
--
|
||||
invitation_status: VARCHAR(20)
|
||||
attended: BOOLEAN
|
||||
}
|
||||
|
||||
entity "minutes" as minutes {
|
||||
* id: VARCHAR(50)
|
||||
--
|
||||
meeting_id: VARCHAR(50) [FK]
|
||||
user_id: VARCHAR(100) [V3]
|
||||
title: VARCHAR(200)
|
||||
status: VARCHAR(20)
|
||||
created_by: VARCHAR(50)
|
||||
finalized_at: TIMESTAMP
|
||||
}
|
||||
|
||||
entity "minutes_sections" as sections {
|
||||
* id: VARCHAR(50)
|
||||
--
|
||||
minutes_id: VARCHAR(50) [FK]
|
||||
type: VARCHAR(50)
|
||||
title: VARCHAR(200)
|
||||
content: TEXT
|
||||
locked: BOOLEAN
|
||||
}
|
||||
|
||||
entity "agenda_sections" as agenda {
|
||||
* id: VARCHAR(36)
|
||||
--
|
||||
minutes_id: VARCHAR(36) [FK, 통합회의록만]
|
||||
meeting_id: VARCHAR(50) [FK]
|
||||
agenda_number: INT
|
||||
agenda_title: VARCHAR(200)
|
||||
ai_summary_short: TEXT
|
||||
discussions: TEXT
|
||||
decisions: JSON
|
||||
opinions: JSON
|
||||
todos: JSON [V4]
|
||||
}
|
||||
|
||||
entity "ai_summaries" as summaries {
|
||||
* id: VARCHAR(36)
|
||||
--
|
||||
meeting_id: VARCHAR(50) [FK]
|
||||
summary_type: VARCHAR(50)
|
||||
result: JSON
|
||||
keywords: JSON
|
||||
statistics: JSON
|
||||
}
|
||||
|
||||
entity "todos" as todos {
|
||||
* todo_id: VARCHAR(50)
|
||||
--
|
||||
meeting_id: VARCHAR(50) [FK]
|
||||
minutes_id: VARCHAR(50) [FK]
|
||||
title: VARCHAR(200)
|
||||
assignee_id: VARCHAR(50)
|
||||
status: VARCHAR(20)
|
||||
extracted_by: VARCHAR(50) [V3]
|
||||
}
|
||||
|
||||
meetings ||--o{ participants: "1:N"
|
||||
meetings ||--o{ minutes: "1:N"
|
||||
meetings ||--o{ agenda: "1:N"
|
||||
meetings ||--o{ todos: "1:N"
|
||||
minutes ||--o{ sections: "1:N"
|
||||
minutes ||--o{ agenda: "1:N"
|
||||
meetings ||--o{ summaries: "1:N"
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 회의록 작성 전체 플로우
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 1. 회의 시작 (StartMeeting) │
|
||||
│ ├─ meetings.status = IN_PROGRESS │
|
||||
│ └─ meetings.started_at 기록 │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────▼───────────────────────────────────┐
|
||||
│ 2. 회의 진행 중 (회의록 작성) │
|
||||
│ ├─ CreateMinutes: minutes 생성 (user_id=NULL 통합) │
|
||||
│ ├─ CreateMinutes: 참석자별 minutes 생성 │
|
||||
│ ├─ UpdateMinutes: minutes_sections 작성 │
|
||||
│ │ └─ content에 회의 내용 저장 │
|
||||
│ └─ SaveMinutes: draft 상태 유지 │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────▼───────────────────────────────────┐
|
||||
│ 3. 회의 종료 (EndMeeting) │
|
||||
│ ├─ meetings.status = COMPLETED │
|
||||
│ ├─ meetings.ended_at = NOW() [V3] │
|
||||
│ └─ 회의 기본 정보 확정 │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────▼───────────────────────────────────┐
|
||||
│ 4. 회의록 최종화 (FinalizeMinutes) │
|
||||
│ ├─ minutes.status = FINALIZED │
|
||||
│ ├─ minutes.finalized_by = 확정자 │
|
||||
│ ├─ minutes.finalized_at = NOW() │
|
||||
│ └─ minutes_sections 내용 확정 (locked) │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────▼───────────────────────────────────┐
|
||||
│ 5. AI 분석 처리 (MinutesAnalysisEventConsumer) │
|
||||
│ ├─ 통합 회의록 분석 (user_id=NULL) │
|
||||
│ │ │
|
||||
│ ├─ agenda_sections INSERT [V3] │
|
||||
│ │ ├─ minutes_id = 통합 회의록 ID │
|
||||
│ │ ├─ ai_summary_short, discussions │
|
||||
│ │ ├─ decisions, pending_items, opinions (JSON) │
|
||||
│ │ └─ todos (JSON) [V4] │
|
||||
│ │ │
|
||||
│ ├─ ai_summaries INSERT [V3] │
|
||||
│ │ ├─ summary_type = CONSOLIDATED │
|
||||
│ │ ├─ result = AI 응답 전체 │
|
||||
│ │ └─ keywords, statistics │
|
||||
│ │ │
|
||||
│ └─ todos TABLE INSERT (선택) │
|
||||
│ ├─ extracted_by = 'AI' [V3] │
|
||||
│ └─ extraction_confidence [V3] │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────▼───────────────────────────────────┐
|
||||
│ 6. 회의록 조회 │
|
||||
│ ├─ 통합 회의록 조회 │
|
||||
│ │ └─ minutes + minutes_sections + agenda_sections │
|
||||
│ ├─ 참석자별 회의록 조회 │
|
||||
│ │ └─ minutes (user_id=참석자) + minutes_sections │
|
||||
│ └─ Todo 조회 │
|
||||
│ └─ agenda_sections.todos 또는 todos 테이블 │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 성능 최적화 포인트
|
||||
|
||||
### 11.1 인덱스 현황
|
||||
```
|
||||
meetings:
|
||||
- PK: meeting_id
|
||||
|
||||
minutes:
|
||||
- PK: id
|
||||
- idx_minutes_meeting_user (meeting_id, user_id) [V3] ← 핵심
|
||||
|
||||
minutes_sections:
|
||||
- PK: id
|
||||
- FK: minutes_id
|
||||
|
||||
agenda_sections: [V3]
|
||||
- PK: id
|
||||
- idx_sections_meeting (meeting_id)
|
||||
- idx_sections_agenda (meeting_id, agenda_number)
|
||||
- idx_sections_minutes (minutes_id)
|
||||
|
||||
ai_summaries: [V3]
|
||||
- PK: id
|
||||
- idx_summaries_meeting (meeting_id)
|
||||
- idx_summaries_type (meeting_id, summary_type)
|
||||
- idx_summaries_created (created_at)
|
||||
|
||||
todos:
|
||||
- PK: todo_id
|
||||
- idx_todos_extracted (extracted_by) [V3]
|
||||
- idx_todos_meeting (meeting_id) [V3]
|
||||
|
||||
meeting_participants: [V2]
|
||||
- PK: (meeting_id, user_id)
|
||||
- idx_user_id (user_id)
|
||||
- idx_invitation_status (invitation_status)
|
||||
```
|
||||
|
||||
### 11.2 추천 추가 인덱스
|
||||
```sql
|
||||
-- 빠른 조회를 위한 인덱스
|
||||
CREATE INDEX idx_minutes_status ON minutes(status, created_at DESC);
|
||||
CREATE INDEX idx_agenda_meeting_created ON agenda_sections(meeting_id, created_at DESC);
|
||||
CREATE INDEX idx_todos_meeting_assignee ON todos(meeting_id, assignee_id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 결론
|
||||
|
||||
### 핵심 설계 원칙
|
||||
1. **참석자별 회의록**: minutes.user_id로 구분 (NULL=AI 통합, NOT NULL=개인)
|
||||
2. **내용 저장**: minutes_sections.content에 사용자가 작성한 내용 저장
|
||||
3. **구조화된 요약**: agenda_sections에 AI 요약을 JSON으로 저장
|
||||
4. **추적 가능성**: extracted_by, section_reference로 Todo 출처 추적
|
||||
5. **정규화**: V2에서 meeting_participants로 정규화 완료
|
||||
|
||||
### 주의사항
|
||||
- `minutes` 테이블 자체는 메타데이터만 저장 (title, status 등)
|
||||
- 실제 회의 내용: `minutes_sections.content`
|
||||
- AI 요약 결과: `agenda_sections` (구조화됨)
|
||||
- Todo는 두 곳에 저장 가능: agenda_sections.todos (JSON) / todos 테이블
|
||||
538
docs/DB-Schema-회의종료.md
Normal file
538
docs/DB-Schema-회의종료.md
Normal file
@ -0,0 +1,538 @@
|
||||
# 회의종료 기능 DB 스키마 설계
|
||||
|
||||
## 📋 개요
|
||||
회의 종료 시 참석자별 회의록을 통합하고 AI 요약 및 Todo 자동 추출을 지원하기 위한 데이터베이스 스키마
|
||||
|
||||
**마이그레이션 버전**: V3__add_meeting_end_support.sql
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ 테이블 구조
|
||||
|
||||
### 1. minutes (확장)
|
||||
**설명**: 참석자별 회의록 및 AI 통합 회의록 저장
|
||||
|
||||
#### 새로 추가된 컬럼
|
||||
| 컬럼명 | 타입 | 제약조건 | 설명 |
|
||||
|--------|------|----------|------|
|
||||
| user_id | VARCHAR(100) | NULL | 작성자 사용자 ID (참석자별 회의록인 경우) |
|
||||
| is_consolidated | BOOLEAN | DEFAULT FALSE | AI 통합 회의록 여부 |
|
||||
| consolidated_by | VARCHAR(255) | DEFAULT 'AI' | 통합 처리자 |
|
||||
| section_type | VARCHAR(50) | DEFAULT 'PARTICIPANT' | 회의록 섹션 타입 |
|
||||
|
||||
#### 인덱스
|
||||
- `idx_minutes_meeting_user`: (meeting_id, user_id)
|
||||
- `idx_minutes_consolidated`: (is_consolidated)
|
||||
- `idx_minutes_section_type`: (section_type)
|
||||
|
||||
#### 사용 패턴
|
||||
```sql
|
||||
-- 참석자별 회의록 조회
|
||||
SELECT * FROM minutes
|
||||
WHERE meeting_id = 'meeting-123'
|
||||
AND is_consolidated = false
|
||||
AND section_type = 'PARTICIPANT';
|
||||
|
||||
-- AI 통합 회의록 조회
|
||||
SELECT * FROM minutes
|
||||
WHERE meeting_id = 'meeting-123'
|
||||
AND is_consolidated = true
|
||||
AND section_type = 'CONSOLIDATED';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. agenda_sections (신규)
|
||||
**설명**: 안건별 AI 요약 결과 저장
|
||||
|
||||
#### 컬럼 구조
|
||||
| 컬럼명 | 타입 | 제약조건 | 설명 |
|
||||
|--------|------|----------|------|
|
||||
| id | VARCHAR(36) | PRIMARY KEY | 섹션 고유 ID |
|
||||
| minutes_id | VARCHAR(36) | NOT NULL, FK | 회의록 ID (통합 회의록) |
|
||||
| meeting_id | VARCHAR(50) | NOT NULL, FK | 회의 ID |
|
||||
| agenda_number | INT | NOT NULL | 안건 번호 |
|
||||
| agenda_title | VARCHAR(200) | NOT NULL | 안건 제목 |
|
||||
| ai_summary_short | TEXT | NULL | AI 생성 짧은 요약 (1줄) |
|
||||
| discussions | TEXT | NULL | 논의 사항 (3-5문장) |
|
||||
| decisions | JSON | NULL | 결정 사항 배열 |
|
||||
| pending_items | JSON | NULL | 보류 사항 배열 |
|
||||
| opinions | JSON | NULL | 참석자별 의견 |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 생성 시간 |
|
||||
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 수정 시간 |
|
||||
|
||||
#### 외래키
|
||||
- `fk_agenda_sections_minutes`: minutes(id) ON DELETE CASCADE
|
||||
- `fk_agenda_sections_meeting`: meetings(meeting_id) ON DELETE CASCADE
|
||||
|
||||
#### 인덱스
|
||||
- `idx_sections_meeting`: (meeting_id)
|
||||
- `idx_sections_agenda`: (meeting_id, agenda_number)
|
||||
- `idx_sections_minutes`: (minutes_id)
|
||||
|
||||
#### JSON 데이터 구조
|
||||
|
||||
**decisions** (결정 사항):
|
||||
```json
|
||||
[
|
||||
"타겟 고객: 20-30대 직장인",
|
||||
"UI/UX 개선을 최우선 과제로 설정",
|
||||
"예산: 5억원"
|
||||
]
|
||||
```
|
||||
|
||||
**pending_items** (보류 사항):
|
||||
```json
|
||||
[
|
||||
"세부 일정 확정은 다음 회의에서 논의",
|
||||
"추가 예산 검토 필요"
|
||||
]
|
||||
```
|
||||
|
||||
**opinions** (참석자별 의견):
|
||||
```json
|
||||
[
|
||||
{
|
||||
"speaker": "김민준",
|
||||
"opinion": "타겟 고객층을 명확히 설정하여 마케팅 전략 수립 필요"
|
||||
},
|
||||
{
|
||||
"speaker": "박서연",
|
||||
"opinion": "UI/UX 개선에 AI 기술 적용 검토"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 사용 패턴
|
||||
```sql
|
||||
-- 안건별 섹션 조회
|
||||
SELECT * FROM agenda_sections
|
||||
WHERE meeting_id = 'meeting-123'
|
||||
ORDER BY agenda_number;
|
||||
|
||||
-- 특정 안건 조회
|
||||
SELECT * FROM agenda_sections
|
||||
WHERE meeting_id = 'meeting-123'
|
||||
AND agenda_number = 1;
|
||||
|
||||
-- JSON 필드 쿼리 (PostgreSQL)
|
||||
SELECT
|
||||
agenda_title,
|
||||
decisions,
|
||||
jsonb_array_length(decisions::jsonb) as decision_count
|
||||
FROM agenda_sections
|
||||
WHERE meeting_id = 'meeting-123';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. ai_summaries (신규)
|
||||
**설명**: AI 요약 결과 캐싱 및 성능 최적화
|
||||
|
||||
#### 컬럼 구조
|
||||
| 컬럼명 | 타입 | 제약조건 | 설명 |
|
||||
|--------|------|----------|------|
|
||||
| id | VARCHAR(36) | PRIMARY KEY | 요약 결과 고유 ID |
|
||||
| meeting_id | VARCHAR(50) | NOT NULL, FK | 회의 ID |
|
||||
| summary_type | VARCHAR(50) | NOT NULL | 요약 타입 |
|
||||
| source_minutes_ids | JSON | NOT NULL | 통합에 사용된 회의록 ID 배열 |
|
||||
| result | JSON | NOT NULL | AI 응답 전체 결과 |
|
||||
| processing_time_ms | INT | NULL | AI 처리 시간 (밀리초) |
|
||||
| model_version | VARCHAR(50) | DEFAULT 'claude-3.5-sonnet' | AI 모델 버전 |
|
||||
| keywords | JSON | NULL | 주요 키워드 배열 |
|
||||
| statistics | JSON | NULL | 통계 정보 |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 생성 시간 |
|
||||
|
||||
#### summary_type 값
|
||||
- `CONSOLIDATED`: 통합 회의록 요약
|
||||
- `TODO_EXTRACTION`: Todo 자동 추출
|
||||
|
||||
#### 외래키
|
||||
- `fk_ai_summaries_meeting`: meetings(meeting_id) ON DELETE CASCADE
|
||||
|
||||
#### 인덱스
|
||||
- `idx_summaries_meeting`: (meeting_id)
|
||||
- `idx_summaries_type`: (meeting_id, summary_type)
|
||||
- `idx_summaries_created`: (created_at)
|
||||
|
||||
#### JSON 데이터 구조
|
||||
|
||||
**source_minutes_ids**:
|
||||
```json
|
||||
[
|
||||
"minutes-uuid-1",
|
||||
"minutes-uuid-2",
|
||||
"minutes-uuid-3"
|
||||
]
|
||||
```
|
||||
|
||||
**result** (CONSOLIDATED 타입):
|
||||
```json
|
||||
{
|
||||
"agendaSections": [
|
||||
{
|
||||
"agendaNumber": 1,
|
||||
"aiSummaryShort": "타겟 고객을 20-30대로 설정...",
|
||||
"discussions": "신제품의 주요 타겟 고객층...",
|
||||
"decisions": ["타겟 고객: 20-30대", "UI/UX 개선"],
|
||||
"pendingItems": [],
|
||||
"opinions": [
|
||||
{"speaker": "김민준", "opinion": "..."}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**result** (TODO_EXTRACTION 타입):
|
||||
```json
|
||||
{
|
||||
"todos": [
|
||||
{
|
||||
"content": "시장 조사 보고서 작성",
|
||||
"assignee": "김민준",
|
||||
"dueDate": "2025-11-01",
|
||||
"priority": "HIGH",
|
||||
"sectionReference": "안건 1",
|
||||
"confidence": 0.92
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**keywords**:
|
||||
```json
|
||||
[
|
||||
"신제품기획",
|
||||
"예산편성",
|
||||
"일정조율",
|
||||
"시장조사",
|
||||
"UI/UX"
|
||||
]
|
||||
```
|
||||
|
||||
**statistics**:
|
||||
```json
|
||||
{
|
||||
"participantsCount": 4,
|
||||
"durationMinutes": 90,
|
||||
"agendasCount": 3,
|
||||
"todosCount": 5
|
||||
}
|
||||
```
|
||||
|
||||
#### 사용 패턴
|
||||
```sql
|
||||
-- 캐시된 통합 요약 조회
|
||||
SELECT * FROM ai_summaries
|
||||
WHERE meeting_id = 'meeting-123'
|
||||
AND summary_type = 'CONSOLIDATED'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- 성능 통계 조회
|
||||
SELECT
|
||||
summary_type,
|
||||
AVG(processing_time_ms) as avg_time,
|
||||
MAX(processing_time_ms) as max_time
|
||||
FROM ai_summaries
|
||||
GROUP BY summary_type;
|
||||
|
||||
-- JSON 필드 쿼리
|
||||
SELECT
|
||||
meeting_id,
|
||||
keywords,
|
||||
statistics->>'participantsCount' as participants,
|
||||
statistics->>'todosCount' as todos
|
||||
FROM ai_summaries
|
||||
WHERE summary_type = 'CONSOLIDATED';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. todos (확장)
|
||||
**설명**: AI 자동 추출 정보 추가
|
||||
|
||||
#### 새로 추가된 컬럼
|
||||
| 컬럼명 | 타입 | 제약조건 | 설명 |
|
||||
|--------|------|----------|------|
|
||||
| extracted_by | VARCHAR(50) | DEFAULT 'AI' | Todo 추출 방법 |
|
||||
| section_reference | VARCHAR(200) | NULL | 관련 회의록 섹션 참조 |
|
||||
| extraction_confidence | DECIMAL(3,2) | DEFAULT 0.00 | AI 추출 신뢰도 점수 |
|
||||
|
||||
#### extracted_by 값
|
||||
- `AI`: AI 자동 추출
|
||||
- `MANUAL`: 사용자 수동 작성
|
||||
|
||||
#### 인덱스
|
||||
- `idx_todos_extracted`: (extracted_by)
|
||||
- `idx_todos_meeting`: (meeting_id)
|
||||
|
||||
#### 사용 패턴
|
||||
```sql
|
||||
-- AI 추출 Todo 조회
|
||||
SELECT * FROM todos
|
||||
WHERE meeting_id = 'meeting-123'
|
||||
AND extracted_by = 'AI'
|
||||
ORDER BY extraction_confidence DESC;
|
||||
|
||||
-- 신뢰도 높은 Todo 조회
|
||||
SELECT * FROM todos
|
||||
WHERE meeting_id = 'meeting-123'
|
||||
AND extracted_by = 'AI'
|
||||
AND extraction_confidence >= 0.80;
|
||||
|
||||
-- 안건별 Todo 조회
|
||||
SELECT * FROM todos
|
||||
WHERE meeting_id = 'meeting-123'
|
||||
AND section_reference = '안건 1';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 데이터 플로우
|
||||
|
||||
### 회의 종료 시 데이터 저장 순서
|
||||
|
||||
```
|
||||
1. 참석자별 회의록 저장 (minutes)
|
||||
├─ user_id: 각 참석자 ID
|
||||
├─ is_consolidated: false
|
||||
└─ section_type: 'PARTICIPANT'
|
||||
|
||||
2. AI Service 호출 → 통합 요약 생성
|
||||
|
||||
3. 통합 회의록 저장 (minutes)
|
||||
├─ is_consolidated: true
|
||||
├─ section_type: 'CONSOLIDATED'
|
||||
└─ consolidated_by: 'AI'
|
||||
|
||||
4. 안건별 섹션 저장 (agenda_sections)
|
||||
├─ meeting_id: 회의 ID
|
||||
├─ minutes_id: 통합 회의록 ID
|
||||
└─ AI 요약 결과 (discussions, decisions, opinions 등)
|
||||
|
||||
5. AI 요약 결과 캐싱 (ai_summaries)
|
||||
├─ summary_type: 'CONSOLIDATED'
|
||||
├─ source_minutes_ids: 참석자 회의록 ID 배열
|
||||
└─ result: 전체 AI 응답 (JSON)
|
||||
|
||||
6. Todo 자동 추출 및 저장 (todos)
|
||||
├─ extracted_by: 'AI'
|
||||
├─ section_reference: 관련 안건
|
||||
└─ extraction_confidence: 신뢰도 점수
|
||||
|
||||
7. 통계 정보 캐싱 (ai_summaries)
|
||||
├─ keywords: 주요 키워드 배열
|
||||
└─ statistics: 참석자 수, 안건 수, Todo 수 등
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 ERD (Entity Relationship Diagram)
|
||||
|
||||
```
|
||||
meetings
|
||||
├─ 1:N → minutes (참석자별 회의록)
|
||||
│ ├─ user_id (참석자)
|
||||
│ └─ is_consolidated = false
|
||||
│
|
||||
├─ 1:1 → minutes (통합 회의록)
|
||||
│ └─ is_consolidated = true
|
||||
│
|
||||
├─ 1:N → agenda_sections (안건별 섹션)
|
||||
│ ├─ FK: minutes_id (통합 회의록)
|
||||
│ └─ FK: meeting_id
|
||||
│
|
||||
├─ 1:N → ai_summaries (AI 요약 캐시)
|
||||
│ ├─ summary_type: CONSOLIDATED | TODO_EXTRACTION
|
||||
│ └─ source_minutes_ids (JSON)
|
||||
│
|
||||
└─ 1:N → todos (할일)
|
||||
├─ extracted_by: AI | MANUAL
|
||||
├─ section_reference
|
||||
└─ extraction_confidence
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 주요 쿼리 예시
|
||||
|
||||
### 1. 회의 종료 후 전체 데이터 조회
|
||||
```sql
|
||||
-- 회의 기본 정보 + 통계
|
||||
SELECT
|
||||
m.meeting_id,
|
||||
m.title,
|
||||
m.status,
|
||||
s.statistics->>'participantsCount' as participants,
|
||||
s.statistics->>'durationMinutes' as duration,
|
||||
s.statistics->>'agendasCount' as agendas,
|
||||
s.statistics->>'todosCount' as todos,
|
||||
s.keywords
|
||||
FROM meetings m
|
||||
LEFT JOIN ai_summaries s ON m.meeting_id = s.meeting_id
|
||||
AND s.summary_type = 'CONSOLIDATED'
|
||||
WHERE m.meeting_id = 'meeting-123';
|
||||
|
||||
-- 안건별 섹션 + Todo
|
||||
SELECT
|
||||
a.agenda_number,
|
||||
a.agenda_title,
|
||||
a.ai_summary_short,
|
||||
a.discussions,
|
||||
a.decisions,
|
||||
a.pending_items,
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'content', t.content,
|
||||
'assignee', t.assignee,
|
||||
'dueDate', t.due_date,
|
||||
'priority', t.priority,
|
||||
'confidence', t.extraction_confidence
|
||||
)
|
||||
) as todos
|
||||
FROM agenda_sections a
|
||||
LEFT JOIN todos t ON a.meeting_id = t.meeting_id
|
||||
AND t.section_reference = CONCAT('안건 ', a.agenda_number)
|
||||
WHERE a.meeting_id = 'meeting-123'
|
||||
GROUP BY a.id, a.agenda_number, a.agenda_title,
|
||||
a.ai_summary_short, a.discussions, a.decisions, a.pending_items
|
||||
ORDER BY a.agenda_number;
|
||||
```
|
||||
|
||||
### 2. 참석자별 회의록 vs AI 통합 회의록 비교
|
||||
```sql
|
||||
SELECT
|
||||
'PARTICIPANT' as type,
|
||||
user_id,
|
||||
content,
|
||||
created_at
|
||||
FROM minutes
|
||||
WHERE meeting_id = 'meeting-123'
|
||||
AND is_consolidated = false
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'CONSOLIDATED' as type,
|
||||
'AI' as user_id,
|
||||
content,
|
||||
created_at
|
||||
FROM minutes
|
||||
WHERE meeting_id = 'meeting-123'
|
||||
AND is_consolidated = true
|
||||
ORDER BY type, created_at;
|
||||
```
|
||||
|
||||
### 3. AI 성능 모니터링
|
||||
```sql
|
||||
-- AI 처리 시간 통계
|
||||
SELECT
|
||||
summary_type,
|
||||
COUNT(*) as total_count,
|
||||
AVG(processing_time_ms) as avg_time_ms,
|
||||
MIN(processing_time_ms) as min_time_ms,
|
||||
MAX(processing_time_ms) as max_time_ms,
|
||||
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY processing_time_ms) as p95_time_ms
|
||||
FROM ai_summaries
|
||||
WHERE created_at >= CURRENT_DATE - INTERVAL '7 days'
|
||||
GROUP BY summary_type;
|
||||
|
||||
-- Todo 추출 정확도 (신뢰도 분포)
|
||||
SELECT
|
||||
CASE
|
||||
WHEN extraction_confidence >= 0.90 THEN 'HIGH (>=0.90)'
|
||||
WHEN extraction_confidence >= 0.70 THEN 'MEDIUM (0.70-0.89)'
|
||||
ELSE 'LOW (<0.70)'
|
||||
END as confidence_level,
|
||||
COUNT(*) as count
|
||||
FROM todos
|
||||
WHERE extracted_by = 'AI'
|
||||
AND created_at >= CURRENT_DATE - INTERVAL '7 days'
|
||||
GROUP BY confidence_level
|
||||
ORDER BY confidence_level DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 마이그레이션 실행 방법
|
||||
|
||||
### 1. 로컬 환경
|
||||
```bash
|
||||
# Flyway 마이그레이션 (Spring Boot)
|
||||
./gradlew flywayMigrate
|
||||
|
||||
# 또는 직접 SQL 실행
|
||||
psql -h localhost -U postgres -d hgzero -f meeting/src/main/resources/db/migration/V3__add_meeting_end_support.sql
|
||||
```
|
||||
|
||||
### 2. Docker Compose 환경
|
||||
```bash
|
||||
# 컨테이너 재시작 (자동 마이그레이션)
|
||||
docker-compose down
|
||||
docker-compose up -d meeting-service
|
||||
|
||||
# 마이그레이션 로그 확인
|
||||
docker-compose logs -f meeting-service | grep "Flyway"
|
||||
```
|
||||
|
||||
### 3. 프로덕션 환경
|
||||
```bash
|
||||
# 1. 백업
|
||||
pg_dump -h prod-db-host -U postgres -d hgzero > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||
|
||||
# 2. 마이그레이션 실행
|
||||
psql -h prod-db-host -U postgres -d hgzero -f V3__add_meeting_end_support.sql
|
||||
|
||||
# 3. 검증
|
||||
psql -h prod-db-host -U postgres -d hgzero -c "\d+ agenda_sections"
|
||||
psql -h prod-db-host -U postgres -d hgzero -c "\d+ ai_summaries"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 검증 체크리스트
|
||||
|
||||
### 마이그레이션 후 검증
|
||||
- [ ] `minutes` 테이블에 새 컬럼 추가 확인
|
||||
- [ ] `agenda_sections` 테이블 생성 확인
|
||||
- [ ] `ai_summaries` 테이블 생성 확인
|
||||
- [ ] `todos` 테이블 확장 확인
|
||||
- [ ] 모든 인덱스 생성 확인
|
||||
- [ ] 외래키 제약조건 확인
|
||||
- [ ] 트리거 생성 확인 (updated_at)
|
||||
|
||||
### 성능 검증
|
||||
- [ ] 참석자별 회의록 조회 성능 (인덱스 활용)
|
||||
- [ ] 안건별 섹션 조회 성능
|
||||
- [ ] AI 캐시 조회 성능 (<100ms)
|
||||
- [ ] JSON 필드 쿼리 성능
|
||||
|
||||
---
|
||||
|
||||
## 📝 참고 사항
|
||||
|
||||
### JSON 필드 쿼리 (PostgreSQL)
|
||||
```sql
|
||||
-- JSON 배열 길이
|
||||
SELECT jsonb_array_length(decisions::jsonb) FROM agenda_sections;
|
||||
|
||||
-- JSON 필드 추출
|
||||
SELECT
|
||||
keywords->0 as first_keyword,
|
||||
statistics->>'todosCount' as todo_count
|
||||
FROM ai_summaries;
|
||||
|
||||
-- JSON 배열 펼치기
|
||||
SELECT
|
||||
agenda_title,
|
||||
jsonb_array_elements_text(decisions::jsonb) as decision
|
||||
FROM agenda_sections;
|
||||
```
|
||||
|
||||
### 성능 최적화 팁
|
||||
1. **캐싱 활용**: ai_summaries 테이블을 우선 조회하여 불필요한 AI 호출 방지
|
||||
2. **인덱스 활용**: meeting_id + summary_type 복합 인덱스로 빠른 조회
|
||||
3. **JSON 필드**: PostgreSQL의 jsonb 타입 사용 권장 (인덱스 지원)
|
||||
4. **파티셔닝**: 대용량 데이터의 경우 created_at 기준 파티셔닝 고려
|
||||
151
docs/ERD-회의종료.puml
Normal file
151
docs/ERD-회의종료.puml
Normal file
@ -0,0 +1,151 @@
|
||||
@startuml ERD-회의종료
|
||||
!theme mono
|
||||
|
||||
' ========================================
|
||||
' 회의종료 기능 ERD
|
||||
' ========================================
|
||||
|
||||
!define TABLE(name,desc) class name as "desc" << (T,#FFAAAA) >>
|
||||
!define PRIMARY_KEY(x) <b>PK: x</b>
|
||||
!define FOREIGN_KEY(x) <i>FK: x</i>
|
||||
!define NOT_NULL(x) <u>x</u>
|
||||
!define UNIQUE(x) <color:blue>x</color>
|
||||
|
||||
' 기존 테이블
|
||||
TABLE(meetings, "meetings\n회의") {
|
||||
PRIMARY_KEY(meeting_id: VARCHAR(50))
|
||||
--
|
||||
title: VARCHAR(200)
|
||||
start_time: TIMESTAMP
|
||||
end_time: TIMESTAMP
|
||||
status: VARCHAR(20)
|
||||
created_at: TIMESTAMP
|
||||
updated_at: TIMESTAMP
|
||||
}
|
||||
|
||||
TABLE(meeting_participants, "meeting_participants\n회의 참석자") {
|
||||
PRIMARY_KEY(meeting_id, user_id)
|
||||
--
|
||||
FOREIGN_KEY(meeting_id: VARCHAR(50))
|
||||
FOREIGN_KEY(user_id: VARCHAR(100))
|
||||
invitation_status: VARCHAR(20)
|
||||
attended: BOOLEAN
|
||||
created_at: TIMESTAMP
|
||||
}
|
||||
|
||||
' 확장된 minutes 테이블
|
||||
TABLE(minutes, "minutes\n회의록") {
|
||||
PRIMARY_KEY(id: VARCHAR(36))
|
||||
--
|
||||
FOREIGN_KEY(meeting_id: VARCHAR(50))
|
||||
content: TEXT
|
||||
NOT_NULL(user_id: VARCHAR(100))
|
||||
NOT_NULL(is_consolidated: BOOLEAN)
|
||||
consolidated_by: VARCHAR(255)
|
||||
section_type: VARCHAR(50)
|
||||
created_at: TIMESTAMP
|
||||
updated_at: TIMESTAMP
|
||||
}
|
||||
|
||||
' 신규 테이블: agenda_sections
|
||||
TABLE(agenda_sections, "agenda_sections\n안건별 섹션") {
|
||||
PRIMARY_KEY(id: VARCHAR(36))
|
||||
--
|
||||
FOREIGN_KEY(minutes_id: VARCHAR(36))
|
||||
FOREIGN_KEY(meeting_id: VARCHAR(50))
|
||||
NOT_NULL(agenda_number: INT)
|
||||
NOT_NULL(agenda_title: VARCHAR(200))
|
||||
ai_summary_short: TEXT
|
||||
discussions: TEXT
|
||||
decisions: JSON
|
||||
pending_items: JSON
|
||||
opinions: JSON
|
||||
created_at: TIMESTAMP
|
||||
updated_at: TIMESTAMP
|
||||
}
|
||||
|
||||
' 신규 테이블: ai_summaries
|
||||
TABLE(ai_summaries, "ai_summaries\nAI 요약 캐시") {
|
||||
PRIMARY_KEY(id: VARCHAR(36))
|
||||
--
|
||||
FOREIGN_KEY(meeting_id: VARCHAR(50))
|
||||
NOT_NULL(summary_type: VARCHAR(50))
|
||||
NOT_NULL(source_minutes_ids: JSON)
|
||||
NOT_NULL(result: JSON)
|
||||
processing_time_ms: INT
|
||||
model_version: VARCHAR(50)
|
||||
keywords: JSON
|
||||
statistics: JSON
|
||||
created_at: TIMESTAMP
|
||||
}
|
||||
|
||||
' 확장된 todos 테이블
|
||||
TABLE(todos, "todos\n할일") {
|
||||
PRIMARY_KEY(id: VARCHAR(36))
|
||||
--
|
||||
FOREIGN_KEY(meeting_id: VARCHAR(50))
|
||||
content: TEXT
|
||||
assignee: VARCHAR(100)
|
||||
due_date: DATE
|
||||
priority: VARCHAR(20)
|
||||
status: VARCHAR(20)
|
||||
NOT_NULL(extracted_by: VARCHAR(50))
|
||||
section_reference: VARCHAR(200)
|
||||
extraction_confidence: DECIMAL(3,2)
|
||||
created_at: TIMESTAMP
|
||||
updated_at: TIMESTAMP
|
||||
}
|
||||
|
||||
' 관계 정의
|
||||
meetings "1" --> "N" minutes : "has"
|
||||
meetings "1" --> "N" meeting_participants : "has"
|
||||
meetings "1" --> "N" agenda_sections : "has"
|
||||
meetings "1" --> "N" ai_summaries : "has"
|
||||
meetings "1" --> "N" todos : "has"
|
||||
|
||||
minutes "1" --> "N" agenda_sections : "contains"
|
||||
minutes "1" ..> "1" meeting_participants : "written by\n(user_id)"
|
||||
|
||||
' 노트 추가
|
||||
note right of minutes
|
||||
**참석자별 회의록**
|
||||
- is_consolidated = false
|
||||
- section_type = 'PARTICIPANT'
|
||||
- user_id: 작성자
|
||||
|
||||
**AI 통합 회의록**
|
||||
- is_consolidated = true
|
||||
- section_type = 'CONSOLIDATED'
|
||||
- user_id: NULL
|
||||
end note
|
||||
|
||||
note right of agenda_sections
|
||||
**JSON 필드 구조**
|
||||
- decisions: ["결정1", "결정2"]
|
||||
- pending_items: ["보류1"]
|
||||
- opinions: [
|
||||
{"speaker": "김민준", "opinion": "..."}
|
||||
]
|
||||
end note
|
||||
|
||||
note right of ai_summaries
|
||||
**summary_type**
|
||||
- CONSOLIDATED: 통합 요약
|
||||
- TODO_EXTRACTION: Todo 추출
|
||||
|
||||
**캐싱 전략**
|
||||
- 재조회 시 DB 우선 조회
|
||||
- 처리 시간 <0.5초
|
||||
end note
|
||||
|
||||
note right of todos
|
||||
**extracted_by**
|
||||
- AI: AI 자동 추출
|
||||
- MANUAL: 사용자 수동 작성
|
||||
|
||||
**extraction_confidence**
|
||||
- 0.00 ~ 1.00
|
||||
- 0.80 이상: 신뢰도 높음
|
||||
end note
|
||||
|
||||
@enduml
|
||||
107
docs/setup-eventhub-policy.md
Normal file
107
docs/setup-eventhub-policy.md
Normal file
@ -0,0 +1,107 @@
|
||||
# Event Hub 정책 설정 가이드
|
||||
|
||||
## 📌 개요
|
||||
Azure Event Hub 사용을 위한 공유 액세스 정책(SAS Policy) 설정 방법
|
||||
|
||||
## 🔧 설정 단계
|
||||
|
||||
### 1. Azure Portal 접속
|
||||
- Event Hub Namespace: `hgzero-eventhub-ns`
|
||||
- 리소스 그룹: `rg-digitalgarage-02`
|
||||
|
||||
### 2. 정책 생성
|
||||
|
||||
#### (1) STT 서비스용 Send 정책
|
||||
```
|
||||
정책 이름: stt-send-policy
|
||||
권한: ✓ Send
|
||||
용도: STT 서비스가 음성 인식 결과를 Event Hub에 발행
|
||||
```
|
||||
|
||||
**생성 방법**:
|
||||
1. `hgzero-eventhub-ns` → 공유 액세스 정책
|
||||
2. `+ 추가` 클릭
|
||||
3. 정책 이름: `stt-send-policy`
|
||||
4. **Send** 체크
|
||||
5. **만들기** 클릭
|
||||
6. 생성된 정책 클릭 → **연결 문자열-기본 키** 복사
|
||||
|
||||
#### (2) AI 서비스용 Listen 정책
|
||||
```
|
||||
정책 이름: ai-listen-policy
|
||||
권한: ✓ Listen
|
||||
용도: AI 서비스가 Event Hub에서 이벤트를 구독
|
||||
```
|
||||
|
||||
**생성 방법**:
|
||||
1. 공유 액세스 정책 → `+ 추가`
|
||||
2. 정책 이름: `ai-listen-policy`
|
||||
3. **Listen** 체크
|
||||
4. **만들기** → 연결 문자열 복사
|
||||
|
||||
#### (3) 관리용 Manage 정책
|
||||
```
|
||||
정책 이름: hgzero-manage-policy
|
||||
권한: ✓ Manage (Send + Listen 포함)
|
||||
용도: 개발, 테스트, 관리 작업
|
||||
```
|
||||
|
||||
**생성 방법**:
|
||||
1. 공유 액세스 정책 → `+ 추가`
|
||||
2. 정책 이름: `hgzero-manage-policy`
|
||||
3. **Manage** 체크 (자동으로 Send/Listen 포함)
|
||||
4. **만들기** → 연결 문자열 복사
|
||||
|
||||
### 3. 연결 문자열 형식
|
||||
```
|
||||
Endpoint=sb://hgzero-eventhub-ns.servicebus.windows.net/;
|
||||
SharedAccessKeyName=stt-send-policy;
|
||||
SharedAccessKey=<YOUR_KEY>;
|
||||
EntityPath=hgzero-eventhub-name
|
||||
```
|
||||
|
||||
### 4. IntelliJ 실행 프로파일 설정
|
||||
|
||||
#### STT 서비스 (`.run/SttServiceApplication.run.xml`)
|
||||
```xml
|
||||
<option name="env">
|
||||
<map>
|
||||
<entry key="EVENTHUB_CONNECTION_STRING" value="Endpoint=sb://...;SharedAccessKeyName=stt-send-policy;SharedAccessKey=..."/>
|
||||
<entry key="EVENTHUB_NAME" value="hgzero-eventhub-name"/>
|
||||
</map>
|
||||
</option>
|
||||
```
|
||||
|
||||
#### AI 서비스 (`.run/AiServiceApplication.run.xml`)
|
||||
```xml
|
||||
<option name="env">
|
||||
<map>
|
||||
<entry key="EVENTHUB_CONNECTION_STRING" value="Endpoint=sb://...;SharedAccessKeyName=ai-listen-policy;SharedAccessKey=..."/>
|
||||
<entry key="EVENTHUB_NAME" value="hgzero-eventhub-name"/>
|
||||
</map>
|
||||
</option>
|
||||
```
|
||||
|
||||
## ✅ 검증 방법
|
||||
|
||||
### 연결 테스트
|
||||
```bash
|
||||
# STT 서비스 시작 후 로그 확인
|
||||
# 성공 메시지 예시:
|
||||
[INFO] EventHubProducerClient - Connected to Event Hub: hgzero-eventhub-name
|
||||
```
|
||||
|
||||
### 권한 검증
|
||||
- **Send 정책**: 이벤트 발행만 가능
|
||||
- **Listen 정책**: 이벤트 구독만 가능
|
||||
- **Manage 정책**: 모든 작업 가능
|
||||
|
||||
## 🔐 보안 권장사항
|
||||
|
||||
1. **최소 권한 원칙**: 각 서비스는 필요한 권한만 부여
|
||||
2. **키 회전**: 정기적으로 액세스 키 재생성
|
||||
3. **환경 분리**: Dev/Prod 환경별 별도 정책 사용
|
||||
4. **연결 문자열 보호**: Git에 커밋하지 않음 (환경 변수 사용)
|
||||
|
||||
## 📚 참고 문서
|
||||
- [Azure Event Hub SAS 인증](https://learn.microsoft.com/azure/event-hubs/authenticate-shared-access-signature)
|
||||
360
docs/회의종료-개발계획.md
Normal file
360
docs/회의종료-개발계획.md
Normal file
@ -0,0 +1,360 @@
|
||||
# 회의종료 기능 개발 계획
|
||||
|
||||
## 📋 개요
|
||||
회의가 종료되면 모든 참석자의 회의록을 수집하여 Claude AI가 통합 요약하고 Todo를 자동 추출하는 기능
|
||||
|
||||
## 🎯 핵심 기능
|
||||
1. **참석자별 회의록 통합**: 모든 참석자가 작성한 회의록을 DB에서 조회
|
||||
2. **AI 통합 요약**: Claude AI가 안건별로 요약 및 구조화
|
||||
3. **Todo 자동 추출**: AI가 회의록에서 액션 아이템 자동 추출
|
||||
4. **통계 생성**: 참석자 수, 회의 시간, 안건 수, Todo 수 통계
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ 데이터베이스 스키마 설계
|
||||
|
||||
### 1. minutes (회의록 테이블) - 확장
|
||||
```sql
|
||||
-- 기존 테이블 확장
|
||||
ALTER TABLE minutes ADD COLUMN user_id VARCHAR(50); -- 작성자 ID
|
||||
ALTER TABLE minutes ADD COLUMN is_consolidated BOOLEAN DEFAULT FALSE; -- 통합 회의록 여부
|
||||
ALTER TABLE minutes ADD COLUMN consolidated_by VARCHAR(255); -- 통합 처리자 (AI)
|
||||
|
||||
-- 인덱스 추가
|
||||
CREATE INDEX idx_minutes_meeting_user ON minutes(meeting_id, user_id);
|
||||
CREATE INDEX idx_minutes_consolidated ON minutes(is_consolidated);
|
||||
```
|
||||
|
||||
### 2. agenda_sections (안건별 섹션 테이블) - 신규
|
||||
```sql
|
||||
CREATE TABLE agenda_sections (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
minutes_id VARCHAR(36) NOT NULL,
|
||||
meeting_id VARCHAR(36) NOT NULL,
|
||||
agenda_number INT NOT NULL, -- 안건 번호 (1, 2, 3...)
|
||||
agenda_title VARCHAR(200) NOT NULL, -- 안건 제목
|
||||
|
||||
-- AI 요약 결과
|
||||
ai_summary_short TEXT, -- 짧은 요약 (1줄)
|
||||
discussion TEXT, -- 논의 사항
|
||||
decisions JSON, -- 결정 사항 배열
|
||||
pending_items JSON, -- 보류 사항 배열
|
||||
opinions JSON, -- 참석자별 의견 배열
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (minutes_id) REFERENCES minutes(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (meeting_id) REFERENCES meetings(id) ON DELETE CASCADE,
|
||||
|
||||
INDEX idx_sections_meeting (meeting_id),
|
||||
INDEX idx_sections_agenda (meeting_id, agenda_number)
|
||||
);
|
||||
```
|
||||
|
||||
### 3. ai_summaries (AI 요약 결과 캐시) - 신규
|
||||
```sql
|
||||
CREATE TABLE ai_summaries (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
meeting_id VARCHAR(36) NOT NULL,
|
||||
summary_type VARCHAR(50) NOT NULL, -- 'CONSOLIDATED', 'TODO_EXTRACTION'
|
||||
|
||||
-- 입력 정보
|
||||
source_minutes_ids JSON NOT NULL, -- 통합에 사용된 회의록 ID 배열
|
||||
|
||||
-- AI 처리 결과
|
||||
result JSON NOT NULL, -- AI 응답 전체 결과
|
||||
processing_time_ms INT, -- 처리 시간 (밀리초)
|
||||
model_version VARCHAR(50), -- 사용한 AI 모델 버전
|
||||
|
||||
-- 통계
|
||||
keywords JSON, -- 주요 키워드 배열
|
||||
statistics JSON, -- 통계 정보 (참석자 수, 안건 수 등)
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (meeting_id) REFERENCES meetings(id) ON DELETE CASCADE,
|
||||
|
||||
INDEX idx_summaries_meeting (meeting_id),
|
||||
INDEX idx_summaries_type (meeting_id, summary_type)
|
||||
);
|
||||
```
|
||||
|
||||
### 4. todos (Todo 테이블) - 확장
|
||||
```sql
|
||||
-- 기존 테이블 확장
|
||||
ALTER TABLE todos ADD COLUMN extracted_by VARCHAR(50) DEFAULT 'AI'; -- 'AI' 또는 'MANUAL'
|
||||
ALTER TABLE todos ADD COLUMN section_reference VARCHAR(200); -- 관련 안건 참조
|
||||
ALTER TABLE todos ADD COLUMN extraction_confidence DECIMAL(3,2); -- AI 추출 신뢰도 (0.00~1.00)
|
||||
|
||||
-- 인덱스 추가
|
||||
CREATE INDEX idx_todos_extracted ON todos(extracted_by);
|
||||
CREATE INDEX idx_todos_meeting ON todos(meeting_id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔌 API 설계
|
||||
|
||||
### Meeting Service API
|
||||
|
||||
#### 1. 참석자별 회의록 조회
|
||||
```yaml
|
||||
GET /meetings/{meetingId}/minutes/by-participants
|
||||
Response:
|
||||
participantMinutes:
|
||||
- userId: "user1"
|
||||
userName: "김민준"
|
||||
minutesId: "uuid"
|
||||
content: "회의록 내용..."
|
||||
status: "DRAFT"
|
||||
- userId: "user2"
|
||||
userName: "박서연"
|
||||
...
|
||||
```
|
||||
|
||||
#### 2. 안건별 섹션 조회
|
||||
```yaml
|
||||
GET /meetings/{meetingId}/agenda-sections
|
||||
Response:
|
||||
sections:
|
||||
- agendaNumber: 1
|
||||
agendaTitle: "신제품 기획 방향성"
|
||||
aiSummaryShort: "타겟 고객을 20-30대로 설정..."
|
||||
discussions: "..."
|
||||
decisions: [...]
|
||||
todos: [...]
|
||||
```
|
||||
|
||||
#### 3. 회의 통계 조회
|
||||
```yaml
|
||||
GET /meetings/{meetingId}/statistics
|
||||
Response:
|
||||
participantsCount: 4
|
||||
durationMinutes: 90
|
||||
agendasCount: 3
|
||||
todosCount: 5
|
||||
keywords: ["신제품기획", "예산편성", ...]
|
||||
```
|
||||
|
||||
### AI Service API
|
||||
|
||||
#### 1. 통합 회의록 요약 API (신규)
|
||||
```yaml
|
||||
POST /transcripts/consolidate
|
||||
Request:
|
||||
meetingId: "uuid"
|
||||
participantMinutes:
|
||||
- userId: "user1"
|
||||
content: "회의록 내용..."
|
||||
- userId: "user2"
|
||||
content: "회의록 내용..."
|
||||
agendas:
|
||||
- number: 1
|
||||
title: "신제품 기획 방향성"
|
||||
|
||||
Response:
|
||||
consolidatedMinutesId: "uuid"
|
||||
agendaSections:
|
||||
- agendaNumber: 1
|
||||
aiSummaryShort: "타겟 고객을 20-30대로 설정..."
|
||||
discussions: "..."
|
||||
decisions: [...]
|
||||
pendingItems: [...]
|
||||
keywords: ["신제품기획", "예산편성"]
|
||||
processingTimeMs: 3500
|
||||
```
|
||||
|
||||
#### 2. Todo 자동 추출 API (기존)
|
||||
```yaml
|
||||
POST /todos/extract
|
||||
Request:
|
||||
meetingId: "uuid"
|
||||
minutesContent: "통합된 회의록 전체 내용..."
|
||||
|
||||
Response:
|
||||
todos:
|
||||
- content: "시장 조사 보고서 작성"
|
||||
assignee: "김민준"
|
||||
dueDate: "2025-11-01"
|
||||
priority: "HIGH"
|
||||
sectionReference: "안건 1"
|
||||
confidence: 0.92
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🤖 Claude AI 프롬프트 설계
|
||||
|
||||
### 1. 통합 회의록 요약 프롬프트
|
||||
```
|
||||
당신은 회의록 통합 전문가입니다.
|
||||
|
||||
입력:
|
||||
- 회의 제목: {meetingTitle}
|
||||
- 안건 목록: {agendas}
|
||||
- 참석자별 회의록:
|
||||
* {userName1}: {content1}
|
||||
* {userName2}: {content2}
|
||||
...
|
||||
|
||||
작업:
|
||||
각 안건별로 다음을 생성하세요:
|
||||
1. 짧은 요약 (1줄, 20자 이내)
|
||||
2. 논의 사항 (핵심 내용 3-5문장)
|
||||
3. 결정 사항 (배열 형태)
|
||||
4. 보류 사항 (배열 형태)
|
||||
5. 참석자별 의견 (speaker, opinion)
|
||||
|
||||
출력 형식 (JSON):
|
||||
{
|
||||
"agendaSections": [
|
||||
{
|
||||
"agendaNumber": 1,
|
||||
"aiSummaryShort": "...",
|
||||
"discussions": "...",
|
||||
"decisions": ["결정1", "결정2"],
|
||||
"pendingItems": ["보류1"],
|
||||
"opinions": [
|
||||
{"speaker": "김민준", "opinion": "..."}
|
||||
]
|
||||
}
|
||||
],
|
||||
"keywords": ["키워드1", "키워드2", ...]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Todo 자동 추출 프롬프트
|
||||
```
|
||||
당신은 Todo 추출 전문가입니다.
|
||||
|
||||
입력:
|
||||
- 회의록 전체 내용: {minutesContent}
|
||||
- 참석자 목록: {participants}
|
||||
|
||||
작업:
|
||||
회의록에서 액션 아이템(Todo)을 추출하세요.
|
||||
- "~하기로 함", "~가 작성", "~까지 완료" 등의 패턴 탐지
|
||||
- 담당자와 마감일 식별
|
||||
- 우선순위 판단 (HIGH/MEDIUM/LOW)
|
||||
|
||||
출력 형식 (JSON):
|
||||
{
|
||||
"todos": [
|
||||
{
|
||||
"content": "시장 조사 보고서 작성",
|
||||
"assignee": "김민준",
|
||||
"dueDate": "2025-11-01",
|
||||
"priority": "HIGH",
|
||||
"sectionReference": "안건 1",
|
||||
"confidence": 0.92
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 통합 플로우
|
||||
|
||||
### 회의 종료 처리 시퀀스
|
||||
```
|
||||
1. 사용자가 "회의 종료" 버튼 클릭
|
||||
↓
|
||||
2. Meeting Service: 회의 상태를 'ENDED'로 변경
|
||||
↓
|
||||
3. Meeting Service: 모든 참석자의 회의록 조회 (GET /meetings/{meetingId}/minutes/by-participants)
|
||||
↓
|
||||
4. AI Service 호출: 통합 요약 요청 (POST /transcripts/consolidate)
|
||||
↓
|
||||
5. Claude AI: 안건별 요약 및 구조화
|
||||
↓
|
||||
6. AI Service: agenda_sections 테이블에 저장
|
||||
↓
|
||||
7. AI Service 호출: Todo 자동 추출 (POST /todos/extract)
|
||||
↓
|
||||
8. Claude AI: Todo 추출 및 담당자 식별
|
||||
↓
|
||||
9. Meeting Service: todos 테이블에 저장
|
||||
↓
|
||||
10. Meeting Service: ai_summaries 테이블에 캐시 저장
|
||||
↓
|
||||
11. 프론트엔드: 07-회의종료.html 화면 렌더링
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 성능 고려사항
|
||||
|
||||
### 1. 처리 시간 목표
|
||||
- AI 통합 요약: 3-5초 이내
|
||||
- Todo 추출: 2-3초 이내
|
||||
- 전체 회의 종료 처리: 10초 이내
|
||||
|
||||
### 2. 최적화 방안
|
||||
- **병렬 처리**: 통합 요약과 Todo 추출을 병렬로 실행
|
||||
- **캐싱**: ai_summaries 테이블에 결과 캐싱 (재조회 시 0.5초 이내)
|
||||
- **비동기 처리**: 회의 종료 후 백그라운드에서 AI 처리
|
||||
- **진행 상태 표시**: WebSocket으로 실시간 진행률 전달
|
||||
|
||||
### 3. 대용량 처리
|
||||
- 10명 이상 참석자: 회의록을 청크 단위로 분할 처리
|
||||
- 긴 회의 (2시간 이상): 안건별 병렬 처리
|
||||
|
||||
---
|
||||
|
||||
## 🧪 테스트 시나리오
|
||||
|
||||
### 1. 단위 테스트
|
||||
- [ ] 참석자별 회의록 조회 API 테스트
|
||||
- [ ] AI 통합 요약 API 테스트
|
||||
- [ ] Todo 자동 추출 API 테스트
|
||||
- [ ] 안건별 섹션 저장 및 조회 테스트
|
||||
|
||||
### 2. 통합 테스트
|
||||
- [ ] 회의 종료 → AI 요약 → Todo 추출 전체 플로우
|
||||
- [ ] 다수 참석자 (10명) 회의록 통합 테스트
|
||||
- [ ] 긴 회의록 (5000자 이상) 처리 테스트
|
||||
|
||||
### 3. 성능 테스트
|
||||
- [ ] AI 요약 응답 시간 측정 (목표: 5초 이내)
|
||||
- [ ] 동시 다발적 회의 종료 처리 (10개 동시)
|
||||
- [ ] 대용량 회의록 (10000자 이상) 처리 시간
|
||||
|
||||
---
|
||||
|
||||
## 📅 개발 일정 (예상)
|
||||
|
||||
### Phase 1: DB 및 기본 API (3일)
|
||||
- Day 1: DB 스키마 설계 및 마이그레이션
|
||||
- Day 2: Meeting Service API 개발
|
||||
- Day 3: 단위 테스트 및 API 검증
|
||||
|
||||
### Phase 2: AI 통합 (4일)
|
||||
- Day 1-2: Claude AI 프롬프트 설계 및 테스트
|
||||
- Day 3: AI Service API 개발
|
||||
- Day 4: 통합 테스트
|
||||
|
||||
### Phase 3: 최적화 및 배포 (2일)
|
||||
- Day 1: 성능 최적화 및 캐싱
|
||||
- Day 2: 프론트엔드 연동 테스트 및 배포
|
||||
|
||||
**총 예상 기간: 9일**
|
||||
|
||||
---
|
||||
|
||||
## 🚀 배포 체크리스트
|
||||
- [ ] DB 마이그레이션 스크립트 준비
|
||||
- [ ] API 명세서 업데이트
|
||||
- [ ] AI 프롬프트 버전 관리
|
||||
- [ ] 성능 모니터링 설정
|
||||
- [ ] 에러 로깅 및 알림 설정
|
||||
- [ ] 백업 및 롤백 계획 수립
|
||||
|
||||
---
|
||||
|
||||
## 📝 참고 문서
|
||||
- [유저스토리](../design/userstory.md)
|
||||
- [AI Service API 명세](../design/backend/api/ai-service-api.yaml)
|
||||
- [Meeting Service API 명세](../design/backend/api/meeting-service-api.yaml)
|
||||
- [07-회의종료.html 프로토타입](../design/uiux/prototype/07-회의종료.html)
|
||||
1719
logs/ai.log
1719
logs/ai.log
File diff suppressed because it is too large
Load Diff
@ -1,344 +0,0 @@
|
||||
> Task :common:generateEffectiveLombokConfig UP-TO-DATE
|
||||
> Task :common:compileJava UP-TO-DATE
|
||||
> Task :common:processResources NO-SOURCE
|
||||
> Task :common:classes UP-TO-DATE
|
||||
> Task :common:jar UP-TO-DATE
|
||||
> Task :user:generateEffectiveLombokConfig UP-TO-DATE
|
||||
> Task :user:compileJava UP-TO-DATE
|
||||
> Task :user:processResources
|
||||
> Task :user:classes
|
||||
> Task :user:resolveMainClassName
|
||||
|
||||
> Task :user:bootRun
|
||||
|
||||
. ____ _ __ _ _
|
||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
||||
=========|_|==============|___/=/_/_/_/
|
||||
|
||||
:: Spring Boot :: (v3.3.5)
|
||||
|
||||
2025-10-25 13:26:51 - Starting UserApplication using Java 21.0.8 with PID 81570 (/Users/daewoong/home/workspace/HGZero/user/build/classes/java/main started by daewoong in /Users/daewoong/home/workspace/HGZero/user)
|
||||
2025-10-25 13:26:51 - Running with Spring Boot v3.3.5, Spring v6.1.14
|
||||
2025-10-25 13:26:51 - The following 1 profile is active: "dev"
|
||||
2025-10-25 13:26:52 - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||
2025-10-25 13:26:52 - Bootstrapping Spring Data JPA repositories in DEFAULT mode.
|
||||
2025-10-25 13:26:52 - Finished Spring Data repository scanning in 57 ms. Found 1 JPA repository interface.
|
||||
2025-10-25 13:26:52 - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||
2025-10-25 13:26:52 - Bootstrapping Spring Data LDAP repositories in DEFAULT mode.
|
||||
2025-10-25 13:26:52 - Spring Data LDAP - Could not safely identify store assignment for repository candidate interface com.unicorn.hgzero.user.repository.jpa.UserRepository; If you want this repository to be a LDAP repository, consider annotating your entities with one of these annotations: org.springframework.ldap.odm.annotations.Entry (preferred), or consider extending one of the following types with your repository: org.springframework.data.ldap.repository.LdapRepository
|
||||
2025-10-25 13:26:52 - Finished Spring Data repository scanning in 2 ms. Found 0 LDAP repository interfaces.
|
||||
2025-10-25 13:26:52 - Multiple Spring Data modules found, entering strict repository configuration mode
|
||||
2025-10-25 13:26:52 - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
|
||||
2025-10-25 13:26:52 - Spring Data Redis - Could not safely identify store assignment for repository candidate interface com.unicorn.hgzero.user.repository.jpa.UserRepository; If you want this repository to be a Redis repository, consider annotating your entities with one of these annotations: org.springframework.data.redis.core.RedisHash (preferred), or consider extending one of the following types with your repository: org.springframework.data.keyvalue.repository.KeyValueRepository
|
||||
2025-10-25 13:26:52 - Finished Spring Data repository scanning in 1 ms. Found 0 Redis repository interfaces.
|
||||
2025-10-25 13:26:52 - No bean named 'errorChannel' has been explicitly defined. Therefore, a default PublishSubscribeChannel will be created.
|
||||
2025-10-25 13:26:52 - No bean named 'integrationHeaderChannelRegistry' has been explicitly defined. Therefore, a default DefaultHeaderChannelRegistry will be created.
|
||||
2025-10-25 13:26:52 - Tomcat initialized with port 8081 (http)
|
||||
2025-10-25 13:26:52 - Starting service [Tomcat]
|
||||
2025-10-25 13:26:52 - Starting Servlet engine: [Apache Tomcat/10.1.31]
|
||||
2025-10-25 13:26:52 - Initializing Spring embedded WebApplicationContext
|
||||
2025-10-25 13:26:52 - Root WebApplicationContext: initialization completed in 999 ms
|
||||
2025-10-25 13:26:52 - HHH000204: Processing PersistenceUnitInfo [name: default]
|
||||
2025-10-25 13:26:52 - HHH000412: Hibernate ORM core version 6.5.3.Final
|
||||
2025-10-25 13:26:52 - HHH000026: Second-level cache disabled
|
||||
2025-10-25 13:26:52 - Adding type registration boolean -> org.hibernate.type.BasicTypeReference@3de383f7
|
||||
2025-10-25 13:26:52 - Adding type registration boolean -> org.hibernate.type.BasicTypeReference@3de383f7
|
||||
2025-10-25 13:26:52 - Adding type registration java.lang.Boolean -> org.hibernate.type.BasicTypeReference@3de383f7
|
||||
2025-10-25 13:26:52 - Adding type registration numeric_boolean -> org.hibernate.type.BasicTypeReference@33ccead
|
||||
2025-10-25 13:26:52 - Adding type registration org.hibernate.type.NumericBooleanConverter -> org.hibernate.type.BasicTypeReference@33ccead
|
||||
2025-10-25 13:26:52 - Adding type registration true_false -> org.hibernate.type.BasicTypeReference@42ebece0
|
||||
2025-10-25 13:26:52 - Adding type registration org.hibernate.type.TrueFalseConverter -> org.hibernate.type.BasicTypeReference@42ebece0
|
||||
2025-10-25 13:26:52 - Adding type registration yes_no -> org.hibernate.type.BasicTypeReference@15c4b1a4
|
||||
2025-10-25 13:26:52 - Adding type registration org.hibernate.type.YesNoConverter -> org.hibernate.type.BasicTypeReference@15c4b1a4
|
||||
2025-10-25 13:26:52 - Adding type registration byte -> org.hibernate.type.BasicTypeReference@341964d0
|
||||
2025-10-25 13:26:52 - Adding type registration byte -> org.hibernate.type.BasicTypeReference@341964d0
|
||||
2025-10-25 13:26:52 - Adding type registration java.lang.Byte -> org.hibernate.type.BasicTypeReference@341964d0
|
||||
2025-10-25 13:26:52 - Adding type registration binary -> org.hibernate.type.BasicTypeReference@51b59d58
|
||||
2025-10-25 13:26:52 - Adding type registration byte[] -> org.hibernate.type.BasicTypeReference@51b59d58
|
||||
2025-10-25 13:26:52 - Adding type registration [B -> org.hibernate.type.BasicTypeReference@51b59d58
|
||||
2025-10-25 13:26:52 - Adding type registration binary_wrapper -> org.hibernate.type.BasicTypeReference@4ca4f762
|
||||
2025-10-25 13:26:52 - Adding type registration wrapper-binary -> org.hibernate.type.BasicTypeReference@4ca4f762
|
||||
2025-10-25 13:26:52 - Adding type registration image -> org.hibernate.type.BasicTypeReference@7c5d36c3
|
||||
2025-10-25 13:26:52 - Adding type registration blob -> org.hibernate.type.BasicTypeReference@31de27c
|
||||
2025-10-25 13:26:52 - Adding type registration java.sql.Blob -> org.hibernate.type.BasicTypeReference@31de27c
|
||||
2025-10-25 13:26:52 - Adding type registration materialized_blob -> org.hibernate.type.BasicTypeReference@7ebfe01a
|
||||
2025-10-25 13:26:52 - Adding type registration materialized_blob_wrapper -> org.hibernate.type.BasicTypeReference@154b0748
|
||||
2025-10-25 13:26:52 - Adding type registration short -> org.hibernate.type.BasicTypeReference@35c00c
|
||||
2025-10-25 13:26:52 - Adding type registration short -> org.hibernate.type.BasicTypeReference@35c00c
|
||||
2025-10-25 13:26:52 - Adding type registration java.lang.Short -> org.hibernate.type.BasicTypeReference@35c00c
|
||||
2025-10-25 13:26:52 - Adding type registration integer -> org.hibernate.type.BasicTypeReference@6cd7dc74
|
||||
2025-10-25 13:26:52 - Adding type registration int -> org.hibernate.type.BasicTypeReference@6cd7dc74
|
||||
2025-10-25 13:26:52 - Adding type registration java.lang.Integer -> org.hibernate.type.BasicTypeReference@6cd7dc74
|
||||
2025-10-25 13:26:52 - Adding type registration long -> org.hibernate.type.BasicTypeReference@6d695ec4
|
||||
2025-10-25 13:26:52 - Adding type registration long -> org.hibernate.type.BasicTypeReference@6d695ec4
|
||||
2025-10-25 13:26:52 - Adding type registration java.lang.Long -> org.hibernate.type.BasicTypeReference@6d695ec4
|
||||
2025-10-25 13:26:52 - Adding type registration float -> org.hibernate.type.BasicTypeReference@20556566
|
||||
2025-10-25 13:26:52 - Adding type registration float -> org.hibernate.type.BasicTypeReference@20556566
|
||||
2025-10-25 13:26:52 - Adding type registration java.lang.Float -> org.hibernate.type.BasicTypeReference@20556566
|
||||
2025-10-25 13:26:52 - Adding type registration double -> org.hibernate.type.BasicTypeReference@e4ef4c0
|
||||
2025-10-25 13:26:52 - Adding type registration double -> org.hibernate.type.BasicTypeReference@e4ef4c0
|
||||
2025-10-25 13:26:52 - Adding type registration java.lang.Double -> org.hibernate.type.BasicTypeReference@e4ef4c0
|
||||
2025-10-25 13:26:52 - Adding type registration big_integer -> org.hibernate.type.BasicTypeReference@5ca8bd01
|
||||
2025-10-25 13:26:52 - Adding type registration java.math.BigInteger -> org.hibernate.type.BasicTypeReference@5ca8bd01
|
||||
2025-10-25 13:26:52 - Adding type registration big_decimal -> org.hibernate.type.BasicTypeReference@7b10472e
|
||||
2025-10-25 13:26:52 - Adding type registration java.math.BigDecimal -> org.hibernate.type.BasicTypeReference@7b10472e
|
||||
2025-10-25 13:26:52 - Adding type registration character -> org.hibernate.type.BasicTypeReference@70e5737f
|
||||
2025-10-25 13:26:52 - Adding type registration char -> org.hibernate.type.BasicTypeReference@70e5737f
|
||||
2025-10-25 13:26:52 - Adding type registration java.lang.Character -> org.hibernate.type.BasicTypeReference@70e5737f
|
||||
2025-10-25 13:26:52 - Adding type registration character_nchar -> org.hibernate.type.BasicTypeReference@9746157
|
||||
2025-10-25 13:26:52 - Adding type registration string -> org.hibernate.type.BasicTypeReference@10ad95cd
|
||||
2025-10-25 13:26:52 - Adding type registration java.lang.String -> org.hibernate.type.BasicTypeReference@10ad95cd
|
||||
2025-10-25 13:26:52 - Adding type registration nstring -> org.hibernate.type.BasicTypeReference@69fd99c1
|
||||
2025-10-25 13:26:52 - Adding type registration characters -> org.hibernate.type.BasicTypeReference@32d8710a
|
||||
2025-10-25 13:26:52 - Adding type registration char[] -> org.hibernate.type.BasicTypeReference@32d8710a
|
||||
2025-10-25 13:26:52 - Adding type registration [C -> org.hibernate.type.BasicTypeReference@32d8710a
|
||||
2025-10-25 13:26:52 - Adding type registration wrapper-characters -> org.hibernate.type.BasicTypeReference@180cc0df
|
||||
2025-10-25 13:26:52 - Adding type registration text -> org.hibernate.type.BasicTypeReference@64f33dee
|
||||
2025-10-25 13:26:52 - Adding type registration ntext -> org.hibernate.type.BasicTypeReference@61c58320
|
||||
2025-10-25 13:26:52 - Adding type registration clob -> org.hibernate.type.BasicTypeReference@10e4ee33
|
||||
2025-10-25 13:26:52 - Adding type registration java.sql.Clob -> org.hibernate.type.BasicTypeReference@10e4ee33
|
||||
2025-10-25 13:26:52 - Adding type registration nclob -> org.hibernate.type.BasicTypeReference@6e90cec8
|
||||
2025-10-25 13:26:52 - Adding type registration java.sql.NClob -> org.hibernate.type.BasicTypeReference@6e90cec8
|
||||
2025-10-25 13:26:52 - Adding type registration materialized_clob -> org.hibernate.type.BasicTypeReference@13f182b9
|
||||
2025-10-25 13:26:52 - Adding type registration materialized_clob_char_array -> org.hibernate.type.BasicTypeReference@5ee0cf64
|
||||
2025-10-25 13:26:52 - Adding type registration materialized_clob_character_array -> org.hibernate.type.BasicTypeReference@69c227fd
|
||||
2025-10-25 13:26:52 - Adding type registration materialized_nclob -> org.hibernate.type.BasicTypeReference@14c5283
|
||||
2025-10-25 13:26:52 - Adding type registration materialized_nclob_character_array -> org.hibernate.type.BasicTypeReference@1eb7ec59
|
||||
2025-10-25 13:26:52 - Adding type registration materialized_nclob_char_array -> org.hibernate.type.BasicTypeReference@46748b04
|
||||
2025-10-25 13:26:52 - Adding type registration Duration -> org.hibernate.type.BasicTypeReference@3e71a1f8
|
||||
2025-10-25 13:26:52 - Adding type registration java.time.Duration -> org.hibernate.type.BasicTypeReference@3e71a1f8
|
||||
2025-10-25 13:26:52 - Adding type registration LocalDateTime -> org.hibernate.type.BasicTypeReference@5d4a34ff
|
||||
2025-10-25 13:26:52 - Adding type registration java.time.LocalDateTime -> org.hibernate.type.BasicTypeReference@5d4a34ff
|
||||
2025-10-25 13:26:52 - Adding type registration LocalDate -> org.hibernate.type.BasicTypeReference@7cbede2b
|
||||
2025-10-25 13:26:52 - Adding type registration java.time.LocalDate -> org.hibernate.type.BasicTypeReference@7cbede2b
|
||||
2025-10-25 13:26:52 - Adding type registration LocalTime -> org.hibernate.type.BasicTypeReference@1ef04613
|
||||
2025-10-25 13:26:52 - Adding type registration java.time.LocalTime -> org.hibernate.type.BasicTypeReference@1ef04613
|
||||
2025-10-25 13:26:52 - Adding type registration OffsetDateTime -> org.hibernate.type.BasicTypeReference@2d3d4a54
|
||||
2025-10-25 13:26:52 - Adding type registration java.time.OffsetDateTime -> org.hibernate.type.BasicTypeReference@2d3d4a54
|
||||
2025-10-25 13:26:52 - Adding type registration OffsetDateTimeWithTimezone -> org.hibernate.type.BasicTypeReference@215c6ec0
|
||||
2025-10-25 13:26:52 - Adding type registration OffsetDateTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@2b19b346
|
||||
2025-10-25 13:26:52 - Adding type registration OffsetTime -> org.hibernate.type.BasicTypeReference@37c5b8e8
|
||||
2025-10-25 13:26:52 - Adding type registration java.time.OffsetTime -> org.hibernate.type.BasicTypeReference@37c5b8e8
|
||||
2025-10-25 13:26:52 - Adding type registration OffsetTimeUtc -> org.hibernate.type.BasicTypeReference@706d2bae
|
||||
2025-10-25 13:26:52 - Adding type registration OffsetTimeWithTimezone -> org.hibernate.type.BasicTypeReference@3205610d
|
||||
2025-10-25 13:26:52 - Adding type registration OffsetTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@54e06788
|
||||
2025-10-25 13:26:52 - Adding type registration ZonedDateTime -> org.hibernate.type.BasicTypeReference@4e789704
|
||||
2025-10-25 13:26:52 - Adding type registration java.time.ZonedDateTime -> org.hibernate.type.BasicTypeReference@4e789704
|
||||
2025-10-25 13:26:52 - Adding type registration ZonedDateTimeWithTimezone -> org.hibernate.type.BasicTypeReference@5751e53e
|
||||
2025-10-25 13:26:52 - Adding type registration ZonedDateTimeWithoutTimezone -> org.hibernate.type.BasicTypeReference@4e45fbd0
|
||||
2025-10-25 13:26:52 - Adding type registration date -> org.hibernate.type.BasicTypeReference@19ce19b7
|
||||
2025-10-25 13:26:52 - Adding type registration java.sql.Date -> org.hibernate.type.BasicTypeReference@19ce19b7
|
||||
2025-10-25 13:26:52 - Adding type registration time -> org.hibernate.type.BasicTypeReference@13047d3d
|
||||
2025-10-25 13:26:52 - Adding type registration java.sql.Time -> org.hibernate.type.BasicTypeReference@13047d3d
|
||||
2025-10-25 13:26:52 - Adding type registration timestamp -> org.hibernate.type.BasicTypeReference@4b240276
|
||||
2025-10-25 13:26:52 - Adding type registration java.sql.Timestamp -> org.hibernate.type.BasicTypeReference@4b240276
|
||||
2025-10-25 13:26:52 - Adding type registration java.util.Date -> org.hibernate.type.BasicTypeReference@4b240276
|
||||
2025-10-25 13:26:52 - Adding type registration calendar -> org.hibernate.type.BasicTypeReference@2a5efbb9
|
||||
2025-10-25 13:26:52 - Adding type registration java.util.Calendar -> org.hibernate.type.BasicTypeReference@2a5efbb9
|
||||
2025-10-25 13:26:52 - Adding type registration java.util.GregorianCalendar -> org.hibernate.type.BasicTypeReference@2a5efbb9
|
||||
2025-10-25 13:26:52 - Adding type registration calendar_date -> org.hibernate.type.BasicTypeReference@43b45ce4
|
||||
2025-10-25 13:26:52 - Adding type registration calendar_time -> org.hibernate.type.BasicTypeReference@73e93c3a
|
||||
2025-10-25 13:26:52 - Adding type registration instant -> org.hibernate.type.BasicTypeReference@1835b783
|
||||
2025-10-25 13:26:52 - Adding type registration java.time.Instant -> org.hibernate.type.BasicTypeReference@1835b783
|
||||
2025-10-25 13:26:52 - Adding type registration uuid -> org.hibernate.type.BasicTypeReference@456b140f
|
||||
2025-10-25 13:26:52 - Adding type registration java.util.UUID -> org.hibernate.type.BasicTypeReference@456b140f
|
||||
2025-10-25 13:26:52 - Adding type registration pg-uuid -> org.hibernate.type.BasicTypeReference@456b140f
|
||||
2025-10-25 13:26:52 - Adding type registration uuid-binary -> org.hibernate.type.BasicTypeReference@2459333a
|
||||
2025-10-25 13:26:52 - Adding type registration uuid-char -> org.hibernate.type.BasicTypeReference@1e6bd367
|
||||
2025-10-25 13:26:52 - Adding type registration class -> org.hibernate.type.BasicTypeReference@2bd7f686
|
||||
2025-10-25 13:26:52 - Adding type registration java.lang.Class -> org.hibernate.type.BasicTypeReference@2bd7f686
|
||||
2025-10-25 13:26:52 - Adding type registration currency -> org.hibernate.type.BasicTypeReference@3601549f
|
||||
2025-10-25 13:26:52 - Adding type registration Currency -> org.hibernate.type.BasicTypeReference@3601549f
|
||||
2025-10-25 13:26:52 - Adding type registration java.util.Currency -> org.hibernate.type.BasicTypeReference@3601549f
|
||||
2025-10-25 13:26:52 - Adding type registration locale -> org.hibernate.type.BasicTypeReference@5b2c7186
|
||||
2025-10-25 13:26:52 - Adding type registration java.util.Locale -> org.hibernate.type.BasicTypeReference@5b2c7186
|
||||
2025-10-25 13:26:52 - Adding type registration serializable -> org.hibernate.type.BasicTypeReference@1b9c716f
|
||||
2025-10-25 13:26:52 - Adding type registration java.io.Serializable -> org.hibernate.type.BasicTypeReference@1b9c716f
|
||||
2025-10-25 13:26:52 - Adding type registration timezone -> org.hibernate.type.BasicTypeReference@f6bc75c
|
||||
2025-10-25 13:26:52 - Adding type registration java.util.TimeZone -> org.hibernate.type.BasicTypeReference@f6bc75c
|
||||
2025-10-25 13:26:52 - Adding type registration ZoneOffset -> org.hibernate.type.BasicTypeReference@33f2cf82
|
||||
2025-10-25 13:26:52 - Adding type registration java.time.ZoneOffset -> org.hibernate.type.BasicTypeReference@33f2cf82
|
||||
2025-10-25 13:26:52 - Adding type registration url -> org.hibernate.type.BasicTypeReference@bea283b
|
||||
2025-10-25 13:26:52 - Adding type registration java.net.URL -> org.hibernate.type.BasicTypeReference@bea283b
|
||||
2025-10-25 13:26:52 - Adding type registration vector -> org.hibernate.type.BasicTypeReference@73852720
|
||||
2025-10-25 13:26:52 - Adding type registration row_version -> org.hibernate.type.BasicTypeReference@22854f2b
|
||||
2025-10-25 13:26:52 - Adding type registration object -> org.hibernate.type.JavaObjectType@21ce2e4d
|
||||
2025-10-25 13:26:52 - Adding type registration java.lang.Object -> org.hibernate.type.JavaObjectType@21ce2e4d
|
||||
2025-10-25 13:26:52 - Adding type registration null -> org.hibernate.type.NullType@2ab7f649
|
||||
2025-10-25 13:26:52 - Adding type registration imm_date -> org.hibernate.type.BasicTypeReference@52a74328
|
||||
2025-10-25 13:26:52 - Adding type registration imm_time -> org.hibernate.type.BasicTypeReference@220be130
|
||||
2025-10-25 13:26:52 - Adding type registration imm_timestamp -> org.hibernate.type.BasicTypeReference@379b4e86
|
||||
2025-10-25 13:26:52 - Adding type registration imm_calendar -> org.hibernate.type.BasicTypeReference@5f4df55e
|
||||
2025-10-25 13:26:52 - Adding type registration imm_calendar_date -> org.hibernate.type.BasicTypeReference@72bce309
|
||||
2025-10-25 13:26:52 - Adding type registration imm_calendar_time -> org.hibernate.type.BasicTypeReference@f439e0f
|
||||
2025-10-25 13:26:52 - Adding type registration imm_binary -> org.hibernate.type.BasicTypeReference@62410e1f
|
||||
2025-10-25 13:26:52 - Adding type registration imm_serializable -> org.hibernate.type.BasicTypeReference@5296f00c
|
||||
2025-10-25 13:26:52 - No LoadTimeWeaver setup: ignoring JPA class transformer
|
||||
2025-10-25 13:26:52 - HikariPool-1 - Starting...
|
||||
2025-10-25 13:26:53 - HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@1b5d1d9
|
||||
2025-10-25 13:26:53 - HikariPool-1 - Start completed.
|
||||
2025-10-25 13:26:53 - HHH90000025: PostgreSQLDialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default)
|
||||
2025-10-25 13:26:53 - addDescriptor(2003, org.hibernate.type.descriptor.sql.internal.ArrayDdlTypeImpl@558575fe) replaced previous registration(org.hibernate.type.descriptor.sql.internal.ArrayDdlTypeImpl@25fcdcc6)
|
||||
2025-10-25 13:26:53 - addDescriptor(6, org.hibernate.type.descriptor.sql.internal.CapacityDependentDdlType@180fb796) replaced previous registration(org.hibernate.type.descriptor.sql.internal.DdlTypeImpl@79ae3fb1)
|
||||
2025-10-25 13:26:53 - addDescriptor(2004, BlobTypeDescriptor(BLOB_BINDING)) replaced previous registration(BlobTypeDescriptor(DEFAULT))
|
||||
2025-10-25 13:26:53 - addDescriptor(2005, ClobTypeDescriptor(CLOB_BINDING)) replaced previous registration(ClobTypeDescriptor(DEFAULT))
|
||||
2025-10-25 13:26:53 - Adding type registration JAVA_OBJECT -> org.hibernate.type.JavaObjectType@396c1228
|
||||
2025-10-25 13:26:53 - Adding type registration java.lang.Object -> org.hibernate.type.JavaObjectType@396c1228
|
||||
2025-10-25 13:26:53 - Type registration key [java.lang.Object] overrode previous entry : `org.hibernate.type.JavaObjectType@21ce2e4d`
|
||||
2025-10-25 13:26:53 - Adding type registration org.hibernate.type.DurationType -> basicType@1(java.time.Duration,3015)
|
||||
2025-10-25 13:26:53 - Adding type registration Duration -> basicType@1(java.time.Duration,3015)
|
||||
2025-10-25 13:26:53 - Adding type registration java.time.Duration -> basicType@1(java.time.Duration,3015)
|
||||
2025-10-25 13:26:53 - Adding type registration org.hibernate.type.OffsetDateTimeType -> basicType@2(java.time.OffsetDateTime,3003)
|
||||
2025-10-25 13:26:53 - Adding type registration OffsetDateTime -> basicType@2(java.time.OffsetDateTime,3003)
|
||||
2025-10-25 13:26:53 - Adding type registration java.time.OffsetDateTime -> basicType@2(java.time.OffsetDateTime,3003)
|
||||
2025-10-25 13:26:53 - Adding type registration org.hibernate.type.ZonedDateTimeType -> basicType@3(java.time.ZonedDateTime,3003)
|
||||
2025-10-25 13:26:53 - Adding type registration ZonedDateTime -> basicType@3(java.time.ZonedDateTime,3003)
|
||||
2025-10-25 13:26:53 - Adding type registration java.time.ZonedDateTime -> basicType@3(java.time.ZonedDateTime,3003)
|
||||
2025-10-25 13:26:53 - Adding type registration org.hibernate.type.OffsetTimeType -> basicType@4(java.time.OffsetTime,3007)
|
||||
2025-10-25 13:26:53 - Adding type registration OffsetTime -> basicType@4(java.time.OffsetTime,3007)
|
||||
2025-10-25 13:26:53 - Adding type registration java.time.OffsetTime -> basicType@4(java.time.OffsetTime,3007)
|
||||
2025-10-25 13:26:53 - Scoping TypeConfiguration [org.hibernate.type.spi.TypeConfiguration@4930213b] to MetadataBuildingContext [org.hibernate.boot.internal.MetadataBuildingContextRootImpl@67372d20]
|
||||
2025-10-25 13:26:53 - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
|
||||
2025-10-25 13:26:53 - Scoping TypeConfiguration [org.hibernate.type.spi.TypeConfiguration@4930213b] to SessionFactoryImplementor [org.hibernate.internal.SessionFactoryImpl@4b59a1c1]
|
||||
2025-10-25 13:26:53 - Handling #sessionFactoryCreated from [org.hibernate.internal.SessionFactoryImpl@4b59a1c1] for TypeConfiguration
|
||||
2025-10-25 13:26:53 - Initialized JPA EntityManagerFactory for persistence unit 'default'
|
||||
2025-10-25 13:26:53 - Property 'userDn' not set - anonymous context will be used for read-only operations
|
||||
2025-10-25 13:26:53 - Unable to load io.netty.resolver.dns.macos.MacOSDnsServerAddressStreamProvider, fallback to system defaults. This may result in incorrect DNS resolutions on MacOS. Check whether you have a dependency on 'io.netty:netty-resolver-dns-native-macos'. Use DEBUG level to see the full stack: java.lang.UnsatisfiedLinkError: failed to load the required native library
|
||||
2025-10-25 13:26:53 - spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
|
||||
2025-10-25 13:26:53 -
|
||||
|
||||
Using generated security password: 80447d0f-8aa4-4180-b5ff-df63f44dbce6
|
||||
|
||||
This generated password is for development use only. Your security configuration must be updated before running your application in production.
|
||||
|
||||
2025-10-25 13:26:53 - Global AuthenticationManager configured with UserDetailsService bean with name inMemoryUserDetailsManager
|
||||
2025-10-25 13:26:54 - Exposing 3 endpoints beneath base path '/actuator'
|
||||
2025-10-25 13:26:54 - Will secure any request with filters: DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter, HeaderWriterFilter, CorsFilter, LogoutFilter, JwtAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, SessionManagementFilter, ExceptionTranslationFilter, AuthorizationFilter
|
||||
2025-10-25 13:26:54 - Adding {logging-channel-adapter:_org.springframework.integration.errorLogger} as a subscriber to the 'errorChannel' channel
|
||||
2025-10-25 13:26:54 - Channel 'user.errorChannel' has 1 subscriber(s).
|
||||
2025-10-25 13:26:54 - started bean '_org.springframework.integration.errorLogger'
|
||||
2025-10-25 13:26:54 - Tomcat started on port 8081 (http) with context path '/'
|
||||
2025-10-25 13:26:54 - Started UserApplication in 3.207 seconds (process running for 3.362)
|
||||
2025-10-25 13:27:12 - Initializing Spring DispatcherServlet 'dispatcherServlet'
|
||||
2025-10-25 13:27:12 - Initializing Servlet 'dispatcherServlet'
|
||||
2025-10-25 13:27:12 - Completed initialization in 1 ms
|
||||
2025-10-25 13:27:12 - Securing POST /api/v1/auth/login
|
||||
2025-10-25 13:27:12 - Set SecurityContextHolder to anonymous SecurityContext
|
||||
2025-10-25 13:27:12 - Secured POST /api/v1/auth/login
|
||||
2025-10-25 13:27:12 - [Controller] com.unicorn.hgzero.user.controller.UserController.login 호출 - 파라미터: [com.unicorn.hgzero.user.dto.LoginRequest@f956efc]
|
||||
2025-10-25 13:27:12 - 로그인 요청: userId=user-005
|
||||
2025-10-25 13:27:12 - 로그인 시도: userId=user-005
|
||||
2025-10-25 13:27:12 -
|
||||
select
|
||||
ue1_0.user_id,
|
||||
ue1_0.authority,
|
||||
ue1_0.created_at,
|
||||
ue1_0.email,
|
||||
ue1_0.failed_login_attempts,
|
||||
ue1_0.last_login_at,
|
||||
ue1_0.locked,
|
||||
ue1_0.locked_at,
|
||||
ue1_0.updated_at,
|
||||
ue1_0.username
|
||||
from
|
||||
users ue1_0
|
||||
where
|
||||
ue1_0.user_id=?
|
||||
Hibernate:
|
||||
select
|
||||
ue1_0.user_id,
|
||||
ue1_0.authority,
|
||||
ue1_0.created_at,
|
||||
ue1_0.email,
|
||||
ue1_0.failed_login_attempts,
|
||||
ue1_0.last_login_at,
|
||||
ue1_0.locked,
|
||||
ue1_0.locked_at,
|
||||
ue1_0.updated_at,
|
||||
ue1_0.username
|
||||
from
|
||||
users ue1_0
|
||||
where
|
||||
ue1_0.user_id=?
|
||||
2025-10-25 13:27:12 - LDAP 인증 시도: username=user-005, profile=dev
|
||||
2025-10-25 13:27:12 - 개발 환경 - LDAP 인증 건너뛰기: username=user-005
|
||||
2025-10-25 13:27:12 - LDAP 인증 성공: userId=user-005, username=user-005, email=user-005@example.com
|
||||
2025-10-25 13:27:12 - 신규 사용자 등록: userId=user-005, username=user-005, email=user-005@example.com, department=미지정, title=미지정
|
||||
2025-10-25 13:27:12 -
|
||||
select
|
||||
ue1_0.user_id,
|
||||
ue1_0.authority,
|
||||
ue1_0.created_at,
|
||||
ue1_0.email,
|
||||
ue1_0.failed_login_attempts,
|
||||
ue1_0.last_login_at,
|
||||
ue1_0.locked,
|
||||
ue1_0.locked_at,
|
||||
ue1_0.updated_at,
|
||||
ue1_0.username
|
||||
from
|
||||
users ue1_0
|
||||
where
|
||||
ue1_0.user_id=?
|
||||
Hibernate:
|
||||
select
|
||||
ue1_0.user_id,
|
||||
ue1_0.authority,
|
||||
ue1_0.created_at,
|
||||
ue1_0.email,
|
||||
ue1_0.failed_login_attempts,
|
||||
ue1_0.last_login_at,
|
||||
ue1_0.locked,
|
||||
ue1_0.locked_at,
|
||||
ue1_0.updated_at,
|
||||
ue1_0.username
|
||||
from
|
||||
users ue1_0
|
||||
where
|
||||
ue1_0.user_id=?
|
||||
2025-10-25 13:27:12 - Refresh Token 저장: userId=user-005
|
||||
2025-10-25 13:27:12 - 로그인 성공: userId=user-005
|
||||
2025-10-25 13:27:12 - EventHub 미설정으로 로그인 이벤트 발행 건너뜀: userId=user-005
|
||||
2025-10-25 13:27:12 -
|
||||
/* insert for
|
||||
com.unicorn.hgzero.user.repository.entity.UserEntity */insert
|
||||
into
|
||||
users (authority, created_at, email, failed_login_attempts, last_login_at, locked, locked_at, updated_at, username, user_id)
|
||||
values
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
Hibernate:
|
||||
/* insert for
|
||||
com.unicorn.hgzero.user.repository.entity.UserEntity */insert
|
||||
into
|
||||
users (authority, created_at, email, failed_login_attempts, last_login_at, locked, locked_at, updated_at, username, user_id)
|
||||
values
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
2025-10-25 13:27:12 - [Controller] com.unicorn.hgzero.user.controller.UserController.login 완료 - 실행시간: 263ms
|
||||
2025-10-25 13:28:42 - Removing {logging-channel-adapter:_org.springframework.integration.errorLogger} as a subscriber to the 'errorChannel' channel
|
||||
2025-10-25 13:28:42 - Channel 'user.errorChannel' has 0 subscriber(s).
|
||||
2025-10-25 13:28:42 - stopped bean '_org.springframework.integration.errorLogger'
|
||||
2025-10-25 13:28:42 - Closing JPA EntityManagerFactory for persistence unit 'default'
|
||||
2025-10-25 13:28:42 - Handling #sessionFactoryClosed from [org.hibernate.internal.SessionFactoryImpl@4b59a1c1] for TypeConfiguration
|
||||
2025-10-25 13:28:42 - Un-scoping TypeConfiguration [org.hibernate.type.spi.TypeConfiguration$Scope@2ae29bca] from SessionFactory [org.hibernate.internal.SessionFactoryImpl@4b59a1c1]
|
||||
2025-10-25 13:28:42 - HikariPool-1 - Shutdown initiated...
|
||||
2025-10-25 13:28:42 - HikariPool-1 - Shutdown completed.
|
||||
|
||||
> Task :user:bootRun FAILED
|
||||
|
||||
FAILURE: Build failed with an exception.
|
||||
|
||||
* What went wrong:
|
||||
Execution failed for task ':user:bootRun'.
|
||||
> Process 'command '/Library/Java/JavaVirtualMachines/microsoft-21.jdk/Contents/Home/bin/java'' finished with non-zero exit value 143
|
||||
|
||||
* Try:
|
||||
> Run with --stacktrace option to get the stack trace.
|
||||
> Run with --info or --debug option to get more log output.
|
||||
> Run with --scan to get full insights.
|
||||
> Get more help at https://help.gradle.org.
|
||||
|
||||
BUILD FAILED in 1m 51s
|
||||
8 actionable tasks: 3 executed, 5 up-to-date
|
||||
56
meeting/QUICK-FIX-GUIDE.md
Normal file
56
meeting/QUICK-FIX-GUIDE.md
Normal file
@ -0,0 +1,56 @@
|
||||
# Minutes Sections 테이블 에러 빠른 해결 가이드
|
||||
|
||||
## 🚨 발생한 에러
|
||||
```
|
||||
Caused by: org.postgresql.util.PSQLException:
|
||||
ERROR: column "id" of relation "minutes_sections" contains null values
|
||||
```
|
||||
|
||||
## ✅ 해결 방법 (2단계)
|
||||
|
||||
### 1단계: 데이터베이스 정리
|
||||
|
||||
IntelliJ에서 다음 중 하나를 실행:
|
||||
|
||||
**방법 A: 직접 SQL 실행**
|
||||
```sql
|
||||
DELETE FROM minutes_sections WHERE id IS NULL;
|
||||
```
|
||||
|
||||
**방법 B: cleanup-minutes-sections.sql 파일 실행**
|
||||
1. IntelliJ Database 탭 열기
|
||||
2. `meetingdb` 우클릭 → New → Query Console
|
||||
3. `cleanup-minutes-sections.sql` 파일 내용 복사 & 실행
|
||||
|
||||
### 2단계: Meeting 서비스 재시작
|
||||
|
||||
IntelliJ Run Configuration에서 Meeting 서비스 재시작
|
||||
|
||||
## 📝 수정된 파일
|
||||
|
||||
1. **test-data-minutes-sections.sql**
|
||||
- Entity 구조에 맞게 컬럼명 수정
|
||||
- `id` 컬럼 추가 (필수)
|
||||
- `type`, `title`, `order` 등 추가
|
||||
- `section_number`, `section_title` 제거
|
||||
|
||||
2. **cleanup-minutes-sections.sql**
|
||||
- null id 레코드 삭제 스크립트
|
||||
|
||||
3. **README-FIX-MINUTES-SECTIONS.md**
|
||||
- 상세 문제 해결 가이드
|
||||
|
||||
## 🔍 확인 사항
|
||||
|
||||
서비스 시작 후 로그 확인:
|
||||
```bash
|
||||
tail -f logs/meeting-service.log
|
||||
```
|
||||
|
||||
에러가 없으면 성공! 다음 단계로 진행하세요.
|
||||
|
||||
## 📚 참고
|
||||
|
||||
- Entity: `MinutesSectionEntity.java`
|
||||
- Repository: `MinutesSectionRepository.java` (필요시 생성)
|
||||
- Service: `EndMeetingService.java`
|
||||
72
meeting/README-FIX-MINUTES-SECTIONS.md
Normal file
72
meeting/README-FIX-MINUTES-SECTIONS.md
Normal file
@ -0,0 +1,72 @@
|
||||
# minutes_sections 테이블 에러 해결 가이드
|
||||
|
||||
## 문제 상황
|
||||
Meeting 서비스 시작 시 다음 에러 발생:
|
||||
```
|
||||
Caused by: org.postgresql.util.PSQLException: ERROR: column "id" of relation "minutes_sections" contains null values
|
||||
```
|
||||
|
||||
## 원인
|
||||
- `minutes_sections` 테이블에 null id를 가진 레코드가 존재
|
||||
- Hibernate가 id 컬럼을 NOT NULL PRIMARY KEY로 변경하려 시도
|
||||
- 기존 null 데이터 때문에 ALTER TABLE 실패
|
||||
|
||||
## 해결 방법
|
||||
|
||||
### 방법 1: IntelliJ Database 도구 사용 (권장)
|
||||
|
||||
1. IntelliJ에서 Database 탭 열기
|
||||
2. `meetingdb` 데이터베이스 연결
|
||||
3. Query Console 열기
|
||||
4. 다음 SQL 실행:
|
||||
|
||||
```sql
|
||||
-- null id를 가진 레코드 삭제
|
||||
DELETE FROM minutes_sections WHERE id IS NULL;
|
||||
|
||||
-- 결과 확인
|
||||
SELECT COUNT(*) FROM minutes_sections;
|
||||
```
|
||||
|
||||
### 방법 2: cleanup-minutes-sections.sql 파일 실행
|
||||
|
||||
IntelliJ Database Console에서 `cleanup-minutes-sections.sql` 파일을 열어서 실행
|
||||
|
||||
## 실행 후
|
||||
|
||||
1. Meeting 서비스 재시작
|
||||
2. 로그에서 에러가 없는지 확인:
|
||||
```bash
|
||||
tail -f logs/meeting-service.log | grep -i error
|
||||
```
|
||||
3. 정상 시작되면 테스트 진행
|
||||
|
||||
## 추가 정보
|
||||
|
||||
### 테이블 구조 확인
|
||||
```sql
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable,
|
||||
column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'minutes_sections'
|
||||
ORDER BY ordinal_position;
|
||||
```
|
||||
|
||||
### 현재 데이터 확인
|
||||
```sql
|
||||
SELECT id, minutes_id, type, title FROM minutes_sections LIMIT 10;
|
||||
```
|
||||
|
||||
### Flyway 마이그레이션 이력 확인
|
||||
```sql
|
||||
SELECT * FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 5;
|
||||
```
|
||||
|
||||
## 참고사항
|
||||
|
||||
- 이 에러는 기존 테이블에 데이터가 있는 상태에서 Entity 구조가 변경되어 발생
|
||||
- 향후 같은 문제를 방지하려면 Flyway 마이그레이션 파일로 스키마 변경을 관리해야 함
|
||||
- 테스트 데이터는 `test-data-minutes-sections.sql` 파일 참조
|
||||
18
meeting/check-minutes-table.sql
Normal file
18
meeting/check-minutes-table.sql
Normal file
@ -0,0 +1,18 @@
|
||||
-- minutes 테이블 구조 확인
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable,
|
||||
column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'minutes'
|
||||
ORDER BY ordinal_position;
|
||||
|
||||
-- Primary Key 확인
|
||||
SELECT
|
||||
kcu.column_name
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
WHERE tc.table_name = 'minutes'
|
||||
AND tc.constraint_type = 'PRIMARY KEY';
|
||||
40
meeting/cleanup-minutes-sections.sh
Executable file
40
meeting/cleanup-minutes-sections.sh
Executable file
@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
# minutes_sections 테이블 정리 스크립트
|
||||
# 목적: null id를 가진 레코드 삭제
|
||||
|
||||
echo "========================================="
|
||||
echo "minutes_sections 테이블 정리 시작"
|
||||
echo "========================================="
|
||||
|
||||
# PostgreSQL 연결 정보
|
||||
DB_HOST="localhost"
|
||||
DB_PORT="5432"
|
||||
DB_NAME="meetingdb"
|
||||
DB_USER="postgres"
|
||||
|
||||
# 1. 기존 데이터 확인
|
||||
echo ""
|
||||
echo "1. 현재 테이블 상태 확인..."
|
||||
docker exec -i postgres-meeting psql -U $DB_USER -d $DB_NAME -c "SELECT COUNT(*) as total_rows FROM minutes_sections;"
|
||||
docker exec -i postgres-meeting psql -U $DB_USER -d $DB_NAME -c "SELECT COUNT(*) as null_id_rows FROM minutes_sections WHERE id IS NULL;"
|
||||
|
||||
# 2. null id를 가진 레코드 삭제
|
||||
echo ""
|
||||
echo "2. null id를 가진 레코드 삭제..."
|
||||
docker exec -i postgres-meeting psql -U $DB_USER -d $DB_NAME -c "DELETE FROM minutes_sections WHERE id IS NULL;"
|
||||
|
||||
# 3. 정리 완료 확인
|
||||
echo ""
|
||||
echo "3. 테이블 정리 완료. 현재 상태:"
|
||||
docker exec -i postgres-meeting psql -U $DB_USER -d $DB_NAME -c "SELECT COUNT(*) as remaining_rows FROM minutes_sections;"
|
||||
|
||||
# 4. 테이블 구조 확인
|
||||
echo ""
|
||||
echo "4. 테이블 구조 확인:"
|
||||
docker exec -i postgres-meeting psql -U $DB_USER -d $DB_NAME -c "\d minutes_sections"
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "정리 완료! Meeting 서비스를 재시작하세요."
|
||||
echo "========================================="
|
||||
26
meeting/cleanup-minutes-sections.sql
Normal file
26
meeting/cleanup-minutes-sections.sql
Normal file
@ -0,0 +1,26 @@
|
||||
-- ========================================
|
||||
-- minutes_sections 테이블 정리 SQL
|
||||
-- ========================================
|
||||
-- 목적: null id를 가진 레코드 삭제하여 서비스 시작 가능하게 함
|
||||
-- 실행방법: IntelliJ Database 도구에서 실행
|
||||
|
||||
-- 1. 현재 상태 확인
|
||||
SELECT 'Total rows:' as info, COUNT(*) as count FROM minutes_sections
|
||||
UNION ALL
|
||||
SELECT 'Null ID rows:', COUNT(*) FROM minutes_sections WHERE id IS NULL;
|
||||
|
||||
-- 2. null id를 가진 레코드 삭제
|
||||
DELETE FROM minutes_sections WHERE id IS NULL;
|
||||
|
||||
-- 3. 결과 확인
|
||||
SELECT 'Remaining rows:' as info, COUNT(*) as count FROM minutes_sections;
|
||||
|
||||
-- 4. 테이블 구조 확인
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable,
|
||||
column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'minutes_sections'
|
||||
ORDER BY ordinal_position;
|
||||
39
meeting/fix-minutes-sections-direct.sql
Normal file
39
meeting/fix-minutes-sections-direct.sql
Normal file
@ -0,0 +1,39 @@
|
||||
-- 직접 실행: minutes_sections 테이블 재생성
|
||||
|
||||
-- 1. 기존 테이블 삭제
|
||||
DROP TABLE IF EXISTS minutes_sections CASCADE;
|
||||
|
||||
-- 2. 테이블 재생성
|
||||
CREATE TABLE minutes_sections (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
minutes_id VARCHAR(50) NOT NULL,
|
||||
type VARCHAR(50),
|
||||
title VARCHAR(200),
|
||||
content TEXT,
|
||||
"order" INTEGER,
|
||||
verified BOOLEAN DEFAULT FALSE,
|
||||
locked BOOLEAN DEFAULT FALSE,
|
||||
locked_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT fk_minutes_sections_minutes
|
||||
FOREIGN KEY (minutes_id) REFERENCES minutes(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 3. 인덱스 생성
|
||||
CREATE INDEX idx_minutes_sections_minutes ON minutes_sections(minutes_id);
|
||||
CREATE INDEX idx_minutes_sections_order ON minutes_sections(minutes_id, "order");
|
||||
CREATE INDEX idx_minutes_sections_type ON minutes_sections(type);
|
||||
CREATE INDEX idx_minutes_sections_verified ON minutes_sections(verified);
|
||||
|
||||
-- 4. 트리거 생성
|
||||
DROP TRIGGER IF EXISTS update_minutes_sections_updated_at ON minutes_sections;
|
||||
CREATE TRIGGER update_minutes_sections_updated_at
|
||||
BEFORE UPDATE ON minutes_sections
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- 확인
|
||||
SELECT 'minutes_sections 테이블이 성공적으로 생성되었습니다!' as status;
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1 +0,0 @@
|
||||
nohup: ./gradlew: No such file or directory
|
||||
@ -1 +0,0 @@
|
||||
nohup: ./gradlew: No such file or directory
|
||||
142
meeting/migrate-agenda-sections.sql
Normal file
142
meeting/migrate-agenda-sections.sql
Normal file
@ -0,0 +1,142 @@
|
||||
-- ====================================================================
|
||||
-- agenda_sections 테이블 마이그레이션 스크립트
|
||||
--
|
||||
-- 목적:
|
||||
-- 1. agenda_number: varchar(50) → integer 변환
|
||||
-- 2. decisions, pending_items, todos: text → json 변환
|
||||
-- 3. opinions 컬럼 삭제 (서비스에서 미사용)
|
||||
--
|
||||
-- 실행 전 필수 작업:
|
||||
-- 1. 데이터베이스 백업 (pg_dump)
|
||||
-- 2. 테스트 환경에서 먼저 실행 및 검증
|
||||
-- ====================================================================
|
||||
|
||||
-- 트랜잭션 시작
|
||||
BEGIN;
|
||||
|
||||
-- ====================================================================
|
||||
-- 1단계: 백업 테이블 생성 (롤백용)
|
||||
-- ====================================================================
|
||||
CREATE TABLE IF NOT EXISTS agenda_sections_backup AS
|
||||
SELECT * FROM agenda_sections;
|
||||
|
||||
SELECT '✓ 백업 테이블 생성 완료: agenda_sections_backup' AS status;
|
||||
|
||||
-- ====================================================================
|
||||
-- 2단계: agenda_number 컬럼 타입 변경 (varchar → integer)
|
||||
-- ====================================================================
|
||||
-- 데이터 검증: 숫자가 아닌 값이 있는지 확인
|
||||
DO $$
|
||||
DECLARE
|
||||
invalid_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO invalid_count
|
||||
FROM agenda_sections
|
||||
WHERE agenda_number !~ '^[0-9]+$';
|
||||
|
||||
IF invalid_count > 0 THEN
|
||||
RAISE EXCEPTION '숫자가 아닌 agenda_number 값이 % 건 발견됨. 데이터 정리 필요.', invalid_count;
|
||||
END IF;
|
||||
|
||||
RAISE NOTICE '✓ agenda_number 데이터 검증 완료 (모두 숫자)';
|
||||
END $$;
|
||||
|
||||
-- 타입 변경 실행
|
||||
ALTER TABLE agenda_sections
|
||||
ALTER COLUMN agenda_number TYPE integer
|
||||
USING agenda_number::integer;
|
||||
|
||||
SELECT '✓ agenda_number 타입 변경 완료: varchar(50) → integer' AS status;
|
||||
|
||||
-- ====================================================================
|
||||
-- 3단계: JSON 컬럼 타입 변경 (text → json)
|
||||
-- ====================================================================
|
||||
|
||||
-- 3-1. decisions 컬럼 변경
|
||||
ALTER TABLE agenda_sections
|
||||
ALTER COLUMN decisions TYPE json
|
||||
USING CASE
|
||||
WHEN decisions IS NULL OR decisions = '' THEN NULL
|
||||
ELSE decisions::json
|
||||
END;
|
||||
|
||||
SELECT '✓ decisions 타입 변경 완료: text → json' AS status;
|
||||
|
||||
-- 3-2. pending_items 컬럼 변경
|
||||
ALTER TABLE agenda_sections
|
||||
ALTER COLUMN pending_items TYPE json
|
||||
USING CASE
|
||||
WHEN pending_items IS NULL OR pending_items = '' THEN NULL
|
||||
ELSE pending_items::json
|
||||
END;
|
||||
|
||||
SELECT '✓ pending_items 타입 변경 완료: text → json' AS status;
|
||||
|
||||
-- 3-3. todos 컬럼 변경
|
||||
ALTER TABLE agenda_sections
|
||||
ALTER COLUMN todos TYPE json
|
||||
USING CASE
|
||||
WHEN todos IS NULL OR todos = '' THEN NULL
|
||||
ELSE todos::json
|
||||
END;
|
||||
|
||||
SELECT '✓ todos 타입 변경 완료: text → json' AS status;
|
||||
|
||||
-- ====================================================================
|
||||
-- 4단계: opinions 컬럼 삭제 (서비스에서 미사용)
|
||||
-- ====================================================================
|
||||
ALTER TABLE agenda_sections
|
||||
DROP COLUMN IF EXISTS opinions;
|
||||
|
||||
SELECT '✓ opinions 컬럼 삭제 완료' AS status;
|
||||
|
||||
-- ====================================================================
|
||||
-- 5단계: 변경 사항 검증
|
||||
-- ====================================================================
|
||||
DO $$
|
||||
DECLARE
|
||||
rec RECORD;
|
||||
BEGIN
|
||||
-- 테이블 구조 확인
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
character_maximum_length,
|
||||
is_nullable
|
||||
INTO rec
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'agenda_sections'
|
||||
AND column_name = 'agenda_number';
|
||||
|
||||
RAISE NOTICE '========================================';
|
||||
RAISE NOTICE '✓ 마이그레이션 검증 결과';
|
||||
RAISE NOTICE '========================================';
|
||||
RAISE NOTICE 'agenda_number 타입: %', rec.data_type;
|
||||
|
||||
-- 데이터 건수 확인
|
||||
RAISE NOTICE '원본 데이터 건수: %', (SELECT COUNT(*) FROM agenda_sections_backup);
|
||||
RAISE NOTICE '마이그레이션 후 건수: %', (SELECT COUNT(*) FROM agenda_sections);
|
||||
RAISE NOTICE '========================================';
|
||||
END $$;
|
||||
|
||||
-- ====================================================================
|
||||
-- 커밋 또는 롤백 선택
|
||||
-- ====================================================================
|
||||
-- 문제가 없으면 COMMIT, 문제가 있으면 ROLLBACK 실행
|
||||
|
||||
-- 성공 시: COMMIT;
|
||||
-- 실패 시: ROLLBACK;
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT '
|
||||
====================================================================
|
||||
✓ 마이그레이션 완료!
|
||||
|
||||
다음 작업:
|
||||
1. 애플리케이션 재시작
|
||||
2. 기능 테스트 수행
|
||||
3. 문제 없으면 백업 테이블 삭제:
|
||||
DROP TABLE agenda_sections_backup;
|
||||
====================================================================
|
||||
' AS next_steps;
|
||||
@ -37,6 +37,11 @@ public class MeetingAnalysis {
|
||||
*/
|
||||
private List<String> keywords;
|
||||
|
||||
/**
|
||||
* 회의 전체 결정사항
|
||||
*/
|
||||
private String decisions;
|
||||
|
||||
/**
|
||||
* 안건별 분석 결과
|
||||
*/
|
||||
|
||||
@ -31,6 +31,11 @@ public class Minutes {
|
||||
*/
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* 작성자 사용자 ID (NULL: AI 통합 회의록, NOT NULL: 참석자별 회의록)
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 회의록 제목
|
||||
*/
|
||||
@ -71,6 +76,11 @@ public class Minutes {
|
||||
*/
|
||||
private String lastModifiedBy;
|
||||
|
||||
/**
|
||||
* 회의 전체 결정사항
|
||||
*/
|
||||
private String decisions;
|
||||
|
||||
/**
|
||||
* 확정자 ID
|
||||
*/
|
||||
|
||||
@ -106,6 +106,11 @@ public class MinutesDTO {
|
||||
*/
|
||||
private final LocalDateTime lastModifiedAt;
|
||||
|
||||
/**
|
||||
* 회의 전체 결정사항
|
||||
*/
|
||||
private final String decisions;
|
||||
|
||||
/**
|
||||
* Todo 개수
|
||||
*/
|
||||
|
||||
@ -0,0 +1,267 @@
|
||||
package com.unicorn.hgzero.meeting.biz.service;
|
||||
|
||||
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.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.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.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;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Service;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 회의 종료 비즈니스 로직 (AI 통합)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Primary
|
||||
@RequiredArgsConstructor
|
||||
public class EndMeetingService implements EndMeetingUseCase {
|
||||
|
||||
private final MeetingJpaRepository meetingRepository;
|
||||
private final MinutesJpaRepository minutesRepository;
|
||||
private final MinutesSectionJpaRepository minutesSectionRepository;
|
||||
private final TodoJpaRepository todoRepository;
|
||||
private final MeetingAnalysisJpaRepository analysisRepository;
|
||||
private final AIServiceClient aiServiceClient;
|
||||
|
||||
/**
|
||||
* 회의 종료 및 AI 분석 실행
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return 회의 종료 결과 DTO
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public MeetingEndDTO endMeeting(String meetingId) {
|
||||
log.info("회의 종료 시작 - meetingId: {}", meetingId);
|
||||
|
||||
// 1. 회의 정보 조회
|
||||
MeetingEntity meeting = meetingRepository.findById(meetingId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("회의를 찾을 수 없습니다: " + meetingId));
|
||||
|
||||
// 2. 참석자별 회의록 조회 (userId가 있는 회의록들)
|
||||
List<MinutesEntity> participantMinutesList = minutesRepository.findByMeetingIdAndUserIdIsNotNull(meetingId);
|
||||
|
||||
if (participantMinutesList.isEmpty()) {
|
||||
throw new IllegalStateException("참석자 회의록이 없습니다: " + meetingId);
|
||||
}
|
||||
|
||||
// 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 분석 결과 저장
|
||||
MeetingAnalysis analysis = saveAnalysisResult(meeting, aiResponse);
|
||||
|
||||
// 6. Todo 생성 및 저장
|
||||
List<TodoEntity> todos = createAndSaveTodos(meeting, aiResponse, analysis);
|
||||
|
||||
// 6. 회의 종료 처리
|
||||
meeting.end();
|
||||
meetingRepository.save(meeting);
|
||||
|
||||
// 7. 응답 DTO 생성
|
||||
return createMeetingEndDTO(meeting, analysis, todos, participantMinutesList.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 통합 분석 요청 데이터 생성
|
||||
* 참석자별 회의록의 섹션들을 참석자별로 그룹화하여 AI 요청 데이터 생성
|
||||
*/
|
||||
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()
|
||||
.meetingId(meeting.getMeetingId())
|
||||
.participantMinutes(participantMinutes)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 분석 결과 저장
|
||||
*/
|
||||
private MeetingAnalysis saveAnalysisResult(MeetingEntity meeting, ConsolidateResponse aiResponse) {
|
||||
// AgendaAnalysis 리스트 생성
|
||||
List<MeetingAnalysis.AgendaAnalysis> agendaAnalyses = aiResponse.getAgendaSummaries().stream()
|
||||
.<MeetingAnalysis.AgendaAnalysis>map(summary -> MeetingAnalysis.AgendaAnalysis.builder()
|
||||
.agendaId(UUID.randomUUID().toString())
|
||||
.title(summary.getAgendaTitle())
|
||||
.aiSummaryShort(summary.getSummaryShort())
|
||||
.discussion(summary.getSummary() != null ? summary.getSummary() : "")
|
||||
.decisions(List.of())
|
||||
.pending(summary.getPending() != null ? summary.getPending() : List.of())
|
||||
.extractedTodos(summary.getTodos() != null
|
||||
? summary.getTodos().stream()
|
||||
.<String>map(todo -> todo.getTitle())
|
||||
.collect(Collectors.toList())
|
||||
: List.of())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// MeetingAnalysis 도메인 생성
|
||||
MeetingAnalysis analysis = MeetingAnalysis.builder()
|
||||
.analysisId(UUID.randomUUID().toString())
|
||||
.meetingId(meeting.getMeetingId())
|
||||
.minutesId(meeting.getMeetingId()) // 실제로는 minutesId 필요
|
||||
.keywords(aiResponse.getKeywords())
|
||||
.decisions(aiResponse.getDecisions())
|
||||
.agendaAnalyses(agendaAnalyses)
|
||||
.status("COMPLETED")
|
||||
.completedAt(LocalDateTime.now())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// Entity 저장
|
||||
MeetingAnalysisEntity entity = MeetingAnalysisEntity.fromDomain(analysis);
|
||||
analysisRepository.save(entity);
|
||||
|
||||
log.info("AI 분석 결과 저장 완료 - analysisId: {}", analysis.getAnalysisId());
|
||||
|
||||
return analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Todo 생성 및 저장
|
||||
*/
|
||||
private List<TodoEntity> createAndSaveTodos(MeetingEntity meeting, ConsolidateResponse aiResponse, MeetingAnalysis analysis) {
|
||||
List<TodoEntity> todos = aiResponse.getAgendaSummaries().stream()
|
||||
.<TodoEntity>flatMap(agenda -> {
|
||||
String agendaId = findAgendaIdByTitle(analysis, agenda.getAgendaTitle());
|
||||
List<ExtractedTodoDTO> todoList = agenda.getTodos() != null ? agenda.getTodos() : List.of();
|
||||
return todoList.stream()
|
||||
.<TodoEntity>map(todo -> TodoEntity.builder()
|
||||
.todoId(UUID.randomUUID().toString())
|
||||
.meetingId(meeting.getMeetingId())
|
||||
.minutesId(meeting.getMeetingId()) // 실제로는 minutesId 필요
|
||||
.title(todo.getTitle())
|
||||
.assigneeId("") // AI가 담당자를 추출하지 않으므로 빈 값
|
||||
.status("PENDING")
|
||||
.build());
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!todos.isEmpty()) {
|
||||
todoRepository.saveAll(todos);
|
||||
log.info("Todo 생성 완료 - 총 {}개", todos.size());
|
||||
}
|
||||
|
||||
return todos;
|
||||
}
|
||||
|
||||
/**
|
||||
* 안건 제목으로 안건 ID 찾기
|
||||
*/
|
||||
private String findAgendaIdByTitle(MeetingAnalysis analysis, String title) {
|
||||
return analysis.getAgendaAnalyses().stream()
|
||||
.filter(agenda -> agenda.getTitle().equals(title))
|
||||
.findFirst()
|
||||
.map(MeetingAnalysis.AgendaAnalysis::getAgendaId)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 종료 결과 DTO 생성
|
||||
*/
|
||||
private MeetingEndDTO createMeetingEndDTO(MeetingEntity meeting, MeetingAnalysis analysis,
|
||||
List<TodoEntity> todos, int participantCount) {
|
||||
// 회의 소요 시간 계산
|
||||
int durationMinutes = calculateDurationMinutes(meeting.getStartedAt(), meeting.getEndedAt());
|
||||
|
||||
// 안건별 요약 DTO 생성
|
||||
List<MeetingEndDTO.AgendaSummaryDTO> agendaSummaries = analysis.getAgendaAnalyses().stream()
|
||||
.<MeetingEndDTO.AgendaSummaryDTO>map(agenda -> {
|
||||
// 해당 안건의 Todo 필터링 (agendaId가 없을 수 있음)
|
||||
List<MeetingEndDTO.TodoSummaryDTO> agendaTodos = todos.stream()
|
||||
.filter(todo -> agenda.getAgendaId().equals(todo.getMinutesId())) // 임시 매핑
|
||||
.<MeetingEndDTO.TodoSummaryDTO>map(todo -> MeetingEndDTO.TodoSummaryDTO.builder()
|
||||
.title(todo.getTitle())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return MeetingEndDTO.AgendaSummaryDTO.builder()
|
||||
.title(agenda.getTitle())
|
||||
.aiSummaryShort(agenda.getAiSummaryShort())
|
||||
.details(MeetingEndDTO.AgendaDetailsDTO.builder()
|
||||
.discussion(agenda.getDiscussion())
|
||||
.decisions(agenda.getDecisions())
|
||||
.pending(agenda.getPending())
|
||||
.build())
|
||||
.todos(agendaTodos)
|
||||
.build();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return MeetingEndDTO.builder()
|
||||
.title(meeting.getTitle())
|
||||
.participantCount(participantCount)
|
||||
.durationMinutes(durationMinutes)
|
||||
.agendaCount(analysis.getAgendaAnalyses().size())
|
||||
.todoCount(todos.size())
|
||||
.keywords(analysis.getKeywords())
|
||||
.agendaSummaries(agendaSummaries)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 소요 시간 계산 (분 단위)
|
||||
*/
|
||||
private int calculateDurationMinutes(LocalDateTime startedAt, LocalDateTime endedAt) {
|
||||
if (startedAt == null || endedAt == null) {
|
||||
return 0;
|
||||
}
|
||||
return (int) Duration.between(startedAt, endedAt).toMinutes();
|
||||
}
|
||||
}
|
||||
@ -389,4 +389,30 @@ public class MinutesService implements
|
||||
.sections(sectionDTOs) // 섹션 정보 추가
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 ID로 참석자별 회의록 조회
|
||||
* AI Service가 통합 회의록 생성 시 사용
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return 참석자별 회의록 목록 (user_id IS NOT NULL)
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Minutes> getParticipantMinutesByMeeting(String meetingId) {
|
||||
log.info("회의 ID로 참석자별 회의록 조회: {}", meetingId);
|
||||
return minutesReader.findParticipantMinutesByMeetingId(meetingId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 ID로 AI 통합 회의록 조회
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return AI 통합 회의록 (user_id IS NULL)
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Minutes getConsolidatedMinutesByMeeting(String meetingId) {
|
||||
log.info("회의 ID로 AI 통합 회의록 조회: {}", meetingId);
|
||||
return minutesReader.findConsolidatedMinutesByMeetingId(meetingId)
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.AgendaSection;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 안건별 회의록 섹션 조회 인터페이스
|
||||
*/
|
||||
public interface AgendaSectionReader {
|
||||
|
||||
/**
|
||||
* 회의 ID로 안건별 섹션 조회
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return 안건별 섹션 목록 (안건 번호 순 정렬)
|
||||
*/
|
||||
List<AgendaSection> findByMeetingId(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의록 ID로 안건별 섹션 조회
|
||||
*
|
||||
* @param minutesId 회의록 ID
|
||||
* @return 안건별 섹션 목록
|
||||
*/
|
||||
List<AgendaSection> findByMinutesId(String minutesId);
|
||||
|
||||
/**
|
||||
* 섹션 ID로 조회
|
||||
*
|
||||
* @param id 섹션 ID
|
||||
* @return 안건 섹션
|
||||
*/
|
||||
Optional<AgendaSection> findById(String id);
|
||||
|
||||
/**
|
||||
* 회의 ID와 안건 번호로 섹션 조회
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @param agendaNumber 안건 번호
|
||||
* @return 안건 섹션
|
||||
*/
|
||||
Optional<AgendaSection> findByMeetingIdAndAgendaNumber(String meetingId, Integer agendaNumber);
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.AgendaSection;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 안건별 회의록 섹션 저장 인터페이스
|
||||
*/
|
||||
public interface AgendaSectionWriter {
|
||||
|
||||
/**
|
||||
* 안건별 섹션 저장
|
||||
*
|
||||
* @param section 안건 섹션
|
||||
* @return 저장된 안건 섹션
|
||||
*/
|
||||
AgendaSection save(AgendaSection section);
|
||||
|
||||
/**
|
||||
* 안건별 섹션 일괄 저장
|
||||
*
|
||||
* @param sections 안건 섹션 목록
|
||||
* @return 저장된 안건 섹션 목록
|
||||
*/
|
||||
List<AgendaSection> saveAll(List<AgendaSection> sections);
|
||||
|
||||
/**
|
||||
* 안건별 섹션 삭제
|
||||
*
|
||||
* @param id 섹션 ID
|
||||
*/
|
||||
void delete(String id);
|
||||
|
||||
/**
|
||||
* 회의 ID로 안건별 섹션 전체 삭제
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
*/
|
||||
void deleteByMeetingId(String meetingId);
|
||||
}
|
||||
@ -39,4 +39,21 @@ public interface MinutesReader {
|
||||
* 확정자 ID로 회의록 목록 조회
|
||||
*/
|
||||
List<Minutes> findByFinalizedBy(String finalizedBy);
|
||||
|
||||
/**
|
||||
* 회의 ID로 참석자별 회의록 조회 (user_id IS NOT NULL)
|
||||
* AI Service가 통합 회의록 생성 시 사용
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return 참석자별 회의록 목록
|
||||
*/
|
||||
List<Minutes> findParticipantMinutesByMeetingId(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의 ID로 AI 통합 회의록 조회 (user_id IS NULL)
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return AI 통합 회의록
|
||||
*/
|
||||
Optional<Minutes> findConsolidatedMinutesByMeetingId(String meetingId);
|
||||
}
|
||||
|
||||
@ -0,0 +1,80 @@
|
||||
package com.unicorn.hgzero.meeting.infra.client;
|
||||
|
||||
import com.unicorn.hgzero.meeting.infra.dto.ai.ConsolidateRequest;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.ai.ConsolidateResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* AI Service 호출 클라이언트
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class AIServiceClient {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final String aiServiceUrl;
|
||||
|
||||
public AIServiceClient(
|
||||
RestTemplateBuilder restTemplateBuilder,
|
||||
@Value("${ai.service.url:http://localhost:8087}") String aiServiceUrl,
|
||||
@Value("${ai.service.timeout:30000}") int timeout
|
||||
) {
|
||||
this.restTemplate = restTemplateBuilder
|
||||
.setConnectTimeout(Duration.ofMillis(timeout))
|
||||
.setReadTimeout(Duration.ofMillis(timeout))
|
||||
.build();
|
||||
this.aiServiceUrl = aiServiceUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의록 통합 요약 API 호출
|
||||
*
|
||||
* @param request 통합 요약 요청
|
||||
* @return 통합 요약 응답
|
||||
*/
|
||||
public ConsolidateResponse consolidateMinutes(ConsolidateRequest request) {
|
||||
log.info("AI Service 호출 - 회의록 통합 요약: {}", request.getMeetingId());
|
||||
|
||||
try {
|
||||
String url = aiServiceUrl + "/api/transcripts/consolidate";
|
||||
|
||||
// HTTP 헤더 설정
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
// HTTP 요청 생성
|
||||
HttpEntity<ConsolidateRequest> httpEntity = new HttpEntity<>(request, headers);
|
||||
|
||||
// API 호출
|
||||
ResponseEntity<ConsolidateResponse> response = restTemplate.postForEntity(
|
||||
url,
|
||||
httpEntity,
|
||||
ConsolidateResponse.class
|
||||
);
|
||||
|
||||
ConsolidateResponse result = response.getBody();
|
||||
|
||||
if (result == null) {
|
||||
throw new RuntimeException("AI Service 응답이 비어있습니다");
|
||||
}
|
||||
|
||||
log.info("AI Service 응답 수신 완료 - 안건 수: {}", result.getAgendaSummaries().size());
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("AI Service 호출 실패: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("AI 회의록 통합 처리 중 오류가 발생했습니다: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,261 @@
|
||||
package com.unicorn.hgzero.meeting.infra.controller;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.unicorn.hgzero.common.dto.ApiResponse;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.AgendaSection;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
|
||||
import com.unicorn.hgzero.meeting.biz.service.AgendaSectionService;
|
||||
import com.unicorn.hgzero.meeting.biz.service.MeetingService;
|
||||
import com.unicorn.hgzero.meeting.biz.service.MinutesService;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.ParticipantMinutesResponse;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.AgendaSectionResponse;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.MeetingStatisticsResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 회의 AI 기능 API Controller
|
||||
* 회의 종료 후 AI 통합 회의록 생성을 위한 API
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/meetings")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Tag(name = "Meeting AI", description = "회의 AI 통합 기능 API")
|
||||
public class MeetingAiController {
|
||||
|
||||
private final MinutesService minutesService;
|
||||
private final AgendaSectionService agendaSectionService;
|
||||
private final MeetingService meetingService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@GetMapping("/{meetingId}/ai/participant-minutes")
|
||||
@Operation(
|
||||
summary = "참석자별 회의록 조회",
|
||||
description = "특정 회의의 모든 참석자가 작성한 회의록을 조회합니다. AI Service가 통합 회의록 생성 시 사용합니다."
|
||||
)
|
||||
public ResponseEntity<ApiResponse<ParticipantMinutesResponse>> getParticipantMinutes(
|
||||
@Parameter(description = "회의 ID") @PathVariable String meetingId,
|
||||
@RequestHeader("X-User-Id") String userId) {
|
||||
|
||||
log.info("참석자별 회의록 조회 요청 - meetingId: {}, userId: {}", meetingId, userId);
|
||||
|
||||
try {
|
||||
List<Minutes> participantMinutes = minutesService.getParticipantMinutesByMeeting(meetingId);
|
||||
|
||||
List<ParticipantMinutesResponse.ParticipantMinutesItem> items = participantMinutes.stream()
|
||||
.map(this::convertToParticipantMinutesItem)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
ParticipantMinutesResponse response = ParticipantMinutesResponse.builder()
|
||||
.meetingId(meetingId)
|
||||
.participantMinutes(items)
|
||||
.build();
|
||||
|
||||
log.info("참석자별 회의록 조회 성공 - meetingId: {}, count: {}", meetingId, items.size());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("참석자별 회의록 조회 실패 - meetingId: {}", meetingId, e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ApiResponse.errorWithType("참석자별 회의록 조회에 실패했습니다"));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{meetingId}/ai/agenda-sections")
|
||||
@Operation(
|
||||
summary = "안건별 섹션 조회",
|
||||
description = "특정 회의의 안건별 AI 요약 섹션을 조회합니다. 회의 종료 화면에서 사용합니다."
|
||||
)
|
||||
public ResponseEntity<ApiResponse<AgendaSectionResponse>> getAgendaSections(
|
||||
@Parameter(description = "회의 ID") @PathVariable String meetingId,
|
||||
@RequestHeader("X-User-Id") String userId) {
|
||||
|
||||
log.info("안건별 섹션 조회 요청 - meetingId: {}, userId: {}", meetingId, userId);
|
||||
|
||||
try {
|
||||
List<AgendaSection> sections = agendaSectionService.getAgendaSectionsByMeetingId(meetingId);
|
||||
|
||||
List<AgendaSectionResponse.AgendaSectionItem> items = sections.stream()
|
||||
.map(this::convertToAgendaSectionItem)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
AgendaSectionResponse response = AgendaSectionResponse.builder()
|
||||
.meetingId(meetingId)
|
||||
.sections(items)
|
||||
.build();
|
||||
|
||||
log.info("안건별 섹션 조회 성공 - meetingId: {}, count: {}", meetingId, items.size());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("안건별 섹션 조회 실패 - meetingId: {}", meetingId, e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ApiResponse.errorWithType("안건별 섹션 조회에 실패했습니다"));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{meetingId}/ai/statistics")
|
||||
@Operation(
|
||||
summary = "회의 통계 조회",
|
||||
description = "특정 회의의 통계 정보를 조회합니다. 회의 종료 화면에서 사용합니다."
|
||||
)
|
||||
public ResponseEntity<ApiResponse<MeetingStatisticsResponse>> getMeetingStatistics(
|
||||
@Parameter(description = "회의 ID") @PathVariable String meetingId,
|
||||
@RequestHeader("X-User-Id") String userId) {
|
||||
|
||||
log.info("회의 통계 조회 요청 - meetingId: {}, userId: {}", meetingId, userId);
|
||||
|
||||
try {
|
||||
MeetingStatisticsResponse response = buildMeetingStatistics(meetingId);
|
||||
|
||||
log.info("회의 통계 조회 성공 - meetingId: {}", meetingId);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("회의 통계 조회 실패 - meetingId: {}", meetingId, e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ApiResponse.errorWithType("회의 통계 조회에 실패했습니다"));
|
||||
}
|
||||
}
|
||||
|
||||
private ParticipantMinutesResponse.ParticipantMinutesItem convertToParticipantMinutesItem(Minutes minutes) {
|
||||
List<ParticipantMinutesResponse.ParticipantMinutesItem.MinutesSection> sections = null;
|
||||
if (minutes.getSections() != null) {
|
||||
sections = minutes.getSections().stream()
|
||||
.map(this::convertToMinutesSection)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return ParticipantMinutesResponse.ParticipantMinutesItem.builder()
|
||||
.minutesId(minutes.getMinutesId())
|
||||
.userId(minutes.getUserId())
|
||||
.title(minutes.getTitle())
|
||||
.status(minutes.getStatus())
|
||||
.version(minutes.getVersion())
|
||||
.createdBy(minutes.getCreatedBy())
|
||||
.createdAt(minutes.getCreatedAt())
|
||||
.lastModifiedAt(minutes.getLastModifiedAt())
|
||||
.sections(sections)
|
||||
.build();
|
||||
}
|
||||
|
||||
private ParticipantMinutesResponse.ParticipantMinutesItem.MinutesSection convertToMinutesSection(MinutesSection section) {
|
||||
return ParticipantMinutesResponse.ParticipantMinutesItem.MinutesSection.builder()
|
||||
.sectionId(section.getSectionId())
|
||||
.title(section.getTitle())
|
||||
.type(section.getType())
|
||||
.content(section.getContent())
|
||||
.orderIndex(section.getOrder())
|
||||
.build();
|
||||
}
|
||||
|
||||
private AgendaSectionResponse.AgendaSectionItem convertToAgendaSectionItem(AgendaSection section) {
|
||||
// todos JSON 파싱
|
||||
List<AgendaSectionResponse.AgendaSectionItem.TodoItem> todos = parseTodosJson(section.getTodos());
|
||||
|
||||
// pendingItems는 사용하지 않으므로 null 처리
|
||||
List<String> pendingItems = null;
|
||||
|
||||
return AgendaSectionResponse.AgendaSectionItem.builder()
|
||||
.id(section.getId())
|
||||
.minutesId(section.getMinutesId())
|
||||
.agendaNumber(section.getAgendaNumber())
|
||||
.agendaTitle(section.getAgendaTitle())
|
||||
.aiSummaryShort(section.getAiSummaryShort())
|
||||
.summary(section.getAiSummaryShort()) // summary 필드가 없으므로 aiSummaryShort 사용
|
||||
.pendingItems(pendingItems)
|
||||
.todos(todos)
|
||||
.createdAt(section.getCreatedAt())
|
||||
.updatedAt(section.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* todos JSON 문자열을 파싱하여 TodoItem 리스트로 변환
|
||||
* JSON 구조: [{"title": "...", "assignee": "...", "dueDate": "...", "description": "...", "priority": "..."}]
|
||||
*/
|
||||
private List<AgendaSectionResponse.AgendaSectionItem.TodoItem> parseTodosJson(String todosJson) {
|
||||
if (todosJson == null || todosJson.trim().isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
try {
|
||||
List<TodoJson> todoJsonList = objectMapper.readValue(
|
||||
todosJson,
|
||||
new TypeReference<List<TodoJson>>() {}
|
||||
);
|
||||
|
||||
return todoJsonList.stream()
|
||||
.map(json -> AgendaSectionResponse.AgendaSectionItem.TodoItem.builder()
|
||||
.title(json.getTitle())
|
||||
.assignee(json.getAssignee())
|
||||
.dueDate(json.getDueDate())
|
||||
.description(json.getDescription())
|
||||
.priority(json.getPriority())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to parse todos JSON: {}", todosJson, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 파싱을 위한 임시 DTO
|
||||
*/
|
||||
@lombok.Data
|
||||
private static class TodoJson {
|
||||
private String title;
|
||||
private String assignee;
|
||||
private String dueDate;
|
||||
private String description;
|
||||
private String priority;
|
||||
}
|
||||
|
||||
private MeetingStatisticsResponse buildMeetingStatistics(String meetingId) {
|
||||
Meeting meeting = meetingService.getMeeting(meetingId);
|
||||
List<AgendaSection> sections = agendaSectionService.getAgendaSectionsByMeetingId(meetingId);
|
||||
|
||||
// AI가 추출한 Todo 수 계산
|
||||
int todoCount = sections.stream()
|
||||
.mapToInt(s -> parseTodosJson(s.getTodos()).size())
|
||||
.sum();
|
||||
|
||||
// 회의 시간 계산
|
||||
Integer durationMinutes = null;
|
||||
if (meeting.getStartedAt() != null && meeting.getEndedAt() != null) {
|
||||
durationMinutes = (int) java.time.Duration.between(
|
||||
meeting.getStartedAt(),
|
||||
meeting.getEndedAt()
|
||||
).toMinutes();
|
||||
}
|
||||
|
||||
return MeetingStatisticsResponse.builder()
|
||||
.meetingId(meetingId)
|
||||
.meetingTitle(meeting.getTitle())
|
||||
.startTime(meeting.getStartedAt())
|
||||
.endTime(meeting.getEndedAt())
|
||||
.durationMinutes(durationMinutes)
|
||||
.participantCount(meeting.getParticipants() != null ? meeting.getParticipants().size() : 0)
|
||||
.agendaCount(sections.size())
|
||||
.todoCount(todoCount)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package com.unicorn.hgzero.meeting.infra.dto.ai;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 안건별 요약 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AgendaSummaryDTO {
|
||||
|
||||
/**
|
||||
* 안건 번호
|
||||
*/
|
||||
@JsonProperty("agenda_number")
|
||||
private Integer agendaNumber;
|
||||
|
||||
/**
|
||||
* 안건 제목
|
||||
*/
|
||||
@JsonProperty("agenda_title")
|
||||
private String agendaTitle;
|
||||
|
||||
/**
|
||||
* 짧은 요약 (1줄)
|
||||
*/
|
||||
@JsonProperty("summary_short")
|
||||
private String summaryShort;
|
||||
|
||||
/**
|
||||
* 안건별 회의록 요약
|
||||
* 사용자가 입력한 회의록 내용을 요약한 결과
|
||||
*/
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 보류 사항
|
||||
*/
|
||||
private List<String> pending;
|
||||
|
||||
/**
|
||||
* Todo 목록
|
||||
*/
|
||||
private List<ExtractedTodoDTO> todos;
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package com.unicorn.hgzero.meeting.infra.dto.ai;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI Service - 회의록 통합 요약 요청 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ConsolidateRequest {
|
||||
|
||||
/**
|
||||
* 회의 ID
|
||||
*/
|
||||
@JsonProperty("meeting_id")
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* 참석자별 회의록 목록
|
||||
*/
|
||||
@JsonProperty("participant_minutes")
|
||||
private List<ParticipantMinutesDTO> participantMinutes;
|
||||
|
||||
/**
|
||||
* 안건 목록 (선택)
|
||||
*/
|
||||
private List<String> agendas;
|
||||
|
||||
/**
|
||||
* 회의 시간(분) (선택)
|
||||
*/
|
||||
@JsonProperty("duration_minutes")
|
||||
private Integer durationMinutes;
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package com.unicorn.hgzero.meeting.infra.dto.ai;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI Service - 회의록 통합 요약 응답 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ConsolidateResponse {
|
||||
|
||||
/**
|
||||
* 회의 ID
|
||||
*/
|
||||
@JsonProperty("meeting_id")
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* 주요 키워드
|
||||
*/
|
||||
private List<String> keywords;
|
||||
|
||||
/**
|
||||
* 통계 정보
|
||||
* - participants_count: 참석자 수
|
||||
* - agendas_count: 안건 수
|
||||
* - todos_count: Todo 개수
|
||||
* - duration_minutes: 회의 시간(분)
|
||||
*/
|
||||
private Map<String, Integer> statistics;
|
||||
|
||||
/**
|
||||
* 회의 전체 결정사항
|
||||
*/
|
||||
private String decisions;
|
||||
|
||||
/**
|
||||
* 안건별 요약
|
||||
*/
|
||||
@JsonProperty("agenda_summaries")
|
||||
private List<AgendaSummaryDTO> agendaSummaries;
|
||||
|
||||
/**
|
||||
* 생성 시각
|
||||
*/
|
||||
@JsonProperty("generated_at")
|
||||
private LocalDateTime generatedAt;
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package com.unicorn.hgzero.meeting.infra.dto.ai;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* AI 추출 Todo DTO (제목만)
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ExtractedTodoDTO {
|
||||
|
||||
/**
|
||||
* Todo 제목
|
||||
*/
|
||||
private String title;
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.unicorn.hgzero.meeting.infra.dto.ai;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 참석자별 회의록 DTO (AI Service 요청용)
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ParticipantMinutesDTO {
|
||||
|
||||
/**
|
||||
* 사용자 ID
|
||||
*/
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 사용자 이름
|
||||
*/
|
||||
@JsonProperty("user_name")
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 회의록 전체 내용 (MEMO 섹션)
|
||||
*/
|
||||
private String content;
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
package com.unicorn.hgzero.meeting.infra.dto.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 안건별 섹션 조회 응답 DTO
|
||||
* 회의 종료 화면에서 사용
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "안건별 섹션 조회 응답")
|
||||
public class AgendaSectionResponse {
|
||||
|
||||
@Schema(description = "회의 ID", example = "MTG-2025-001")
|
||||
private String meetingId;
|
||||
|
||||
@Schema(description = "안건별 섹션 목록")
|
||||
private List<AgendaSectionItem> sections;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "안건별 섹션 항목")
|
||||
public static class AgendaSectionItem {
|
||||
|
||||
@Schema(description = "섹션 ID", example = "AGENDA-SEC-001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "회의록 ID", example = "MIN-CONSOLIDATED-001")
|
||||
private String minutesId;
|
||||
|
||||
@Schema(description = "안건 번호", example = "1")
|
||||
private Integer agendaNumber;
|
||||
|
||||
@Schema(description = "안건 제목", example = "Q1 마케팅 전략 수립")
|
||||
private String agendaTitle;
|
||||
|
||||
@Schema(description = "AI 생성 짧은 요약", example = "타겟 고객을 20-30대로 설정")
|
||||
private String aiSummaryShort;
|
||||
|
||||
@Schema(description = "안건별 회의록 요약", example = "신제품의 주요 타겟 고객층을 20-30대 직장인으로 설정하고...")
|
||||
private String summary;
|
||||
|
||||
@Schema(description = "보류 사항 목록")
|
||||
private List<String> pendingItems;
|
||||
|
||||
@Schema(description = "AI 추출 Todo 목록")
|
||||
private List<TodoItem> todos;
|
||||
|
||||
@Schema(description = "생성 시간", example = "2025-01-20T16:00:00")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "수정 시간", example = "2025-01-20T16:30:00")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Todo 항목")
|
||||
public static class TodoItem {
|
||||
|
||||
@Schema(description = "Todo 제목", example = "시장 조사 보고서 작성")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "담당자", example = "김민준")
|
||||
private String assignee;
|
||||
|
||||
@Schema(description = "마감일", example = "2025-02-15")
|
||||
private String dueDate;
|
||||
|
||||
@Schema(description = "설명", example = "20-30대 타겟 시장 조사")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "우선순위", example = "HIGH", allowableValues = {"HIGH", "MEDIUM", "LOW"})
|
||||
private String priority;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package com.unicorn.hgzero.meeting.infra.dto.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 회의 통계 조회 응답 DTO
|
||||
* 회의 종료 화면에서 통계 정보 표시
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "회의 통계 조회 응답")
|
||||
public class MeetingStatisticsResponse {
|
||||
|
||||
@Schema(description = "회의 ID", example = "MTG-2025-001")
|
||||
private String meetingId;
|
||||
|
||||
@Schema(description = "회의 제목", example = "Q1 마케팅 전략 회의")
|
||||
private String meetingTitle;
|
||||
|
||||
@Schema(description = "회의 시작 시간", example = "2025-01-20T14:00:00")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@Schema(description = "회의 종료 시간", example = "2025-01-20T16:00:00")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
@Schema(description = "회의 총 시간 (분)", example = "120")
|
||||
private Integer durationMinutes;
|
||||
|
||||
@Schema(description = "참석자 수", example = "5")
|
||||
private Integer participantCount;
|
||||
|
||||
@Schema(description = "안건 수", example = "3")
|
||||
private Integer agendaCount;
|
||||
|
||||
@Schema(description = "Todo 수", example = "12")
|
||||
private Integer todoCount;
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
package com.unicorn.hgzero.meeting.infra.dto.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 참석자별 회의록 조회 응답 DTO
|
||||
* AI Service가 통합 회의록 생성 시 사용
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "참석자별 회의록 조회 응답")
|
||||
public class ParticipantMinutesResponse {
|
||||
|
||||
@Schema(description = "회의 ID", example = "MTG-2025-001")
|
||||
private String meetingId;
|
||||
|
||||
@Schema(description = "참석자별 회의록 목록")
|
||||
private List<ParticipantMinutesItem> participantMinutes;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "참석자별 회의록 항목")
|
||||
public static class ParticipantMinutesItem {
|
||||
|
||||
@Schema(description = "회의록 ID", example = "MIN-001")
|
||||
private String minutesId;
|
||||
|
||||
@Schema(description = "작성자 사용자 ID", example = "user@example.com")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "회의록 제목", example = "Q1 마케팅 전략 회의 - 김민준 작성")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "회의록 상태", example = "FINALIZED", allowableValues = {"DRAFT", "FINALIZED"})
|
||||
private String status;
|
||||
|
||||
@Schema(description = "버전", example = "1")
|
||||
private Integer version;
|
||||
|
||||
@Schema(description = "작성자 ID", example = "user@example.com")
|
||||
private String createdBy;
|
||||
|
||||
@Schema(description = "생성 시간", example = "2025-01-20T14:30:00")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "최종 수정 시간", example = "2025-01-20T15:30:00")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime lastModifiedAt;
|
||||
|
||||
@Schema(description = "회의록 섹션 목록")
|
||||
private List<MinutesSection> sections;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "회의록 섹션")
|
||||
public static class MinutesSection {
|
||||
|
||||
@Schema(description = "섹션 ID", example = "SEC-001")
|
||||
private String sectionId;
|
||||
|
||||
@Schema(description = "섹션 제목", example = "주요 논의 사항")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "섹션 유형", example = "DISCUSSION", allowableValues = {"DISCUSSION", "DECISION", "ACTION_ITEM", "NOTE"})
|
||||
private String type;
|
||||
|
||||
@Schema(description = "섹션 내용", example = "Q1 마케팅 캠페인 방향성에 대한 논의가 진행되었습니다...")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "섹션 순서", example = "1")
|
||||
private Integer orderIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.AgendaSection;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.AgendaSectionReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.AgendaSectionWriter;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.AgendaSectionEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.AgendaSectionJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 안건별 회의록 섹션 Gateway 구현체
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Transactional(readOnly = true)
|
||||
public class AgendaSectionGateway implements AgendaSectionReader, AgendaSectionWriter {
|
||||
|
||||
private final AgendaSectionJpaRepository repository;
|
||||
|
||||
@Override
|
||||
public List<AgendaSection> findByMeetingId(String meetingId) {
|
||||
log.debug("회의 ID로 안건별 섹션 조회: {}", meetingId);
|
||||
return repository.findByMeetingIdOrderByAgendaNumberAsc(meetingId).stream()
|
||||
.map(AgendaSectionEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgendaSection> findByMinutesId(String minutesId) {
|
||||
log.debug("회의록 ID로 안건별 섹션 조회: {}", minutesId);
|
||||
return repository.findByMinutesIdOrderByAgendaNumberAsc(minutesId).stream()
|
||||
.map(AgendaSectionEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AgendaSection> findById(String id) {
|
||||
log.debug("ID로 안건별 섹션 조회: {}", id);
|
||||
return repository.findById(id)
|
||||
.map(AgendaSectionEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AgendaSection> findByMeetingIdAndAgendaNumber(String meetingId, Integer agendaNumber) {
|
||||
log.debug("회의 ID와 안건 번호로 섹션 조회: meetingId={}, agendaNumber={}", meetingId, agendaNumber);
|
||||
return Optional.ofNullable(repository.findByMeetingIdAndAgendaNumber(meetingId, agendaNumber))
|
||||
.map(AgendaSectionEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public AgendaSection save(AgendaSection section) {
|
||||
log.debug("안건별 섹션 저장: {}", section.getId());
|
||||
AgendaSectionEntity entity = AgendaSectionEntity.fromDomain(section);
|
||||
AgendaSectionEntity saved = repository.save(entity);
|
||||
return saved.toDomain();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public List<AgendaSection> saveAll(List<AgendaSection> sections) {
|
||||
log.debug("안건별 섹션 일괄 저장: {} 개", sections.size());
|
||||
List<AgendaSectionEntity> entities = sections.stream()
|
||||
.map(AgendaSectionEntity::fromDomain)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<AgendaSectionEntity> saved = repository.saveAll(entities);
|
||||
return saved.stream()
|
||||
.map(AgendaSectionEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void delete(String id) {
|
||||
log.debug("안건별 섹션 삭제: {}", id);
|
||||
repository.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void deleteByMeetingId(String meetingId) {
|
||||
log.debug("회의 ID로 안건별 섹션 전체 삭제: {}", meetingId);
|
||||
repository.deleteByMeetingId(meetingId);
|
||||
}
|
||||
}
|
||||
@ -64,6 +64,21 @@ public class MinutesGateway implements MinutesReader, MinutesWriter {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Minutes> findParticipantMinutesByMeetingId(String meetingId) {
|
||||
log.debug("회의 ID로 참석자별 회의록 조회: {}", meetingId);
|
||||
return minutesJpaRepository.findByMeetingIdAndUserIdIsNotNull(meetingId).stream()
|
||||
.map(MinutesEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Minutes> findConsolidatedMinutesByMeetingId(String meetingId) {
|
||||
log.debug("회의 ID로 AI 통합 회의록 조회: {}", meetingId);
|
||||
return minutesJpaRepository.findByMeetingIdAndUserIdIsNull(meetingId)
|
||||
.map(MinutesEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Minutes save(Minutes minutes) {
|
||||
// 기존 엔티티 조회 (update) 또는 새로 생성 (insert)
|
||||
|
||||
@ -38,6 +38,9 @@ public class MeetingAnalysisEntity {
|
||||
@Column(name = "keyword")
|
||||
private List<String> keywords;
|
||||
|
||||
@Column(name = "decisions", columnDefinition = "TEXT")
|
||||
private String decisions;
|
||||
|
||||
@Column(name = "agenda_analyses", columnDefinition = "TEXT")
|
||||
private String agendaAnalysesJson; // JSON 문자열로 저장
|
||||
|
||||
@ -55,12 +58,13 @@ public class MeetingAnalysisEntity {
|
||||
*/
|
||||
public MeetingAnalysis toDomain() {
|
||||
List<MeetingAnalysis.AgendaAnalysis> agendaAnalyses = parseAgendaAnalyses();
|
||||
|
||||
|
||||
return MeetingAnalysis.builder()
|
||||
.analysisId(this.analysisId)
|
||||
.meetingId(this.meetingId)
|
||||
.minutesId(this.minutesId)
|
||||
.keywords(this.keywords)
|
||||
.decisions(this.decisions)
|
||||
.agendaAnalyses(agendaAnalyses)
|
||||
.status(this.status)
|
||||
.completedAt(this.completedAt)
|
||||
@ -90,12 +94,13 @@ public class MeetingAnalysisEntity {
|
||||
*/
|
||||
public static MeetingAnalysisEntity fromDomain(MeetingAnalysis domain) {
|
||||
String agendaAnalysesJson = convertAgendaAnalysesToJson(domain.getAgendaAnalyses());
|
||||
|
||||
|
||||
return MeetingAnalysisEntity.builder()
|
||||
.analysisId(domain.getAnalysisId())
|
||||
.meetingId(domain.getMeetingId())
|
||||
.minutesId(domain.getMinutesId())
|
||||
.keywords(domain.getKeywords())
|
||||
.decisions(domain.getDecisions())
|
||||
.agendaAnalysesJson(agendaAnalysesJson)
|
||||
.status(domain.getStatus())
|
||||
.completedAt(domain.getCompletedAt())
|
||||
|
||||
@ -31,13 +31,12 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
@Column(name = "meeting_id", length = 50, nullable = false)
|
||||
private String meetingId;
|
||||
|
||||
@Column(name = "user_id", length = 100)
|
||||
private String userId;
|
||||
|
||||
@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";
|
||||
@ -49,6 +48,9 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
@Column(name = "created_by", length = 50, nullable = false)
|
||||
private String createdBy;
|
||||
|
||||
@Column(name = "decisions", columnDefinition = "TEXT")
|
||||
private String decisions;
|
||||
|
||||
@Column(name = "finalized_by", length = 50)
|
||||
private String finalizedBy;
|
||||
|
||||
@ -59,15 +61,15 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
return Minutes.builder()
|
||||
.minutesId(this.minutesId)
|
||||
.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)
|
||||
.createdAt(this.getCreatedAt())
|
||||
.lastModifiedAt(this.getUpdatedAt())
|
||||
.decisions(this.decisions)
|
||||
.finalizedBy(this.finalizedBy)
|
||||
.finalizedAt(this.finalizedAt)
|
||||
.build();
|
||||
@ -77,15 +79,12 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
return MinutesEntity.builder()
|
||||
.minutesId(minutes.getMinutesId())
|
||||
.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())
|
||||
.decisions(minutes.getDecisions())
|
||||
.finalizedBy(minutes.getFinalizedBy())
|
||||
.finalizedAt(minutes.getFinalizedAt())
|
||||
.build();
|
||||
|
||||
@ -10,6 +10,8 @@ import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 Entity
|
||||
* 참석자가 작성한 메모를 안건별로 저장
|
||||
* AI 분석의 입력 데이터로 사용됨
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "minutes_sections")
|
||||
@ -21,42 +23,35 @@ public class MinutesSectionEntity extends BaseTimeEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "id", length = 50)
|
||||
private String sectionId;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway.repository;
|
||||
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.AgendaSectionEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 안건별 회의록 섹션 JPA Repository
|
||||
*/
|
||||
@Repository
|
||||
public interface AgendaSectionJpaRepository extends JpaRepository<AgendaSectionEntity, String> {
|
||||
|
||||
/**
|
||||
* 회의 ID로 안건별 섹션 조회
|
||||
* 안건 번호 순으로 정렬
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return 안건별 섹션 목록
|
||||
*/
|
||||
List<AgendaSectionEntity> findByMeetingIdOrderByAgendaNumberAsc(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의록 ID로 안건별 섹션 조회
|
||||
*
|
||||
* @param minutesId 회의록 ID
|
||||
* @return 안건별 섹션 목록
|
||||
*/
|
||||
List<AgendaSectionEntity> findByMinutesIdOrderByAgendaNumberAsc(String minutesId);
|
||||
|
||||
/**
|
||||
* 회의 ID와 안건 번호로 섹션 조회
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @param agendaNumber 안건 번호
|
||||
* @return 안건 섹션
|
||||
*/
|
||||
AgendaSectionEntity findByMeetingIdAndAgendaNumber(String meetingId, Integer agendaNumber);
|
||||
|
||||
/**
|
||||
* 회의 ID로 안건별 섹션 삭제
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
*/
|
||||
void deleteByMeetingId(String meetingId);
|
||||
}
|
||||
@ -42,4 +42,15 @@ public interface MinutesJpaRepository extends JpaRepository<MinutesEntity, Strin
|
||||
* 회의 ID와 버전으로 회의록 조회
|
||||
*/
|
||||
Optional<MinutesEntity> findByMeetingIdAndVersion(String meetingId, Integer version);
|
||||
|
||||
/**
|
||||
* 회의 ID로 참석자별 회의록 조회 (user_id IS NOT NULL)
|
||||
* AI Service가 통합 회의록 생성 시 사용
|
||||
*/
|
||||
List<MinutesEntity> findByMeetingIdAndUserIdIsNotNull(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의 ID로 AI 통합 회의록 조회 (user_id IS NULL)
|
||||
*/
|
||||
Optional<MinutesEntity> findByMeetingIdAndUserIdIsNull(String meetingId);
|
||||
}
|
||||
|
||||
@ -0,0 +1,192 @@
|
||||
-- ========================================
|
||||
-- V3: 회의종료 기능을 위한 스키마 확장
|
||||
-- ========================================
|
||||
-- 작성일: 2025-10-28
|
||||
-- 설명: 참석자별 회의록, 안건별 섹션, AI 요약 결과 캐싱, Todo 자동 추출 지원
|
||||
|
||||
-- ========================================
|
||||
-- 1. minutes 테이블 확장
|
||||
-- ========================================
|
||||
-- 참석자별 회의록 지원 (user_id로 구분)
|
||||
-- user_id가 NULL이면 AI 통합 회의록, NOT NULL이면 참석자별 회의록
|
||||
|
||||
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);
|
||||
|
||||
-- 코멘트 추가
|
||||
COMMENT ON COLUMN minutes.user_id IS '작성자 사용자 ID (NULL: AI 통합 회의록, NOT NULL: 참석자별 회의록)';
|
||||
|
||||
-- ========================================
|
||||
-- 2. agenda_sections 테이블 생성
|
||||
-- ========================================
|
||||
-- 안건별 AI 요약 결과 저장
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agenda_sections (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
minutes_id VARCHAR(36) NOT NULL,
|
||||
meeting_id VARCHAR(50) NOT NULL,
|
||||
|
||||
-- 안건 정보
|
||||
agenda_number INT NOT NULL,
|
||||
agenda_title VARCHAR(200) NOT NULL,
|
||||
|
||||
-- AI 요약 결과
|
||||
ai_summary_short TEXT,
|
||||
discussions TEXT,
|
||||
decisions JSON,
|
||||
pending_items JSON,
|
||||
opinions JSON,
|
||||
|
||||
-- 메타데이터
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- 외래키
|
||||
CONSTRAINT fk_agenda_sections_minutes
|
||||
FOREIGN KEY (minutes_id) REFERENCES minutes(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT fk_agenda_sections_meeting
|
||||
FOREIGN KEY (meeting_id) REFERENCES meetings(meeting_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 인덱스 생성
|
||||
CREATE INDEX IF NOT EXISTS idx_sections_meeting ON agenda_sections(meeting_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sections_agenda ON agenda_sections(meeting_id, agenda_number);
|
||||
CREATE INDEX IF NOT EXISTS idx_sections_minutes ON agenda_sections(minutes_id);
|
||||
|
||||
-- 코멘트 추가
|
||||
COMMENT ON TABLE agenda_sections IS '안건별 회의록 섹션 - AI 요약 결과 저장';
|
||||
COMMENT ON COLUMN agenda_sections.id IS '섹션 고유 ID';
|
||||
COMMENT ON COLUMN agenda_sections.minutes_id IS '회의록 ID (통합 회의록 참조)';
|
||||
COMMENT ON COLUMN agenda_sections.meeting_id IS '회의 ID';
|
||||
COMMENT ON COLUMN agenda_sections.agenda_number IS '안건 번호 (1, 2, 3...)';
|
||||
COMMENT ON COLUMN agenda_sections.agenda_title IS '안건 제목';
|
||||
COMMENT ON COLUMN agenda_sections.ai_summary_short IS 'AI 생성 짧은 요약 (1줄, 20자 이내)';
|
||||
COMMENT ON COLUMN agenda_sections.discussions IS '논의 사항 (핵심 내용 3-5문장)';
|
||||
COMMENT ON COLUMN agenda_sections.decisions IS '결정 사항 배열 (JSON)';
|
||||
COMMENT ON COLUMN agenda_sections.pending_items IS '보류 사항 배열 (JSON)';
|
||||
COMMENT ON COLUMN agenda_sections.opinions IS '참석자별 의견 (JSON: [{speaker, opinion}])';
|
||||
|
||||
-- ========================================
|
||||
-- 3. ai_summaries 테이블 생성
|
||||
-- ========================================
|
||||
-- AI 요약 결과 캐싱 및 성능 최적화
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ai_summaries (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
meeting_id VARCHAR(50) NOT NULL,
|
||||
summary_type VARCHAR(50) NOT NULL,
|
||||
|
||||
-- 입력 정보
|
||||
source_minutes_ids JSON NOT NULL,
|
||||
|
||||
-- AI 처리 결과
|
||||
result JSON NOT NULL,
|
||||
processing_time_ms INT,
|
||||
model_version VARCHAR(50) DEFAULT 'claude-3.5-sonnet',
|
||||
|
||||
-- 통계 정보
|
||||
keywords JSON,
|
||||
statistics JSON,
|
||||
|
||||
-- 메타데이터
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- 외래키
|
||||
CONSTRAINT fk_ai_summaries_meeting
|
||||
FOREIGN KEY (meeting_id) REFERENCES meetings(meeting_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 인덱스 생성
|
||||
CREATE INDEX IF NOT EXISTS idx_summaries_meeting ON ai_summaries(meeting_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_summaries_type ON ai_summaries(meeting_id, summary_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_summaries_created ON ai_summaries(created_at);
|
||||
|
||||
-- 코멘트 추가
|
||||
COMMENT ON TABLE ai_summaries IS 'AI 요약 결과 캐시 테이블';
|
||||
COMMENT ON COLUMN ai_summaries.id IS '요약 결과 고유 ID';
|
||||
COMMENT ON COLUMN ai_summaries.meeting_id IS '회의 ID';
|
||||
COMMENT ON COLUMN ai_summaries.summary_type IS '요약 타입 (CONSOLIDATED: 통합 요약, TODO_EXTRACTION: Todo 추출)';
|
||||
COMMENT ON COLUMN ai_summaries.source_minutes_ids IS '통합에 사용된 회의록 ID 배열 (JSON)';
|
||||
COMMENT ON COLUMN ai_summaries.result IS 'AI 응답 전체 결과 (JSON)';
|
||||
COMMENT ON COLUMN ai_summaries.processing_time_ms IS 'AI 처리 시간 (밀리초)';
|
||||
COMMENT ON COLUMN ai_summaries.model_version IS '사용한 AI 모델 버전';
|
||||
COMMENT ON COLUMN ai_summaries.keywords IS '주요 키워드 배열 (JSON)';
|
||||
COMMENT ON COLUMN ai_summaries.statistics IS '통계 정보 (참석자 수, 안건 수 등, JSON)';
|
||||
|
||||
-- ========================================
|
||||
-- 4. todos 테이블 확장
|
||||
-- ========================================
|
||||
-- AI 자동 추출 정보 추가
|
||||
|
||||
ALTER TABLE todos
|
||||
ADD COLUMN IF NOT EXISTS extracted_by VARCHAR(50) DEFAULT 'AI',
|
||||
ADD COLUMN IF NOT EXISTS section_reference VARCHAR(200),
|
||||
ADD COLUMN IF NOT EXISTS extraction_confidence DECIMAL(3,2) DEFAULT 0.00;
|
||||
|
||||
-- 인덱스 추가
|
||||
CREATE INDEX IF NOT EXISTS idx_todos_extracted ON todos(extracted_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_todos_meeting ON todos(meeting_id);
|
||||
|
||||
-- 코멘트 추가
|
||||
COMMENT ON COLUMN todos.extracted_by IS 'Todo 추출 방법 (AI: AI 자동 추출, MANUAL: 사용자 수동 작성)';
|
||||
COMMENT ON COLUMN todos.section_reference IS '관련 회의록 섹션 참조 (예: "안건 1", "결정사항 #3")';
|
||||
COMMENT ON COLUMN todos.extraction_confidence IS 'AI 추출 신뢰도 점수 (0.00~1.00)';
|
||||
|
||||
-- ========================================
|
||||
-- 5. 제약조건 및 트리거 추가
|
||||
-- ========================================
|
||||
|
||||
-- updated_at 자동 업데이트 함수 (PostgreSQL)
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- agenda_sections 테이블에 updated_at 트리거 추가
|
||||
DROP TRIGGER IF EXISTS update_agenda_sections_updated_at ON agenda_sections;
|
||||
CREATE TRIGGER update_agenda_sections_updated_at
|
||||
BEFORE UPDATE ON agenda_sections
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- ========================================
|
||||
-- 6. 샘플 데이터 (개발 환경용)
|
||||
-- ========================================
|
||||
-- 실제 운영 환경에서는 주석 처리
|
||||
|
||||
-- 샘플 회의록 (참석자별)
|
||||
-- INSERT INTO minutes (id, meeting_id, user_id, content, created_at)
|
||||
-- VALUES
|
||||
-- ('sample-minutes-1', 'sample-meeting-1', 'user1@example.com', '회의록 내용 1...', CURRENT_TIMESTAMP),
|
||||
-- ('sample-minutes-2', 'sample-meeting-1', 'user2@example.com', '회의록 내용 2...', CURRENT_TIMESTAMP);
|
||||
|
||||
-- 샘플 통합 회의록 (user_id가 NULL)
|
||||
-- INSERT INTO minutes (id, meeting_id, user_id, content, created_at)
|
||||
-- VALUES
|
||||
-- ('sample-minutes-consolidated', 'sample-meeting-1', NULL, 'AI 통합 회의록...', CURRENT_TIMESTAMP);
|
||||
|
||||
-- 샘플 안건 섹션
|
||||
-- INSERT INTO agenda_sections (id, minutes_id, meeting_id, agenda_number, agenda_title, ai_summary_short, discussions, decisions, pending_items, opinions)
|
||||
-- VALUES
|
||||
-- ('sample-section-1', 'sample-minutes-consolidated', 'sample-meeting-1', 1, '신제품 기획 방향성',
|
||||
-- '타겟 고객을 20-30대로 설정...',
|
||||
-- '신제품의 주요 타겟 고객층을 20-30대 직장인으로 설정하고...',
|
||||
-- '["타겟 고객: 20-30대 직장인", "UI/UX 개선을 최우선 과제로 설정"]'::json,
|
||||
-- '[]'::json,
|
||||
-- '[{"speaker": "김민준", "opinion": "타겟 고객층을 명확히 설정하여 마케팅 전략 수립 필요"}]'::json);
|
||||
|
||||
-- ========================================
|
||||
-- 7. 권한 설정 (필요시)
|
||||
-- ========================================
|
||||
-- GRANT SELECT, INSERT, UPDATE, DELETE ON agenda_sections TO meeting_service_user;
|
||||
-- GRANT SELECT, INSERT, UPDATE, DELETE ON ai_summaries TO meeting_service_user;
|
||||
-- GRANT SELECT, INSERT, UPDATE, DELETE ON ai_summaries TO ai_service_user;
|
||||
@ -0,0 +1,37 @@
|
||||
-- ========================================
|
||||
-- V4: agenda_sections 테이블에 todos 컬럼 추가
|
||||
-- ========================================
|
||||
-- 작성일: 2025-10-28
|
||||
-- 설명: AI가 추출한 Todo를 안건별 섹션에 저장
|
||||
|
||||
-- ========================================
|
||||
-- 1. agenda_sections 테이블에 todos 컬럼 추가
|
||||
-- ========================================
|
||||
|
||||
ALTER TABLE agenda_sections
|
||||
ADD COLUMN IF NOT EXISTS todos JSON;
|
||||
|
||||
-- 코멘트 추가
|
||||
COMMENT ON COLUMN agenda_sections.todos IS 'AI 추출 Todo 목록 (JSON: [{title, assignee, dueDate, description, priority}])';
|
||||
|
||||
-- ========================================
|
||||
-- 2. 샘플 데이터 구조 (참고용)
|
||||
-- ========================================
|
||||
--
|
||||
-- todos JSON 구조:
|
||||
-- [
|
||||
-- {
|
||||
-- "title": "시장 조사 보고서 작성",
|
||||
-- "assignee": "김민준",
|
||||
-- "dueDate": "2025-02-15",
|
||||
-- "description": "20-30대 타겟 시장 조사",
|
||||
-- "priority": "HIGH"
|
||||
-- },
|
||||
-- {
|
||||
-- "title": "UI/UX 개선안 초안 작성",
|
||||
-- "assignee": "이서연",
|
||||
-- "dueDate": "2025-02-20",
|
||||
-- "description": "모바일 우선 UI 개선",
|
||||
-- "priority": "MEDIUM"
|
||||
-- }
|
||||
-- ]
|
||||
@ -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,58 @@
|
||||
-- ========================================
|
||||
-- V7: minutes 테이블에 decisions 추가 및 agenda_sections 리팩토링
|
||||
-- ========================================
|
||||
-- 작성일: 2025-10-29
|
||||
-- 설명:
|
||||
-- 1. minutes 테이블에 decisions (결정사항) 컬럼 추가
|
||||
-- 2. agenda_sections 테이블에서 discussions, decisions, opinions를 summary로 통합
|
||||
|
||||
-- ========================================
|
||||
-- 1. minutes 테이블에 decisions 컬럼 추가
|
||||
-- ========================================
|
||||
-- 회의 전체 결정사항을 TEXT 형식으로 저장
|
||||
|
||||
ALTER TABLE minutes
|
||||
ADD COLUMN IF NOT EXISTS decisions TEXT;
|
||||
|
||||
COMMENT ON COLUMN minutes.decisions IS '회의 전체 결정사항 (회의록 수정 시 입력)';
|
||||
|
||||
-- ========================================
|
||||
-- 2. agenda_sections 테이블 백업 및 데이터 마이그레이션
|
||||
-- ========================================
|
||||
-- 기존 데이터 보존을 위한 임시 백업 테이블 생성
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agenda_sections_backup AS
|
||||
SELECT * FROM agenda_sections;
|
||||
|
||||
-- ========================================
|
||||
-- 3. agenda_sections 테이블 컬럼 변경
|
||||
-- ========================================
|
||||
-- discussions, decisions, opinions → summary 통합
|
||||
|
||||
-- summary 컬럼 추가 (기존 discussions 내용으로 초기화)
|
||||
ALTER TABLE agenda_sections
|
||||
ADD COLUMN IF NOT EXISTS summary TEXT;
|
||||
|
||||
-- 기존 데이터 마이그레이션: discussions 내용을 summary로 복사
|
||||
UPDATE agenda_sections
|
||||
SET summary = COALESCE(discussions, '');
|
||||
|
||||
-- 기존 컬럼 삭제
|
||||
ALTER TABLE agenda_sections
|
||||
DROP COLUMN IF EXISTS discussions,
|
||||
DROP COLUMN IF EXISTS decisions,
|
||||
DROP COLUMN IF EXISTS opinions;
|
||||
|
||||
-- 코멘트 추가
|
||||
COMMENT ON COLUMN agenda_sections.summary IS '안건별 회의록 요약 (사용자가 입력한 회의록 내용을 AI가 요약한 결과)';
|
||||
|
||||
-- ========================================
|
||||
-- 4. 인덱스 및 트리거 유지
|
||||
-- ========================================
|
||||
-- 기존 인덱스 및 트리거는 그대로 유지됨
|
||||
|
||||
-- ========================================
|
||||
-- 5. 백업 테이블 정리 안내
|
||||
-- ========================================
|
||||
-- agenda_sections_backup 테이블은 수동으로 검증 후 삭제
|
||||
COMMENT ON TABLE agenda_sections_backup IS 'V7 마이그레이션 백업 - 검증 후 수동 삭제 필요';
|
||||
@ -0,0 +1,111 @@
|
||||
package com.unicorn.hgzero.meeting.manual;
|
||||
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
/**
|
||||
* 테스트 데이터 삽입 스크립트
|
||||
* 실행: ./gradlew :meeting:bootRun --args='--spring.profiles.active=test'
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = "com.unicorn.hgzero.meeting")
|
||||
public class InsertTestData {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(InsertTestData.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommandLineRunner insertData(JdbcTemplate jdbcTemplate) {
|
||||
return args -> {
|
||||
System.out.println("===== 테스트 데이터 삽입 시작 =====");
|
||||
|
||||
// 1. 참석자 회의록 삽입
|
||||
insertParticipantMinutes(jdbcTemplate);
|
||||
|
||||
// 2. 회의록 섹션 삽입
|
||||
insertMinutesSections(jdbcTemplate);
|
||||
|
||||
// 3. 데이터 확인
|
||||
verifyData(jdbcTemplate);
|
||||
|
||||
System.out.println("===== 테스트 데이터 삽입 완료 =====");
|
||||
};
|
||||
}
|
||||
|
||||
private void insertParticipantMinutes(JdbcTemplate jdbc) {
|
||||
System.out.println("참석자 회의록 삽입 중...");
|
||||
|
||||
String[] inserts = {
|
||||
"INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at) " +
|
||||
"VALUES ('minutes-user1', 'meeting-123', 'user-001', '참석자 홍길동 회의록', 'DRAFT', 1, 'user-001', NOW(), NOW()) " +
|
||||
"ON CONFLICT (minutes_id) DO NOTHING",
|
||||
|
||||
"INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at) " +
|
||||
"VALUES ('minutes-user2', 'meeting-123', 'user-002', '참석자 김철수 회의록', 'DRAFT', 1, 'user-002', NOW(), NOW()) " +
|
||||
"ON CONFLICT (minutes_id) DO NOTHING",
|
||||
|
||||
"INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at) " +
|
||||
"VALUES ('minutes-user3', 'meeting-123', 'user-003', '참석자 이영희 회의록', 'DRAFT', 1, 'user-003', NOW(), NOW()) " +
|
||||
"ON CONFLICT (minutes_id) DO NOTHING"
|
||||
};
|
||||
|
||||
for (String sql : inserts) {
|
||||
jdbc.execute(sql);
|
||||
}
|
||||
|
||||
System.out.println("참석자 회의록 삽입 완료");
|
||||
}
|
||||
|
||||
private void insertMinutesSections(JdbcTemplate jdbc) {
|
||||
System.out.println("회의록 섹션 삽입 중...");
|
||||
|
||||
// 참석자 1 섹션
|
||||
insertSection(jdbc, "minutes-user1", 1, "프로젝트 목표 논의",
|
||||
"고객사 요구사항이 명확하지 않아 추가 미팅 필요. 우선순위는 성능 개선으로 결정.");
|
||||
insertSection(jdbc, "minutes-user1", 2, "기술 스택 검토",
|
||||
"React와 Spring Boot로 진행하기로 결정. DB는 PostgreSQL 사용.");
|
||||
|
||||
// 참석자 2 섹션
|
||||
insertSection(jdbc, "minutes-user2", 1, "프로젝트 목표 논의",
|
||||
"성능 개선이 가장 중요. 응답시간 목표는 200ms 이내로 설정.");
|
||||
insertSection(jdbc, "minutes-user2", 2, "기술 스택 검토",
|
||||
"캐시 전략으로 Redis 도입 검토 필요. 모니터링 도구는 Prometheus 사용.");
|
||||
|
||||
// 참석자 3 섹션
|
||||
insertSection(jdbc, "minutes-user3", 1, "프로젝트 목표 논의",
|
||||
"고객사 담당자와 다음 주 화요일에 추가 미팅 예정. 요구사항 명세서 작성 필요.");
|
||||
insertSection(jdbc, "minutes-user3", 2, "기술 스택 검토",
|
||||
"UI 라이브러리는 Material-UI 사용. 백엔드는 MSA 아키텍처 검토.");
|
||||
|
||||
System.out.println("회의록 섹션 삽입 완료");
|
||||
}
|
||||
|
||||
private void insertSection(JdbcTemplate jdbc, String minutesId, int sectionNum, String title, String content) {
|
||||
String sql = "INSERT INTO minutes_sections (minutes_id, section_number, section_title, content, created_at) " +
|
||||
"SELECT id, ?, ?, ?, NOW() FROM minutes WHERE minutes_id = ? " +
|
||||
"ON CONFLICT DO NOTHING";
|
||||
|
||||
jdbc.update(sql, sectionNum, title, content, minutesId);
|
||||
}
|
||||
|
||||
private void verifyData(JdbcTemplate jdbc) {
|
||||
System.out.println("\n===== 데이터 확인 =====");
|
||||
|
||||
Integer minutesCount = jdbc.queryForObject(
|
||||
"SELECT COUNT(*) FROM minutes WHERE meeting_id = 'meeting-123' AND user_id IS NOT NULL",
|
||||
Integer.class
|
||||
);
|
||||
System.out.println("참석자 회의록 개수: " + minutesCount);
|
||||
|
||||
Integer sectionsCount = jdbc.queryForObject(
|
||||
"SELECT COUNT(*) FROM minutes_sections ms " +
|
||||
"JOIN minutes m ON ms.minutes_id = m.id " +
|
||||
"WHERE m.meeting_id = 'meeting-123'",
|
||||
Integer.class
|
||||
);
|
||||
System.out.println("회의록 섹션 개수: " + sectionsCount);
|
||||
}
|
||||
}
|
||||
130
meeting/src/test/resources/test-data-minutes-sections.sql
Normal file
130
meeting/src/test/resources/test-data-minutes-sections.sql
Normal file
@ -0,0 +1,130 @@
|
||||
-- 테스트용 참석자 회의록(minutes) 데이터
|
||||
-- userId가 있는 회의록들 (참석자별 메모)
|
||||
|
||||
-- 참석자 1의 회의록
|
||||
INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at)
|
||||
VALUES ('minutes-user1', 'meeting-123', 'user-001', '참석자 홍길동 회의록', 'DRAFT', 1, 'user-001', NOW(), NOW())
|
||||
ON CONFLICT (minutes_id) DO NOTHING;
|
||||
|
||||
-- 참석자 2의 회의록
|
||||
INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at)
|
||||
VALUES ('minutes-user2', 'meeting-123', 'user-002', '참석자 김철수 회의록', 'DRAFT', 1, 'user-002', NOW(), NOW())
|
||||
ON CONFLICT (minutes_id) DO NOTHING;
|
||||
|
||||
-- 참석자 3의 회의록
|
||||
INSERT INTO minutes (minutes_id, meeting_id, user_id, title, status, version, created_by, created_at, updated_at)
|
||||
VALUES ('minutes-user3', 'meeting-123', 'user-003', '참석자 이영희 회의록', 'DRAFT', 1, 'user-003', NOW(), NOW())
|
||||
ON CONFLICT (minutes_id) DO NOTHING;
|
||||
|
||||
-- minutes_sections 데이터 삽입
|
||||
-- Entity 구조에 맞게 수정: id, minutes_id, type, title, content, "order", verified, locked, locked_by
|
||||
|
||||
-- 참석자 1 (홍길동)의 메모
|
||||
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
|
||||
SELECT
|
||||
'section-user1-1',
|
||||
'minutes-user1',
|
||||
'AGENDA',
|
||||
'프로젝트 목표 논의',
|
||||
'고객사 요구사항이 명확하지 않아 추가 미팅 필요. 우선순위는 성능 개선으로 결정.',
|
||||
1,
|
||||
FALSE,
|
||||
FALSE,
|
||||
NULL,
|
||||
NOW(),
|
||||
NOW()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM minutes_sections WHERE id = 'section-user1-1'
|
||||
);
|
||||
|
||||
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
|
||||
SELECT
|
||||
'section-user1-2',
|
||||
'minutes-user1',
|
||||
'AGENDA',
|
||||
'기술 스택 검토',
|
||||
'React와 Spring Boot로 진행하기로 결정. DB는 PostgreSQL 사용.',
|
||||
2,
|
||||
FALSE,
|
||||
FALSE,
|
||||
NULL,
|
||||
NOW(),
|
||||
NOW()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM minutes_sections WHERE id = 'section-user1-2'
|
||||
);
|
||||
|
||||
-- 참석자 2 (김철수)의 메모
|
||||
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
|
||||
SELECT
|
||||
'section-user2-1',
|
||||
'minutes-user2',
|
||||
'AGENDA',
|
||||
'프로젝트 목표 논의',
|
||||
'성능 개선이 가장 중요. 응답시간 목표는 200ms 이내로 설정.',
|
||||
1,
|
||||
FALSE,
|
||||
FALSE,
|
||||
NULL,
|
||||
NOW(),
|
||||
NOW()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM minutes_sections WHERE id = 'section-user2-1'
|
||||
);
|
||||
|
||||
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
|
||||
SELECT
|
||||
'section-user2-2',
|
||||
'minutes-user2',
|
||||
'AGENDA',
|
||||
'기술 스택 검토',
|
||||
'캐시 전략으로 Redis 도입 검토 필요. 모니터링 도구는 Prometheus 사용.',
|
||||
2,
|
||||
FALSE,
|
||||
FALSE,
|
||||
NULL,
|
||||
NOW(),
|
||||
NOW()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM minutes_sections WHERE id = 'section-user2-2'
|
||||
);
|
||||
|
||||
-- 참석자 3 (이영희)의 메모
|
||||
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
|
||||
SELECT
|
||||
'section-user3-1',
|
||||
'minutes-user3',
|
||||
'AGENDA',
|
||||
'프로젝트 목표 논의',
|
||||
'고객사 담당자와 다음 주 화요일에 추가 미팅 예정. 요구사항 명세서 작성 필요.',
|
||||
1,
|
||||
FALSE,
|
||||
FALSE,
|
||||
NULL,
|
||||
NOW(),
|
||||
NOW()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM minutes_sections WHERE id = 'section-user3-1'
|
||||
);
|
||||
|
||||
INSERT INTO minutes_sections (id, minutes_id, type, title, content, "order", verified, locked, locked_by, created_at, updated_at)
|
||||
SELECT
|
||||
'section-user3-2',
|
||||
'minutes-user3',
|
||||
'AGENDA',
|
||||
'기술 스택 검토',
|
||||
'UI 라이브러리는 Material-UI 사용. 백엔드는 MSA 아키텍처 검토.',
|
||||
2,
|
||||
FALSE,
|
||||
FALSE,
|
||||
NULL,
|
||||
NOW(),
|
||||
NOW()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM minutes_sections WHERE id = 'section-user3-2'
|
||||
);
|
||||
|
||||
-- 확인 쿼리
|
||||
SELECT 'Test data inserted successfully!' as status;
|
||||
SELECT COUNT(*) as minutes_count FROM minutes WHERE meeting_id = 'meeting-123';
|
||||
SELECT COUNT(*) as sections_count FROM minutes_sections;
|
||||
1863
stt/logs/stt.log
1863
stt/logs/stt.log
File diff suppressed because it is too large
Load Diff
214
test-meeting-ai.sh
Executable file
214
test-meeting-ai.sh
Executable file
@ -0,0 +1,214 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Meeting AI 통합 테스트 스크립트
|
||||
# 작성: 이동욱
|
||||
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo "Meeting AI 통합 테스트"
|
||||
echo "=========================================="
|
||||
|
||||
# 색상 정의
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 테스트 변수
|
||||
MEETING_SERVICE="http://localhost:8082"
|
||||
AI_SERVICE="http://localhost:8087"
|
||||
USER_ID="test-user-001"
|
||||
USER_NAME="홍길동"
|
||||
USER_EMAIL="hong@example.com"
|
||||
|
||||
# 1. 서비스 Health Check
|
||||
echo ""
|
||||
echo "1️⃣ 서비스 Health Check..."
|
||||
echo "----------------------------------------"
|
||||
|
||||
echo -n "AI Python Service (8087): "
|
||||
if curl -s -f "$AI_SERVICE/health" > /dev/null; then
|
||||
echo -e "${GREEN}✓ 정상${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ 실패${NC}"
|
||||
echo "AI Python Service가 실행되지 않았습니다."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -n "Meeting Service (8082): "
|
||||
if curl -s -f "$MEETING_SERVICE/actuator/health" > /dev/null; then
|
||||
echo -e "${GREEN}✓ 정상${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ 실패${NC}"
|
||||
echo "Meeting Service가 실행되지 않았습니다."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. 회의 생성
|
||||
echo ""
|
||||
echo "2️⃣ 회의 생성..."
|
||||
echo "----------------------------------------"
|
||||
|
||||
MEETING_RESPONSE=$(curl -s -X POST "$MEETING_SERVICE/api/meetings" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-User-Id: $USER_ID" \
|
||||
-H "X-User-Name: $USER_NAME" \
|
||||
-H "X-User-Email: $USER_EMAIL" \
|
||||
-d '{
|
||||
"title": "AI 통합 테스트 회의",
|
||||
"purpose": "Meeting AI 기능 테스트",
|
||||
"scheduledAt": "2025-10-28T14:00:00",
|
||||
"endTime": "2025-10-28T15:00:00",
|
||||
"location": "회의실 A",
|
||||
"participantIds": ["'$USER_ID'", "user-002"]
|
||||
}')
|
||||
|
||||
MEETING_ID=$(echo "$MEETING_RESPONSE" | grep -o '"meetingId":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$MEETING_ID" ]; then
|
||||
echo -e "${RED}✗ 회의 생성 실패${NC}"
|
||||
echo "$MEETING_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ 회의 생성 성공${NC}"
|
||||
echo "Meeting ID: $MEETING_ID"
|
||||
|
||||
# 3. 회의 시작
|
||||
echo ""
|
||||
echo "3️⃣ 회의 시작..."
|
||||
echo "----------------------------------------"
|
||||
|
||||
START_RESPONSE=$(curl -s -X POST "$MEETING_SERVICE/api/meetings/$MEETING_ID/start" \
|
||||
-H "X-User-Id: $USER_ID" \
|
||||
-H "X-User-Name: $USER_NAME" \
|
||||
-H "X-User-Email: $USER_EMAIL")
|
||||
|
||||
if echo "$START_RESPONSE" | grep -q '"success":true'; then
|
||||
echo -e "${GREEN}✓ 회의 시작 성공${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ 회의 시작 실패${NC}"
|
||||
echo "$START_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4. 테스트 데이터 삽입 안내
|
||||
echo ""
|
||||
echo "4️⃣ 테스트 데이터 준비..."
|
||||
echo "----------------------------------------"
|
||||
echo -e "${YELLOW}⚠️ 수동 작업 필요${NC}"
|
||||
echo ""
|
||||
echo "PostgreSQL에 아래 SQL을 실행해주세요:"
|
||||
echo ""
|
||||
echo "psql -h 4.230.48.72 -U hgzerouser -d meetingdb"
|
||||
echo ""
|
||||
cat << 'SQL'
|
||||
INSERT INTO agenda_sections (
|
||||
id, minutes_id, meeting_id, agenda_number, agenda_title,
|
||||
ai_summary_short, discussions,
|
||||
decisions, pending_items, opinions, todos,
|
||||
created_at, updated_at
|
||||
) VALUES
|
||||
(
|
||||
'test-agenda-001', 'test-minutes-001', '여기에_MEETING_ID', 1, '신제품 기획 방향',
|
||||
NULL,
|
||||
'타겟 고객층을 20-30대 직장인으로 설정하고 UI/UX 개선에 집중하기로 논의했습니다. 모바일 우선 전략을 채택하고, 직관적인 인터페이스 디자인을 최우선 과제로 삼았습니다.',
|
||||
'["타겟 고객: 20-30대 직장인", "UI/UX 개선 최우선", "모바일 우선 전략 채택"]'::json,
|
||||
'["가격 정책 추가 검토 필요", "경쟁사 벤치마킹 분석"]'::json,
|
||||
'[]'::json,
|
||||
'[]'::json,
|
||||
NOW(), NOW()
|
||||
),
|
||||
(
|
||||
'test-agenda-002', 'test-minutes-001', '여기에_MEETING_ID', 2, '마케팅 전략 수립',
|
||||
NULL,
|
||||
'SNS 마케팅과 인플루언서 협업을 통한 브랜드 인지도 제고 방안을 논의했습니다. 인스타그램과 유튜브를 주요 채널로 선정하고, 마이크로 인플루언서 3명과 계약을 진행하기로 결정했습니다.',
|
||||
'["SNS 광고 집행 (Instagram, YouTube)", "인플루언서 3명과 계약", "월 500만원 마케팅 예산"]'::json,
|
||||
'["최종 예산 승인 대기", "인플루언서 선정 기준 확정"]'::json,
|
||||
'[]'::json,
|
||||
'[]'::json,
|
||||
NOW(), NOW()
|
||||
);
|
||||
SQL
|
||||
|
||||
echo ""
|
||||
echo "위 SQL에서 '여기에_MEETING_ID'를 아래 값으로 치환하세요:"
|
||||
echo -e "${GREEN}$MEETING_ID${NC}"
|
||||
echo ""
|
||||
echo -n "데이터 삽입 완료 후 Enter를 누르세요..."
|
||||
read
|
||||
|
||||
# 5. 회의 종료 (핵심 테스트)
|
||||
echo ""
|
||||
echo "5️⃣ 🔥 회의 종료 API 호출 (AI 통합 테스트)..."
|
||||
echo "----------------------------------------"
|
||||
|
||||
END_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$MEETING_SERVICE/api/meetings/$MEETING_ID/end" \
|
||||
-H "X-User-Id: $USER_ID" \
|
||||
-H "X-User-Name: $USER_NAME" \
|
||||
-H "X-User-Email: $USER_EMAIL")
|
||||
|
||||
HTTP_CODE=$(echo "$END_RESPONSE" | tail -n1)
|
||||
BODY=$(echo "$END_RESPONSE" | sed '$d')
|
||||
|
||||
if [ "$HTTP_CODE" -eq 200 ]; then
|
||||
echo -e "${GREEN}✓ 회의 종료 성공 (HTTP $HTTP_CODE)${NC}"
|
||||
echo ""
|
||||
echo "📊 응답 데이터:"
|
||||
echo "$BODY" | python3 -m json.tool 2>/dev/null || echo "$BODY"
|
||||
|
||||
# 주요 데이터 추출
|
||||
AGENDA_COUNT=$(echo "$BODY" | grep -o '"agendaCount":[0-9]*' | cut -d':' -f2)
|
||||
TODO_COUNT=$(echo "$BODY" | grep -o '"todoCount":[0-9]*' | cut -d':' -f2)
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ AI 분석 완료${NC}"
|
||||
echo " - 안건 수: $AGENDA_COUNT"
|
||||
echo " - Todo 수: $TODO_COUNT"
|
||||
else
|
||||
echo -e "${RED}✗ 회의 종료 실패 (HTTP $HTTP_CODE)${NC}"
|
||||
echo "$BODY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 6. 데이터베이스 검증
|
||||
echo ""
|
||||
echo "6️⃣ 데이터베이스 결과 확인..."
|
||||
echo "----------------------------------------"
|
||||
echo ""
|
||||
echo "PostgreSQL에서 아래 쿼리로 결과를 확인하세요:"
|
||||
echo ""
|
||||
cat << SQL
|
||||
-- 회의 상태 확인
|
||||
SELECT meeting_id, title, status, ended_at
|
||||
FROM meetings
|
||||
WHERE meeting_id = '$MEETING_ID';
|
||||
|
||||
-- AI 분석 결과 확인
|
||||
SELECT analysis_id, meeting_id, keywords, status, completed_at
|
||||
FROM meeting_analysis
|
||||
WHERE meeting_id = '$MEETING_ID';
|
||||
|
||||
-- Todo 확인
|
||||
SELECT todo_id, title, status
|
||||
FROM todos
|
||||
WHERE meeting_id = '$MEETING_ID';
|
||||
SQL
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo -e "${GREEN}✅ 통합 테스트 완료!${NC}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "📝 체크리스트:"
|
||||
echo " ✓ AI Python Service 실행"
|
||||
echo " ✓ Meeting Service 실행"
|
||||
echo " ✓ 회의 생성"
|
||||
echo " ✓ 회의 시작"
|
||||
echo " ✓ 회의 종료 + AI 분석"
|
||||
echo ""
|
||||
echo "📁 로그 위치:"
|
||||
echo " - AI Service: logs/ai-python.log"
|
||||
echo " - Meeting Service: meeting/logs/meeting-service.log"
|
||||
echo ""
|
||||
Loading…
x
Reference in New Issue
Block a user