for merge

This commit is contained in:
djeon 2025-10-29 15:33:31 +09:00
commit a84449e88d
92 changed files with 8906 additions and 5757 deletions

View File

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

6
.gitignore vendored
View File

@ -51,3 +51,9 @@ design/*/*back*
design/*back*
backup/
claudedocs/*back*
# Log files
logs/
**/logs/
*.log
**/*.log

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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"])

View 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
View 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()

View File

@ -0,0 +1,16 @@
"""Data Models"""
from .transcript import (
ConsolidateRequest,
ConsolidateResponse,
AgendaSummary,
ParticipantMinutes,
ExtractedTodo
)
__all__ = [
"ConsolidateRequest",
"ConsolidateResponse",
"AgendaSummary",
"ParticipantMinutes",
"ExtractedTodo",
]

View 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"
}
}

View 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"
}
}

View 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="생성 시각")

View 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

View 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()

View 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()

View File

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

File diff suppressed because it is too large Load Diff

58
ai-python/main.py Normal file
View 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()
)

View 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

View File

@ -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

View 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

View File

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

View File

@ -0,0 +1,607 @@
# Meeting Service 데이터베이스 스키마 분석 최종 보고서
**작성일**: 2025-10-28
**분석 대상**: Meeting Service (feat/meeting-ai 브랜치)
**분석 범위**: 마이그레이션 V1~V4, 엔티티 구조, 데이터 플로우
---
## Executive Summary
### 핵심 발견사항
1. **minutes 테이블에 content 필드가 없음**
- 실제 회의록 내용은 `minutes_sections.content`에 저장
- minutes 테이블은 메타데이터만 보유 (title, status, version 등)
2. **사용자별 회의록 완벽하게 지원 (V3)**
- `minutes.user_id = NULL`: AI 통합 회의록
- `minutes.user_id = 참석자ID`: 개인별 회의록
- 인덱스: `idx_minutes_meeting_user` (meeting_id, user_id)
3. **AI 분석 결과 구조화 저장 (V3, V4)**
- `agenda_sections`: 안건별 구조화된 요약
- `ai_summaries`: AI 처리 결과 캐싱
- `todos` (V4): 각 안건의 JSON으로 저장
4. **정규화 완료 (V2)**
- `meetings.participants` (CSV) → `meeting_participants` (테이블)
- 복합 PK: (meeting_id, user_id)
---
## 데이터베이스 구조 개요
### 테이블 분류
**핵심 테이블** (V1):
- `meetings`: 회의 기본 정보
- `minutes`: 회의록 메타데이터
- `minutes_sections`: 회의록 섹션 (실제 내용)
**참석자 관리** (V2):
- `meeting_participants`: 회의 참석자 정보
**AI 분석** (V3):
- `agenda_sections`: 안건별 AI 요약
- `ai_summaries`: AI 처리 결과 캐시
- `todos`: Todo 아이템 (expanded)
---
## 1. 핵심 테이블별 상세 분석
### 1.1 meetings (회의 기본 정보)
**구성**:
- PK: meeting_id (VARCHAR(50))
- 주요 필드: title, purpose, description
- 상태: SCHEDULED → IN_PROGRESS → COMPLETED
- 시간: scheduled_at, started_at, ended_at (V3)
**중요 변경**:
- V3에서 `ended_at` 추가
- 회의 정확한 종료 시간 기록
```sql
-- 조회 예시
SELECT * FROM meetings
WHERE status = 'COMPLETED'
AND ended_at >= NOW() - INTERVAL '7 days'
ORDER BY ended_at DESC;
```
---
### 1.2 minutes (회의록 메타데이터)
**구성**:
```
minutes_id (PK)
├─ meeting_id (FK)
├─ user_id (V3) ← NULL: AI 통합 회의록 / NOT NULL: 개인 회의록
├─ title
├─ status (DRAFT, FINALIZED)
├─ version
├─ created_by, finalized_by
└─ created_at, finalized_at
```
**중요**:
- **content 필드 없음** → minutes_sections에 저장
- 메타데이터만 관리 (생성자, 확정자, 버전 등)
**쿼리 패턴**:
```sql
-- AI 통합 회의록
SELECT * FROM minutes
WHERE meeting_id = ? AND user_id IS NULL;
-- 특정 사용자의 회의록
SELECT * FROM minutes
WHERE meeting_id = ? AND user_id = ?;
-- 복합 인덱스 활용: idx_minutes_meeting_user
```
---
### 1.3 minutes_sections (회의록 섹션 - 실제 내용)
**구성**:
```
section_id (PK)
├─ minutes_id (FK) ← 어느 회의록에 속하는가
├─ type (AGENDA, DISCUSSION, DECISION, ACTION_ITEM)
├─ title
├─ content ← ★ 실제 회의록 내용
├─ order
├─ verified (검증 완료)
├─ locked (수정 불가)
└─ locked_by
```
**핵심 특성**:
- **content**: 사용자가 작성한 실제 내용
- **locked**: finalize_minutes 호출시 잠금
- **verified**: 확정시 TRUE로 설정
**데이터 흐름**:
```
1. CreateMinutes → minutes_sections 초기 생성
2. UpdateMinutes → content 저장 (여러 번)
3. FinalizeMinutes → locked=TRUE, verified=TRUE
4. (locked 상태에서 수정 불가)
```
---
### 1.4 agenda_sections (AI 요약 - V3)
**구성**:
```
id (PK, UUID)
├─ minutes_id (FK) ← 통합 회의록만 (user_id=NULL)
├─ meeting_id (FK)
├─ agenda_number (1, 2, 3...)
├─ agenda_title
├─ ai_summary_short (1줄 요약)
├─ discussions (3-5문장 논의)
├─ decisions (JSON 배열)
├─ pending_items (JSON 배열)
├─ opinions (JSON 배열: {speaker, opinion})
└─ todos (JSON 배열 [V4])
```
**V4 추가 사항**:
- `todos` JSON 필드 추가
- 안건별 추출된 Todo 저장
```json
{
"title": "시장 조사 보고서 작성",
"assignee": "김민준",
"dueDate": "2025-02-15",
"description": "20-30대 타겟 시장 조사",
"priority": "HIGH"
}
```
**중요**:
- **통합 회의록만 분석** (user_id=NULL인 것)
- 참석자별 회의록(user_id NOT NULL)은 AI 분석 대상 아님
- minutes_id로 통합 회의록 참조
---
### 1.5 minutes_sections vs agenda_sections
| 항목 | minutes_sections | agenda_sections |
|------|-----------------|-----------------|
| **용도** | 사용자 작성 | AI 요약 |
| **모든 회의록** | ✓ 통합 + 개인 | ✗ 통합만 |
| **구조** | 순차적 섹션 | 안건별 구조화 |
| **내용 저장** | content (TEXT) | JSON 필드들 |
| **관계** | 1:N (minutes과) | N:1 (minutes과) |
| **목적** | 기록 | 분석/요약 |
**생성 흐름**:
```
회의 시작
minutes 생성 (통합 + 개인)
minutes_sections 생성 (4개 그룹)
사용자 작성 중...
회의 종료 → FinalizeMinutes
minutes_sections locked
AI 분석 (비동기)
agenda_sections 생성 (통합 회의록 기반)
```
---
### 1.6 ai_summaries (AI 처리 결과 - V3)
**구성**:
```
id (PK, UUID)
├─ meeting_id (FK)
├─ summary_type (CONSOLIDATED, TODO_EXTRACTION)
├─ source_minutes_ids (JSON: 사용된 회의록 ID 배열)
├─ result (JSON: AI 응답 전체)
├─ processing_time_ms (처리 시간)
├─ model_version (claude-3.5-sonnet)
├─ keywords (JSON: 키워드 배열)
└─ statistics (JSON: {participants, agendas, todos})
```
**용도**:
- AI 처리 결과 캐싱
- 재처리 필요시 참조
- 성능 통계 기록
---
### 1.7 todos (Todo 아이템)
**기본 구조**:
```
todo_id (PK)
├─ meeting_id (FK)
├─ minutes_id (FK)
├─ title
├─ description
├─ assignee_id
├─ due_date
├─ status (PENDING, COMPLETED)
├─ priority (HIGH, MEDIUM, LOW)
└─ completed_at
```
**V3 추가 필드**:
```
├─ extracted_by (AI / MANUAL) ← AI 자동 추출 vs 수동
├─ section_reference (안건 참조)
└─ extraction_confidence (0.00~1.00) ← AI 신뢰도
```
**저장 전략**:
1. `agenda_sections.todos` (JSON): 간단한 Todo, 기본 저장 위치
2. `todos` 테이블: 상세 관리 필요시만 추가 저장
---
### 1.8 meeting_participants (참석자 관리 - V2)
**구성**:
```
PK: (meeting_id, user_id)
├─ invitation_status (PENDING, ACCEPTED, DECLINED)
├─ attended (BOOLEAN)
└─ created_at, updated_at
```
**V2 개선**:
- 이전: meetings.participants (CSV 문자열)
- 현재: 별도 테이블 (정규화)
- 복합 PK로 중복 방지
---
## 2. 회의록 작성 플로우 (전체)
### 단계별 데이터 변화
```
PHASE 1: 회의 준비
═════════════════════════════════════════════
1. CreateMeeting
→ INSERT meetings (status='SCHEDULED')
→ INSERT meeting_participants (5명)
PHASE 2: 회의 진행
═════════════════════════════════════════════
2. StartMeeting
→ UPDATE meetings SET status='IN_PROGRESS'
3. CreateMinutes (회의 중)
→ INSERT minutes (user_id=NULL) × 1 (통합)
→ INSERT minutes (user_id=user_id) × 5 (개인)
→ INSERT minutes_sections (초기 생성)
4. UpdateMinutes (여러 번)
→ UPDATE minutes_sections SET content='...'
PHASE 3: 회의 종료
═════════════════════════════════════════════
5. EndMeeting
→ UPDATE meetings SET
status='COMPLETED',
ended_at=NOW() [V3]
PHASE 4: 회의록 최종화
═════════════════════════════════════════════
6. FinalizeMinutes
→ UPDATE minutes SET
status='FINALIZED'
→ UPDATE minutes_sections SET
locked=TRUE,
verified=TRUE
PHASE 5: AI 분석 (비동기)
═════════════════════════════════════════════
7. MinutesAnalysisEventConsumer
→ Read minutes (user_id=NULL)
→ Read minutes_sections
→ Call AI Service
→ INSERT agenda_sections [V3]
→ INSERT ai_summaries [V3]
→ INSERT todos [V3 확장]
```
---
## 3. 사용자별 회의록 구조
### 데이터 분리 방식
**1개 회의 (참석자 5명)**:
```
meetings: 1개
├─ meeting_id = 'meeting-001'
└─ status = COMPLETED
meeting_participants: 5개
├─ (meeting-001, user1@example.com)
├─ (meeting-001, user2@example.com)
├─ (meeting-001, user3@example.com)
├─ (meeting-001, user4@example.com)
└─ (meeting-001, user5@example.com)
minutes: 6개 [V3]
├─ (id=consol-1, meeting_id=meeting-001, user_id=NULL)
│ → 통합 회의록 (AI 분석 대상)
├─ (id=user1-min, meeting_id=meeting-001, user_id=user1@example.com)
│ → 사용자1 개인 회의록
├─ (id=user2-min, meeting_id=meeting-001, user_id=user2@example.com)
│ → 사용자2 개인 회의록
├─ ... (user3, user4, user5)
└─
minutes_sections: 수십 개 (6개 회의록 × N개 섹션)
├─ Group 1: consol-1의 섹션들 (AI 작성)
├─ Group 2: user1-min의 섹션들 (사용자1 작성)
├─ Group 3: user2-min의 섹션들 (사용자2 작성)
└─ ... (user3, user4, user5)
agenda_sections: 5개 [V3]
├─ (id=ag-1, minutes_id=consol-1, agenda_number=1)
├─ (id=ag-2, minutes_id=consol-1, agenda_number=2)
└─ ... (3, 4, 5)
```
**핵심**:
- 참석자별 회의록은 minutes.user_id로 구분
- 인덱스 활용: `idx_minutes_meeting_user`
- AI 분석: 통합 회의록만 (user_id=NULL)
---
## 4. 마이그레이션 변경사항 요약
### V2 (2025-10-27)
```sql
-- meeting_participants 테이블 생성
CREATE TABLE meeting_participants (
meeting_id, user_id (복합 PK),
invitation_status, attended
)
-- 데이터 마이그레이션
SELECT TRIM(participant) FROM meetings.participants (CSV)
→ INSERT INTO meeting_participants
-- meetings.participants 컬럼 삭제
ALTER TABLE meetings DROP COLUMN participants
```
**영향**: 정규화 완료, 중복 데이터 제거
---
### V3 (2025-10-28)
#### 3-1. minutes 테이블 확장
```sql
ALTER TABLE minutes ADD COLUMN user_id VARCHAR(100);
CREATE INDEX idx_minutes_meeting_user ON minutes(meeting_id, user_id);
```
**의미**: 사용자별 회의록 지원
---
#### 3-2. agenda_sections 테이블 신규
```sql
CREATE TABLE agenda_sections (
id, minutes_id, meeting_id,
agenda_number, agenda_title,
ai_summary_short, discussions,
decisions (JSON),
pending_items (JSON),
opinions (JSON)
)
```
**의미**: AI 요약을 구조화된 형식으로 저장
---
#### 3-3. ai_summaries 테이블 신규
```sql
CREATE TABLE ai_summaries (
id, meeting_id, summary_type,
source_minutes_ids (JSON),
result (JSON),
processing_time_ms,
model_version,
keywords (JSON),
statistics (JSON)
)
```
**의미**: AI 처리 결과 캐싱
---
#### 3-4. todos 테이블 확장
```sql
ALTER TABLE todos ADD COLUMN extracted_by VARCHAR(50) DEFAULT 'AI';
ALTER TABLE todos ADD COLUMN section_reference VARCHAR(200);
ALTER TABLE todos ADD COLUMN extraction_confidence DECIMAL(3,2);
```
**의미**: AI 자동 추출 추적
---
### V4 (2025-10-28)
```sql
ALTER TABLE agenda_sections ADD COLUMN todos JSON;
```
**의미**: 안건별 Todo를 JSON으로 저장
---
## 5. 성능 최적화
### 현재 인덱스
```
meetings:
├─ PK: meeting_id
minutes:
├─ PK: id
└─ idx_minutes_meeting_user (meeting_id, user_id) [V3]
minutes_sections:
├─ PK: id
└─ (minutes_id로 FK 지원)
agenda_sections: [V3]
├─ PK: id
├─ idx_sections_meeting (meeting_id)
├─ idx_sections_agenda (meeting_id, agenda_number)
└─ idx_sections_minutes (minutes_id)
ai_summaries: [V3]
├─ PK: id
├─ idx_summaries_meeting (meeting_id)
├─ idx_summaries_type (meeting_id, summary_type)
└─ idx_summaries_created (created_at)
todos:
├─ PK: todo_id
├─ idx_todos_extracted (extracted_by) [V3]
└─ idx_todos_meeting (meeting_id) [V3]
meeting_participants: [V2]
├─ PK: (meeting_id, user_id)
├─ idx_user_id (user_id)
└─ idx_invitation_status (invitation_status)
```
### 추천 추가 인덱스
```sql
-- 자주 조회하는 패턴
CREATE INDEX idx_minutes_status_created
ON minutes(status, created_at DESC);
CREATE INDEX idx_agenda_meeting_created
ON agenda_sections(meeting_id, created_at DESC);
CREATE INDEX idx_todos_meeting_assignee
ON todos(meeting_id, assignee_id);
```
---
## 6. 핵심 질문 답변
### Q1: minutes 테이블에 content 필드가 있는가?
**A**: **없음**
- minutes: 메타데이터만 (title, status, version 등)
- 실제 내용: minutes_sections.content
### Q2: minutes_section과 agenda_sections의 차이점?
**A**:
| 항목 | minutes_sections | agenda_sections |
|------|-----------------|-----------------|
| 목적 | 사용자 작성 | AI 요약 |
| 모든 회의록 | O | X (통합만) |
| 내용 저장 | content (TEXT) | JSON |
### Q3: 사용자별 회의록 저장 방식?
**A**:
- minutes.user_id로 구분
- NULL: AI 통합회의록
- NOT NULL: 개인별 회의록
- 인덱스: idx_minutes_meeting_user
### Q4: V3, V4 주요 변경?
**A**:
- V3: user_id, agenda_sections, ai_summaries, todos 확장
- V4: agenda_sections.todos JSON 추가
---
## 7. 개발 시 주의사항
### Do's ✓
- minutes_sections.content에 실제 내용 저장
- AI 분석시 user_id=NULL인 minutes만 처리
- agenda_sections.todos와 todos 테이블 동시 저장 (필요시)
- 복합 인덱스 활용 (meeting_id, user_id)
### Don'ts ✗
- minutes 테이블에 content 저장 (없음)
- 참석자별 회의록(user_id NOT NULL)을 AI 분석 (통합만)
- agenda_sections를 모든 minutes에 생성 (통합만)
- 인덱스 무시한 풀 스캔
---
## 8. 파일 위치 및 참조
**마이그레이션 파일**:
- `/Users/jominseo/HGZero/meeting/src/main/resources/db/migration/V2__*.sql`
- `/Users/jominseo/HGZero/meeting/src/main/resources/db/migration/V3__*.sql`
- `/Users/jominseo/HGZero/meeting/src/main/resources/db/migration/V4__*.sql`
**엔티티**:
- `MeetingEntity`, `MinutesEntity`, `MinutesSectionEntity`
- `AgendaSectionEntity`, `TodoEntity`, `MeetingParticipantEntity`
**서비스**:
- `MinutesService`, `MinutesSectionService`
- `MinutesAnalysisEventConsumer` (비동기)
---
## 9. 결론
### 핵심 설계 원칙
1. **메타데이터 vs 내용 분리**: minutes (메타) vs minutes_sections (내용)
2. **사용자별 격리**: user_id 컬럼으로 개인 회의록 관리
3. **AI 결과 구조화**: JSON으로 유연성과 성능 확보
4. **정규화 완료**: 참석자 정보 테이블화
### 검증 사항
- V3, V4 마이그레이션 정상 적용
- 모든 인덱스 생성됨
- 관계 설정 정상 (FK, 1:N)
### 다음 단계
- 성능 모니터링 (쿼리 실행 계획)
- 추가 인덱스 검토
- AI 분석 결과 검증
- 참석자별 회의록 사용성 테스트
---
**문서 정보**:
- 작성자: Database Architecture Analysis
- 대상 서비스: Meeting Service (AI 통합 회의록)
- 최종 버전: 2025-10-28

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

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

View File

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

View File

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

View 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
View 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

View 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)

View 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)

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
nohup: ./gradlew: No such file or directory

View File

@ -1 +0,0 @@
nohup: ./gradlew: No such file or directory

View 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;

View File

@ -37,6 +37,11 @@ public class MeetingAnalysis {
*/
private List<String> keywords;
/**
* 회의 전체 결정사항
*/
private String decisions;
/**
* 안건별 분석 결과
*/

View File

@ -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
*/

View File

@ -106,6 +106,11 @@ public class MinutesDTO {
*/
private final LocalDateTime lastModifiedAt;
/**
* 회의 전체 결정사항
*/
private final String decisions;
/**
* Todo 개수
*/

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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())

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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"
-- }
-- ]

View File

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

View File

@ -0,0 +1,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 마이그레이션 백업 - 검증 후 수동 삭제 필요';

View File

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

View File

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

File diff suppressed because it is too large Load Diff

214
test-meeting-ai.sh Executable file
View 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 ""