STT-AI 통합 작업 진행 중 변경사항 커밋

- AI 서비스 CORS 설정 업데이트
- 회의 진행 프로토타입 수정
- 빌드 리포트 및 로그 파일 업데이트

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Minseo-Jo
2025-10-27 13:17:47 +09:00
parent 9bf3597cec
commit 14d03dcacf
31 changed files with 9531 additions and 1036 deletions
+832
View File
@@ -0,0 +1,832 @@
# AI 서비스 개발 가이드
## 📋 **목차**
1. [AI 제안 기능 개발](#1-ai-제안-기능)
2. [용어 사전 기능 개발](#2-용어-사전-기능)
3. [관련 회의록 추천 기능 개발](#3-관련-회의록-추천-기능)
4. [백엔드 API 검증](#4-백엔드-api-검증)
5. [프롬프트 엔지니어링 가이드](#5-프롬프트-엔지니어링)
---
## 1. AI 제안 기능
### 📌 **기능 개요**
- **목적**: STT로 5초마다 수신되는 회의 텍스트를 분석하여 실시간 제안사항 제공
- **입력**: Redis에 축적된 최근 5분간의 회의 텍스트
- **출력**:
- 논의사항 (Discussions): 회의와 관련있고 추가 논의가 필요한 주제
- 결정사항 (Decisions): 명확한 의사결정 패턴이 감지된 내용
### ✅ **구현 완료 사항**
1. **SSE 스트리밍 엔드포인트**
- 파일: `ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/SuggestionController.java:111-131`
- 엔드포인트: `GET /api/suggestions/meetings/{meetingId}/stream`
- 프로토콜: Server-Sent Events (SSE)
2. **실시간 텍스트 처리**
- 파일: `ai/src/main/java/com/unicorn/hgzero/ai/biz/service/SuggestionService.java:120-140`
- Event Hub에서 TranscriptSegmentReady 이벤트 수신
- Redis에 최근 5분 텍스트 슬라이딩 윈도우 저장
3. **Claude API 통합**
- 파일: `ai/src/main/java/com/unicorn/hgzero/ai/infra/client/ClaudeApiClient.java`
- 모델: claude-3-5-sonnet-20241022
- 비동기 분석 및 JSON 파싱 구현
### 🔧 **개선 필요 사항**
#### 1.1 프롬프트 엔지니어링 개선
**현재 문제점**:
- 회의와 관련 없는 일상 대화도 분석될 가능성
- 회의 목적/안건 정보가 활용되지 않음
**개선 방법**:
```java
// ClaudeApiClient.java의 analyzeSuggestions 메서드 개선
public Mono<RealtimeSuggestionsDto> analyzeSuggestions(
String transcriptText,
String meetingPurpose, // 추가
List<String> agendaItems // 추가
) {
String systemPrompt = """
당신은 비즈니스 회의록 작성 전문 AI 어시스턴트입니다.
**역할**: 실시간 회의 텍스트를 분석하여 업무 관련 핵심 내용만 추출
**분석 기준**:
1. 논의사항 (discussions):
- 회의 안건과 관련된 미결 주제
- 추가 검토가 필요한 업무 항목
- "~에 대해 논의 필요", "~검토해야 함" 등의 패턴
- **제외**: 잡담, 농담, 인사말, 회의와 무관한 대화
2. 결정사항 (decisions):
- 명확한 의사결정 표현 ("~하기로 함", "~로 결정", "~로 합의")
- 구체적인 Action Item
- 책임자와 일정이 언급된 항목
**필터링 규칙**:
- 회의 목적/안건과 무관한 내용 제외
- 단순 질의응답이나 확인 대화 제외
- 업무 맥락이 명확한 내용만 추출
**응답 형식**: 반드시 JSON만 반환
{
"discussions": [
{
"topic": "구체적인 논의 주제 (회의 안건과 직접 연관)",
"reason": "회의 안건과의 연관성 설명",
"priority": "HIGH|MEDIUM|LOW",
"relatedAgenda": "관련 안건"
}
],
"decisions": [
{
"content": "결정된 내용",
"confidence": 0.9,
"extractedFrom": "원문 인용",
"actionOwner": "담당자 (언급된 경우)",
"deadline": "일정 (언급된 경우)"
}
]
}
""";
String userPrompt = String.format("""
**회의 정보**:
- 목적: %s
- 안건: %s
**회의 내용 (최근 5분)**:
%s
위 회의 목적과 안건에 맞춰 **회의와 관련된 내용만** 분석해주세요.
""",
meetingPurpose,
String.join(", ", agendaItems),
transcriptText
);
// 나머지 코드 동일
}
```
#### 1.2 회의 컨텍스트 조회 기능 추가
**구현 위치**: `SuggestionService.java`
```java
@RequiredArgsConstructor
public class SuggestionService implements SuggestionUseCase {
private final MeetingGateway meetingGateway; // 추가 필요
private void analyzeAndEmitSuggestions(String meetingId) {
// 1. 회의 정보 조회
MeetingInfo meetingInfo = meetingGateway.getMeetingInfo(meetingId);
String meetingPurpose = meetingInfo.getPurpose();
List<String> agendaItems = meetingInfo.getAgendaItems();
// 2. Redis에서 최근 5분 텍스트 조회
String key = "meeting:" + meetingId + ":transcript";
Set<String> recentTexts = redisTemplate.opsForZSet().reverseRange(key, 0, -1);
if (recentTexts == null || recentTexts.isEmpty()) {
return;
}
String accumulatedText = recentTexts.stream()
.map(entry -> entry.split(":", 2)[1])
.collect(Collectors.joining("\n"));
// 3. Claude API 분석 (회의 컨텍스트 포함)
claudeApiClient.analyzeSuggestions(
accumulatedText,
meetingPurpose, // 추가
agendaItems // 추가
)
.subscribe(
suggestions -> {
Sinks.Many<RealtimeSuggestionsDto> sink = meetingSinks.get(meetingId);
if (sink != null) {
sink.tryEmitNext(suggestions);
log.info("AI 제안사항 발행 완료 - meetingId: {}, 논의사항: {}, 결정사항: {}",
meetingId,
suggestions.getDiscussionTopics().size(),
suggestions.getDecisions().size());
}
},
error -> log.error("Claude API 분석 실패 - meetingId: {}", meetingId, error)
);
}
}
```
**필요한 Gateway 인터페이스**:
```java
package com.unicorn.hgzero.ai.biz.gateway;
public interface MeetingGateway {
MeetingInfo getMeetingInfo(String meetingId);
}
@Data
@Builder
public class MeetingInfo {
private String meetingId;
private String purpose;
private List<String> agendaItems;
}
```
---
## 2. 용어 사전 기능
### 📌 **기능 개요**
- **목적**: 회의 중 언급된 전문 용어를 맥락에 맞게 설명
- **입력**: 용어, 회의 컨텍스트
- **출력**:
- 기본 정의
- 회의 맥락에 맞는 설명
- 이전 회의에서의 사용 예시 (있는 경우)
### ✅ **구현 완료 사항**
1. **용어 감지 API**
- 파일: `ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/TermController.java:35-82`
- 엔드포인트: `POST /api/terms/detect`
2. **용어 설명 서비스 (Mock)**
- 파일: `ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TermExplanationService.java:24-53`
### 🔧 **개선 필요 사항**
#### 2.1 RAG 기반 용어 설명 구현
**구현 방법**:
```java
@Service
@RequiredArgsConstructor
public class TermExplanationService implements TermExplanationUseCase {
private final SearchGateway searchGateway;
private final ClaudeApiClient claudeApiClient; // 추가
@Override
public TermExplanationResult explainTerm(
String term,
String meetingId,
String context
) {
log.info("용어 설명 생성 - term: {}, meetingId: {}", term, meetingId);
// 1. RAG 검색: 이전 회의록에서 해당 용어 사용 사례 검색
List<TermUsageExample> pastUsages = searchGateway.searchTermUsages(
term,
meetingId
);
// 2. Claude API로 맥락 기반 설명 생성
String explanation = generateContextualExplanation(
term,
context,
pastUsages
);
return TermExplanationResult.builder()
.term(term)
.definition(explanation)
.context(context)
.pastUsages(pastUsages.stream()
.map(TermUsageExample::getDescription)
.collect(Collectors.toList()))
.build();
}
private String generateContextualExplanation(
String term,
String context,
List<TermUsageExample> pastUsages
) {
String prompt = String.format("""
다음 용어를 회의 맥락에 맞게 설명해주세요:
**용어**: %s
**현재 회의 맥락**: %s
**이전 회의에서의 사용 사례**:
%s
**설명 형식**:
1. 기본 정의 (1-2문장)
2. 현재 회의 맥락에서의 의미 (1-2문장)
3. 이전 논의 참고사항 (있는 경우)
비즈니스 맥락에 맞는 실용적인 설명을 제공하세요.
""",
term,
context,
formatPastUsages(pastUsages)
);
// Claude API 호출 (동기 방식)
return claudeApiClient.generateExplanation(prompt)
.block(); // 또는 비동기 처리
}
private String formatPastUsages(List<TermUsageExample> pastUsages) {
if (pastUsages.isEmpty()) {
return "이전 회의에서 언급된 적 없음";
}
return pastUsages.stream()
.map(usage -> String.format(
"- [%s] %s: %s",
usage.getMeetingDate(),
usage.getMeetingTitle(),
usage.getDescription()
))
.collect(Collectors.joining("\n"));
}
}
```
#### 2.2 Azure AI Search 통합 (RAG)
**SearchGateway 구현**:
```java
package com.unicorn.hgzero.ai.infra.search;
@Service
@RequiredArgsConstructor
public class AzureAiSearchGateway implements SearchGateway {
@Value("${external.ai-search.endpoint}")
private String endpoint;
@Value("${external.ai-search.api-key}")
private String apiKey;
@Value("${external.ai-search.index-name}")
private String indexName;
private final WebClient webClient;
@Override
public List<TermUsageExample> searchTermUsages(String term, String meetingId) {
// 1. 벡터 검색 쿼리 생성
Map<String, Object> searchRequest = Map.of(
"search", term,
"filter", String.format("meetingId ne '%s'", meetingId), // 현재 회의 제외
"top", 5,
"select", "meetingId,meetingTitle,meetingDate,content,score"
);
// 2. Azure AI Search API 호출
String response = webClient.post()
.uri(endpoint + "/indexes/" + indexName + "/docs/search?api-version=2023-11-01")
.header("api-key", apiKey)
.header("Content-Type", "application/json")
.bodyValue(searchRequest)
.retrieve()
.bodyToMono(String.class)
.block();
// 3. 응답 파싱
return parseSearchResults(response);
}
private List<TermUsageExample> parseSearchResults(String response) {
// JSON 파싱 로직
// Azure AI Search 응답 형식에 맞춰 파싱
return List.of(); // 구현 필요
}
}
```
---
## 3. 관련 회의록 추천 기능
### 📌 **기능 개요**
- **목적**: 현재 회의와 관련된 과거 회의록 추천
- **입력**: 회의 목적, 안건, 진행 중인 회의 내용
- **출력**: 관련도 점수와 함께 관련 회의록 목록
### ✅ **구현 완료 사항**
1. **관련 회의록 조회 API**
- 파일: `ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/RelationController.java:31-63`
- 엔드포인트: `GET /api/transcripts/{meetingId}/related`
2. **벡터 검색 서비스 (Mock)**
- 파일: `ai/src/main/java/com/unicorn/hgzero/ai/biz/service/RelatedTranscriptSearchService.java:27-47`
### 🔧 **개선 필요 사항**
#### 3.1 Azure AI Search 벡터 검색 구현
**구현 방법**:
```java
@Service
@RequiredArgsConstructor
public class RelatedTranscriptSearchService implements RelatedTranscriptSearchUseCase {
private final SearchGateway searchGateway;
private final MeetingGateway meetingGateway;
private final ClaudeApiClient claudeApiClient;
@Override
public List<RelatedMinutes> findRelatedTranscripts(
String meetingId,
String transcriptId,
int limit
) {
log.info("관련 회의록 검색 - meetingId: {}, limit: {}", meetingId, limit);
// 1. 현재 회의 정보 조회
MeetingInfo currentMeeting = meetingGateway.getMeetingInfo(meetingId);
String searchQuery = buildSearchQuery(currentMeeting);
// 2. Azure AI Search로 벡터 유사도 검색
List<SearchResult> searchResults = searchGateway.searchRelatedMeetings(
searchQuery,
meetingId,
limit
);
// 3. 검색 결과를 RelatedMinutes로 변환
return searchResults.stream()
.map(this::toRelatedMinutes)
.collect(Collectors.toList());
}
private String buildSearchQuery(MeetingInfo meeting) {
// 회의 목적 + 안건을 검색 쿼리로 변환
return String.format("%s %s",
meeting.getPurpose(),
String.join(" ", meeting.getAgendaItems())
);
}
private RelatedMinutes toRelatedMinutes(SearchResult result) {
return RelatedMinutes.builder()
.transcriptId(result.getMeetingId())
.title(result.getTitle())
.date(result.getDate())
.participants(result.getParticipants())
.relevanceScore(result.getScore() * 100) // 0-1 -> 0-100
.commonKeywords(extractCommonKeywords(result))
.summary(result.getSummary())
.link("/transcripts/" + result.getMeetingId())
.build();
}
}
```
#### 3.2 임베딩 기반 검색 구현
**Azure OpenAI Embedding 활용**:
```java
@Service
public class EmbeddingService {
@Value("${azure.openai.endpoint}")
private String endpoint;
@Value("${azure.openai.api-key}")
private String apiKey;
@Value("${azure.openai.embedding-deployment}")
private String embeddingDeployment;
private final WebClient webClient;
/**
* 텍스트를 벡터로 변환
*/
public float[] generateEmbedding(String text) {
Map<String, Object> request = Map.of(
"input", text
);
String response = webClient.post()
.uri(endpoint + "/openai/deployments/" + embeddingDeployment + "/embeddings?api-version=2024-02-15-preview")
.header("api-key", apiKey)
.header("Content-Type", "application/json")
.bodyValue(request)
.retrieve()
.bodyToMono(String.class)
.block();
// 응답에서 embedding 벡터 추출
return parseEmbedding(response);
}
private float[] parseEmbedding(String response) {
// JSON 파싱하여 float[] 반환
return new float[0]; // 구현 필요
}
}
```
**Azure AI Search 벡터 검색**:
```java
@Override
public List<SearchResult> searchRelatedMeetings(
String query,
String excludeMeetingId,
int limit
) {
// 1. 쿼리 텍스트를 벡터로 변환
float[] queryVector = embeddingService.generateEmbedding(query);
// 2. 벡터 검색 쿼리
Map<String, Object> searchRequest = Map.of(
"vector", Map.of(
"value", queryVector,
"fields", "contentVector",
"k", limit
),
"filter", String.format("meetingId ne '%s'", excludeMeetingId),
"select", "meetingId,title,date,participants,summary,score"
);
// 3. Azure AI Search API 호출
String response = webClient.post()
.uri(endpoint + "/indexes/" + indexName + "/docs/search?api-version=2023-11-01")
.header("api-key", apiKey)
.bodyValue(searchRequest)
.retrieve()
.bodyToMono(String.class)
.block();
return parseSearchResults(response);
}
```
---
## 4. 백엔드 API 검증
### 4.1 SSE 스트리밍 테스트
**테스트 방법**:
```bash
# 1. SSE 엔드포인트 연결
curl -N -H "Accept: text/event-stream" \
"http://localhost:8083/api/suggestions/meetings/test-meeting-001/stream"
# 예상 출력:
# event: ai-suggestion
# data: {"discussionTopics":[...],"decisions":[...]}
```
**프론트엔드 연동 예시** (JavaScript):
```javascript
// 05-회의진행.html에 추가
const meetingId = "test-meeting-001";
const eventSource = new EventSource(
`http://localhost:8083/api/suggestions/meetings/${meetingId}/stream`
);
eventSource.addEventListener('ai-suggestion', (event) => {
const suggestions = JSON.parse(event.data);
// 논의사항 표시
suggestions.discussionTopics.forEach(topic => {
addDiscussionCard(topic);
});
// 결정사항 표시
suggestions.decisions.forEach(decision => {
addDecisionCard(decision);
});
});
eventSource.onerror = (error) => {
console.error('SSE 연결 오류:', error);
eventSource.close();
};
function addDiscussionCard(topic) {
const card = document.createElement('div');
card.className = 'ai-suggestion-card';
card.innerHTML = `
<div class="ai-suggestion-header">
<span class="ai-suggestion-time">${new Date().toLocaleTimeString()}</span>
<span class="badge badge-${topic.priority.toLowerCase()}">${topic.priority}</span>
</div>
<div class="ai-suggestion-text">
<strong>[논의사항]</strong> ${topic.topic}
</div>
<div class="ai-suggestion-reason">${topic.reason}</div>
`;
document.getElementById('aiSuggestionList').prepend(card);
}
```
### 4.2 용어 설명 API 테스트
```bash
# POST /api/terms/detect
curl -X POST http://localhost:8083/api/terms/detect \
-H "Content-Type: application/json" \
-d '{
"meetingId": "test-meeting-001",
"text": "오늘 회의에서는 MSA 아키텍처와 API Gateway 설계에 대해 논의하겠습니다.",
"organizationId": "org-001"
}'
# 예상 응답:
{
"success": true,
"data": {
"detectedTerms": [
{
"term": "MSA",
"confidence": 0.95,
"category": "아키텍처",
"definition": "Microservices Architecture의 약자...",
"context": "회의 맥락에 맞는 설명..."
},
{
"term": "API Gateway",
"confidence": 0.92,
"category": "아키텍처"
}
],
"totalCount": 2
}
}
```
### 4.3 관련 회의록 API 테스트
```bash
# GET /api/transcripts/{meetingId}/related
curl "http://localhost:8083/api/transcripts/test-meeting-001/related?transcriptId=transcript-001&limit=5"
# 예상 응답:
{
"success": true,
"data": {
"relatedTranscripts": [
{
"transcriptId": "meeting-002",
"title": "MSA 아키텍처 설계 회의",
"date": "2025-01-15",
"participants": ["김철수", "이영희"],
"relevanceScore": 85.5,
"commonKeywords": ["MSA", "API Gateway", "Spring Boot"],
"link": "/transcripts/meeting-002"
}
],
"totalCount": 5
}
}
```
---
## 5. 프롬프트 엔지니어링 가이드
### 5.1 AI 제안사항 프롬프트
**핵심 원칙**:
1. **명확한 역할 정의**: AI의 역할을 "회의록 작성 전문가"로 명시
2. **구체적인 기준**: 무엇을 추출하고 무엇을 제외할지 명확히 명시
3. **컨텍스트 제공**: 회의 목적과 안건을 프롬프트에 포함
4. **구조화된 출력**: JSON 형식으로 파싱 가능한 응답 요청
**프롬프트 템플릿**:
```
당신은 비즈니스 회의록 작성 전문 AI 어시스턴트입니다.
**역할**: 실시간 회의 텍스트를 분석하여 업무 관련 핵심 내용만 추출
**분석 기준**:
1. 논의사항 (discussions):
- 회의 안건과 관련된 미결 주제
- 추가 검토가 필요한 업무 항목
- "~에 대해 논의 필요", "~검토해야 함" 등의 패턴
- **제외**: 잡담, 농담, 인사말, 회의와 무관한 대화
2. 결정사항 (decisions):
- 명확한 의사결정 표현 ("~하기로 함", "~로 결정", "~로 합의")
- 구체적인 Action Item
- 책임자와 일정이 언급된 항목
**필터링 규칙**:
- 회의 목적/안건과 무관한 내용 제외
- 단순 질의응답이나 확인 대화 제외
- 업무 맥락이 명확한 내용만 추출
**응답 형식**: 반드시 JSON만 반환
{
"discussions": [
{
"topic": "구체적인 논의 주제",
"reason": "회의 안건과의 연관성",
"priority": "HIGH|MEDIUM|LOW",
"relatedAgenda": "관련 안건"
}
],
"decisions": [
{
"content": "결정 내용",
"confidence": 0.9,
"extractedFrom": "원문 인용",
"actionOwner": "담당자 (있는 경우)",
"deadline": "일정 (있는 경우)"
}
]
}
---
**회의 정보**:
- 목적: {meeting_purpose}
- 안건: {agenda_items}
**회의 내용 (최근 5분)**:
{transcript_text}
위 회의 목적과 안건에 맞춰 **회의와 관련된 내용만** 분석해주세요.
```
### 5.2 용어 설명 프롬프트
```
다음 용어를 회의 맥락에 맞게 설명해주세요:
**용어**: {term}
**현재 회의 맥락**: {context}
**이전 회의에서의 사용 사례**:
{past_usages}
**설명 형식**:
1. 기본 정의 (1-2문장, 비전문가도 이해 가능하도록)
2. 현재 회의 맥락에서의 의미 (1-2문장, 이번 회의에서 이 용어가 어떤 의미로 쓰이는지)
3. 이전 논의 참고사항 (있는 경우, 과거 회의에서 관련 결정사항)
비즈니스 맥락에 맞는 실용적인 설명을 제공하세요.
```
### 5.3 관련 회의록 검색 쿼리 생성 프롬프트
```
현재 회의와 관련된 과거 회의록을 찾기 위한 검색 쿼리를 생성하세요.
**현재 회의 정보**:
- 목적: {meeting_purpose}
- 안건: {agenda_items}
- 진행 중인 주요 논의: {current_discussions}
**검색 쿼리 생성 규칙**:
1. 회의 목적과 안건에서 핵심 키워드 추출
2. 동의어와 관련 용어 포함
3. 너무 일반적인 단어는 제외 (예: "회의", "논의")
4. 5-10개의 키워드로 구성
**출력 형식**: 키워드를 공백으로 구분한 문자열
예시: "MSA 마이크로서비스 API게이트웨이 분산시스템 아키텍처설계"
```
---
## 6. 환경 설정
### 6.1 application.yml 확인 사항
```yaml
# Claude API 설정
external:
ai:
claude:
api-key: ${CLAUDE_API_KEY} # 환경변수 설정 필요
base-url: https://api.anthropic.com
model: claude-3-5-sonnet-20241022
max-tokens: 2000
temperature: 0.3
# Azure AI Search 설정
ai-search:
endpoint: ${AZURE_AI_SEARCH_ENDPOINT}
api-key: ${AZURE_AI_SEARCH_API_KEY}
index-name: meeting-transcripts
# Event Hub 설정
eventhub:
connection-string: ${AZURE_EVENTHUB_CONNECTION_STRING}
namespace: hgzero-eventhub-ns
eventhub-name: hgzero-eventhub-name
consumer-group:
transcript: ai-transcript-group
```
### 6.2 환경 변수 설정
```bash
# Claude API
export CLAUDE_API_KEY="sk-ant-..."
# Azure AI Search
export AZURE_AI_SEARCH_ENDPOINT="https://your-search-service.search.windows.net"
export AZURE_AI_SEARCH_API_KEY="your-api-key"
# Azure Event Hub
export AZURE_EVENTHUB_CONNECTION_STRING="Endpoint=sb://..."
```
---
## 7. 테스트 시나리오
### 7.1 전체 통합 테스트 시나리오
1. **회의 시작**
- 회의 생성 API 호출
- SSE 스트림 연결
2. **STT 텍스트 수신**
- Event Hub로 TranscriptSegmentReady 이벤트 발행
- Redis에 텍스트 축적 확인
3. **AI 제안사항 생성**
- 5분간 텍스트 축적
- Claude API 자동 호출
- SSE로 제안사항 수신 확인
4. **용어 설명 요청**
- 감지된 용어로 설명 API 호출
- 맥락에 맞는 설명 확인
5. **관련 회의록 조회**
- 관련 회의록 API 호출
- 유사도 점수 확인
---
## 8. 다음 단계
1.**MeetingGateway 구현**: Meeting 서비스와 통신하여 회의 정보 조회
2.**SearchGateway Azure AI Search 통합**: 벡터 검색 구현
3.**ClaudeApiClient 프롬프트 개선**: 회의 컨텍스트 활용
4.**프론트엔드 SSE 연동**: 05-회의진행.html에 SSE 클라이언트 추가
5.**통합 테스트**: 전체 플로우 동작 확인
+340
View File
@@ -0,0 +1,340 @@
# AI 제안사항 프론트엔드 연동 가이드 (간소화 버전)
## 📋 **개요**
백엔드를 간소화하여 **논의사항과 결정사항을 구분하지 않고**, 단일 "AI 제안사항" 배열로 통합 제공합니다.
---
## 🔄 **변경 사항 요약**
### **Before (구분)**
```json
{
"discussionTopics": [
{ "topic": "보안 요구사항 검토", ... }
],
"decisions": [
{ "content": "React로 개발", ... }
]
}
```
### **After (통합)** ✅
```json
{
"suggestions": [
{
"id": "sugg-001",
"content": "신제품의 타겟 고객층을 20-30대로 설정하고...",
"timestamp": "00:05:23",
"confidence": 0.92
}
]
}
```
---
## 🎨 **프론트엔드 통합 방법**
### **1. 05-회의진행.html에 스크립트 추가**
기존 `</body>` 태그 직전에 추가:
```html
<!-- AI 제안사항 SSE 연동 -->
<script src="ai-suggestion-integration.js"></script>
</body>
</html>
```
### **2. 전체 플로우**
```
[페이지 로드]
SSE 연결
[회의 진행 중]
AI 분석 완료 시마다
SSE로 제안사항 전송
자동으로 카드 생성
[회의 종료]
SSE 연결 종료
```
---
## 📡 **SSE API 명세**
### **엔드포인트**
```
GET /api/suggestions/meetings/{meetingId}/stream
```
### **헤더**
```
Accept: text/event-stream
```
### **응답 형식**
```
event: ai-suggestion
id: 12345
data: {"suggestions":[{"id":"sugg-001","content":"...","timestamp":"00:05:23","confidence":0.92}]}
event: ai-suggestion
id: 12346
data: {"suggestions":[{"id":"sugg-002","content":"...","timestamp":"00:08:45","confidence":0.88}]}
```
---
## 🧪 **테스트 방법**
### **1. 로컬 테스트 (Mock 데이터)**
백엔드가 아직 없어도 테스트 가능:
```javascript
// 테스트용 Mock 데이터 전송
function testAiSuggestion() {
const mockSuggestion = {
suggestions: [
{
id: "test-001",
content: "테스트 제안사항입니다. 이것은 AI가 생성한 제안입니다.",
timestamp: "00:05:23",
confidence: 0.95
}
]
};
handleAiSuggestions(mockSuggestion);
}
// 콘솔에서 실행
testAiSuggestion();
```
### **2. curl로 SSE 연결 테스트**
```bash
curl -N -H "Accept: text/event-stream" \
"http://localhost:8083/api/suggestions/meetings/test-meeting-001/stream"
```
예상 출력:
```
event: ai-suggestion
data: {"suggestions":[...]}
```
### **3. 브라우저 DevTools로 확인**
1. **Network 탭** → "EventStream" 필터
2. `/stream` 엔드포인트 클릭
3. **Messages** 탭에서 실시간 데이터 확인
---
## 💻 **JavaScript API 사용법**
### **초기화**
```javascript
// 자동으로 실행됨 (페이지 로드 시)
initializeAiSuggestions();
```
### **수동 연결 종료**
```javascript
closeAiSuggestions();
```
### **제안사항 수동 추가 (테스트용)**
```javascript
addSuggestionCard({
id: "manual-001",
content: "수동으로 추가한 제안사항",
timestamp: "00:10:00",
confidence: 0.9
});
```
---
## 🎨 **UI 커스터마이징**
### **신뢰도 표시 스타일**
```css
/* 05-회의진행.html의 <style> 태그에 추가 */
.ai-suggestion-confidence {
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed #E0E0E0;
}
.ai-suggestion-confidence span {
font-size: 11px;
color: var(--gray-500);
}
```
### **신뢰도에 따른 색상 변경**
```javascript
function getConfidenceColor(confidence) {
if (confidence >= 0.9) return '#4CAF50'; // 녹색 (높음)
if (confidence >= 0.7) return '#FFC107'; // 노란색 (중간)
return '#FF9800'; // 주황색 (낮음)
}
// 카드에 적용
card.innerHTML = `
...
<div class="ai-suggestion-confidence">
<span style="color: ${getConfidenceColor(suggestion.confidence)};">
신뢰도: ${Math.round(suggestion.confidence * 100)}%
</span>
</div>
`;
```
---
## 🔧 **트러블슈팅**
### **문제 1: SSE 연결이 안 됨**
**증상**:
```
EventSource's response has a MIME type ("application/json") that is not "text/event-stream"
```
**해결**:
- 백엔드에서 `produces = MediaType.TEXT_EVENT_STREAM_VALUE` 확인
- SuggestionController.java:111 라인 확인
### **문제 2: CORS 오류**
**증상**:
```
Access to XMLHttpRequest has been blocked by CORS policy
```
**해결**:
```java
// SecurityConfig.java에 추가
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
configuration.setAllowedHeaders(Arrays.asList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
```
### **문제 3: 제안사항이 화면에 안 나타남**
**체크리스트**:
1. `aiSuggestionList` ID가 HTML에 있는지 확인
2. 브라우저 콘솔에 에러가 없는지 확인
3. Network 탭에서 SSE 데이터가 오는지 확인
4. `handleAiSuggestions` 함수에 `console.log` 추가하여 디버깅
---
## 📊 **성능 최적화**
### **제안사항 개수 제한**
너무 많은 카드가 쌓이면 성능 저하:
```javascript
function addSuggestionCard(suggestion) {
// 카드 추가 로직...
// 최대 20개까지만 유지
const listElement = document.getElementById('aiSuggestionList');
const cards = listElement.querySelectorAll('.ai-suggestion-card');
if (cards.length > 20) {
cards[cards.length - 1].remove(); // 가장 오래된 카드 삭제
}
}
```
### **중복 제안사항 필터링**
```javascript
const shownSuggestionIds = new Set();
function addSuggestionCard(suggestion) {
// 이미 표시된 제안사항은 무시
if (shownSuggestionIds.has(suggestion.id)) {
console.log('중복 제안사항 무시:', suggestion.id);
return;
}
shownSuggestionIds.add(suggestion.id);
// 카드 추가 로직...
}
```
---
## 🚀 **다음 단계**
1.**SimpleSuggestionDto 생성 완료**
2.**RealtimeSuggestionsDto 수정 완료**
3.**ClaudeApiClient 프롬프트 간소화 완료**
4.**SuggestionService 로직 수정 완료**
5.**프론트엔드 연동 코드 작성 완료**
### **실제 테스트 준비**
1. **백엔드 서버 시작**
```bash
cd ai
./gradlew bootRun
```
2. **프론트엔드 파일 열기**
```
design/uiux/prototype/05-회의진행.html
```
3. **브라우저 DevTools 열고 Network 탭 확인**
4. **SSE 연결 확인**
- EventStream 필터 활성화
- `/stream` 엔드포인트 확인
---
## 📝 **완료 체크리스트**
- [x] SimpleSuggestionDto 생성
- [x] RealtimeSuggestionsDto 수정
- [x] ClaudeApiClient 프롬프트 간소화
- [x] SuggestionService Mock 데이터 수정
- [x] 프론트엔드 연동 JavaScript 작성
- [ ] 05-회의진행.html에 스크립트 추가
- [ ] 로컬 환경에서 테스트
- [ ] Claude API 실제 연동 테스트
---
**🎉 간소화 작업 완료!**
이제 프론트엔드와 백엔드가 일치합니다. 05-회의진행.html에 스크립트만 추가하면 바로 사용 가능합니다.
+385
View File
@@ -0,0 +1,385 @@
# 실시간 AI 제안 스트리밍 개발 가이드
## 📋 개요
회의 진행 중 STT로 변환된 텍스트를 실시간으로 분석하여 논의사항/결정사항을 AI가 제안하는 기능
**개발 일시**: 2025-10-24
**개발자**: AI Specialist (서연)
**사용 기술**: Claude API, Azure Event Hub, Redis, SSE (Server-Sent Events)
---
## 🎯 구현된 기능
### ✅ **1. Claude API 클라이언트**
- **파일**: `ai/src/main/java/com/unicorn/hgzero/ai/infra/client/ClaudeApiClient.java`
- **기능**:
- Anthropic Claude API (claude-3-5-sonnet) 호출
- 실시간 텍스트 분석하여 논의사항/결정사항 추출
- JSON 응답 파싱 및 DTO 변환
### ✅ **2. Azure Event Hub Consumer**
- **파일**: `ai/src/main/java/com/unicorn/hgzero/ai/infra/config/EventHubConfig.java`
- **기능**:
- STT Service의 `TranscriptSegmentReady` 이벤트 구독
- 실시간 음성 변환 텍스트 수신
- SuggestionService로 전달하여 AI 분석 트리거
### ✅ **3. 실시간 텍스트 축적 로직**
- **파일**: `ai/src/main/java/com/unicorn/hgzero/ai/biz/service/SuggestionService.java`
- **메서드**: `processRealtimeTranscript()`
- **기능**:
- Redis Sorted Set을 이용한 슬라이딩 윈도우 (최근 5분 텍스트 유지)
- 임계값 도달 시 자동 AI 분석 (10개 세그먼트 = 약 100-200자)
### ✅ **4. SSE 스트리밍**
- **API**: `GET /api/suggestions/meetings/{meetingId}/stream`
- **Controller**: `SuggestionController:111`
- **기능**:
- Server-Sent Events로 실시간 AI 제안사항 전송
- 멀티캐스트 지원 (여러 클라이언트 동시 연결)
- 자동 리소스 정리 (연결 종료 시)
---
## 🏗️ 아키텍처
```
[회의 진행 중]
┌─────────────────────────────────────┐
│ 1. STT Service (Azure Speech) │
│ - 음성 → 텍스트 실시간 변환 │
└─────────────────────────────────────┘
↓ Azure Event Hub
↓ (TranscriptSegmentReady Event)
┌─────────────────────────────────────┐
│ 2. AI Service (Event Hub Consumer) │
│ - 이벤트 수신 │
│ - Redis에 텍스트 축적 │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 3. Redis (슬라이딩 윈도우) │
│ - 최근 5분 텍스트 유지 │
│ - 임계값 체크 (10 segments) │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 4. Claude API (Anthropic) │
│ - 누적 텍스트 분석 │
│ - 논의사항/결정사항 추출 │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 5. SSE 스트리밍 │
│ - 클라이언트에 실시간 전송 │
└─────────────────────────────────────┘
```
---
## ⚙️ 설정 방법
### **1. Claude API 키 발급**
1. [Anthropic Console](https://console.anthropic.com/) 접속
2. API Keys → Create Key
3. 생성된 API Key 복사
### **2. 환경 변수 설정**
**application.yml** 또는 **환경 변수**에 추가:
```bash
# Claude API 설정
export CLAUDE_API_KEY="sk-ant-api03-..."
export CLAUDE_MODEL="claude-3-5-sonnet-20241022"
export CLAUDE_MAX_TOKENS="2000"
export CLAUDE_TEMPERATURE="0.3"
# Azure Event Hub 설정 (이미 설정됨)
export AZURE_EVENTHUB_CONNECTION_STRING="Endpoint=sb://hgzero-eventhub-ns.servicebus.windows.net/;..."
export AZURE_EVENTHUB_NAME="hgzero-eventhub-name"
export AZURE_EVENTHUB_CONSUMER_GROUP_TRANSCRIPT="ai-transcript-group"
# Redis 설정 (이미 설정됨)
export REDIS_HOST="20.249.177.114"
export REDIS_PORT="6379"
export REDIS_PASSWORD="Hi5Jessica!"
export REDIS_DATABASE="4"
```
### **3. 의존성 확인**
`ai/build.gradle`에 이미 추가됨:
```gradle
dependencies {
// Common module
implementation project(':common')
// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// Anthropic Claude SDK
implementation 'com.anthropic:anthropic-sdk-java:0.1.0'
// Azure Event Hubs
implementation "com.azure:azure-messaging-eventhubs:${azureEventHubsVersion}"
implementation "com.azure:azure-messaging-eventhubs-checkpointstore-blob:${azureEventHubsCheckpointVersion}"
// Spring WebFlux for SSE streaming
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
```
---
## 🚀 실행 방법
### **1. AI Service 빌드**
```bash
cd /Users/jominseo/HGZero
./gradlew :ai:build -x test
```
### **2. AI Service 실행**
```bash
cd ai
./gradlew bootRun
```
또는 IntelliJ Run Configuration 사용
### **3. 클라이언트 테스트 (회의진행.html)**
```javascript
// SSE 연결
const meetingId = "MTG-2025-001";
const eventSource = new EventSource(`/api/suggestions/meetings/${meetingId}/stream`);
// AI 제안사항 수신
eventSource.addEventListener('ai-suggestion', (event) => {
const suggestion = JSON.parse(event.data);
console.log('실시간 AI 제안:', suggestion);
// 논의사항 UI 업데이트
suggestion.discussionTopics.forEach(topic => {
addDiscussionToUI(topic);
});
// 결정사항 UI 업데이트
suggestion.decisions.forEach(decision => {
addDecisionToUI(decision);
});
});
// 에러 처리
eventSource.onerror = (error) => {
console.error('SSE 연결 오류:', error);
eventSource.close();
};
// 회의 종료 시 연결 종료
function endMeeting() {
eventSource.close();
}
```
---
## 📊 데이터 흐름
### **Event Hub 이벤트 구조**
```json
{
"recordingId": "REC-20250123-001",
"meetingId": "MTG-2025-001",
"transcriptId": "TRS-SEG-001",
"text": "안녕하세요, 오늘 회의를 시작하겠습니다.",
"timestamp": 1234567890,
"confidence": 0.92,
"eventTime": "2025-01-23T10:30:00Z"
}
```
### **Claude API 응답 구조**
```json
{
"discussions": [
{
"topic": "보안 요구사항 검토",
"reason": "안건에 포함되어 있으나 아직 논의되지 않음",
"priority": "HIGH"
}
],
"decisions": [
{
"content": "React로 프론트엔드 개발",
"confidence": 0.9,
"extractedFrom": "프론트엔드는 React로 개발하기로 했습니다"
}
]
}
```
### **SSE 스트리밍 응답**
```
event: ai-suggestion
id: 12345
data: {"discussionTopics":[...],"decisions":[...]}
event: ai-suggestion
id: 12346
data: {"discussionTopics":[...],"decisions":[...]}
```
---
## 🔧 주요 설정값
| 설정 | 값 | 설명 |
|------|-----|------|
| `MIN_SEGMENTS_FOR_ANALYSIS` | 10 | AI 분석 시작 임계값 (세그먼트 수) |
| `TEXT_RETENTION_MS` | 300000 (5분) | Redis 텍스트 보관 기간 |
| `CLAUDE_MODEL` | claude-3-5-sonnet-20241022 | 사용 Claude 모델 |
| `CLAUDE_MAX_TOKENS` | 2000 | 최대 응답 토큰 수 |
| `CLAUDE_TEMPERATURE` | 0.3 | 창의성 수준 (0-1) |
---
## 🐛 트러블슈팅
### **1. Event Hub 연결 실패**
**증상**: `Event Hub Processor 시작 실패` 로그
**해결**:
```bash
# 연결 문자열 확인
echo $AZURE_EVENTHUB_CONNECTION_STRING
# Consumer Group 확인
echo $AZURE_EVENTHUB_CONSUMER_GROUP_TRANSCRIPT
```
### **2. Claude API 호출 실패**
**증상**: `Claude API 호출 실패` 로그
**해결**:
```bash
# API 키 확인
echo $CLAUDE_API_KEY
# 네트워크 연결 확인
curl -X POST https://api.anthropic.com/v1/messages \
-H "x-api-key: $CLAUDE_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json"
```
### **3. Redis 연결 실패**
**증상**: `Unable to connect to Redis` 로그
**해결**:
```bash
# Redis 연결 테스트
redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASSWORD ping
# 응답: PONG
```
### **4. SSE 스트림 끊김**
**증상**: 클라이언트에서 연결이 자주 끊김
**해결**:
```javascript
// 자동 재연결 로직 추가
function connectSSE(meetingId) {
const eventSource = new EventSource(`/api/suggestions/meetings/${meetingId}/stream`);
eventSource.onerror = (error) => {
console.error('SSE 연결 오류, 5초 후 재연결...');
eventSource.close();
setTimeout(() => connectSSE(meetingId), 5000);
};
return eventSource;
}
```
---
## 📈 성능 최적화
### **1. Redis 메모리 관리**
- 슬라이딩 윈도우로 최근 5분만 유지
- 회의 종료 시 자동 삭제
- TTL 설정 고려 (향후 추가)
### **2. Claude API 호출 최적화**
- 임계값 도달 시에만 호출 (불필요한 호출 방지)
- 비동기 처리로 응답 대기 시간 최소화
- 에러 발생 시 빈 응답 반환 (서비스 중단 방지)
### **3. SSE 연결 관리**
- 멀티캐스트로 여러 클라이언트 동시 지원
- 연결 종료 시 자동 리소스 정리
- Backpressure 버퍼링으로 과부하 방지
---
## 🔜 향후 개발 계획
### **Phase 2: AI 정확도 향상**
- [ ] 회의 안건 기반 맥락 분석
- [ ] 과거 회의록 참조 (RAG)
- [ ] 조직별 용어 사전 통합
### **Phase 3: 성능 개선**
- [ ] Redis TTL 자동 설정
- [ ] Claude API 캐싱 전략
- [ ] 배치 분석 옵션 추가
### **Phase 4: 모니터링**
- [ ] AI 제안 정확도 측정
- [ ] 응답 시간 메트릭 수집
- [ ] 사용량 대시보드 구축
---
## 📚 참고 자료
- [Anthropic Claude API 문서](https://docs.anthropic.com/claude/reference/messages)
- [Azure Event Hubs 문서](https://learn.microsoft.com/en-us/azure/event-hubs/)
- [Server-Sent Events 스펙](https://html.spec.whatwg.org/multipage/server-sent-events.html)
- [Redis Sorted Sets 가이드](https://redis.io/docs/data-types/sorted-sets/)
---
## ✅ 체크리스트
- [x] Claude API 클라이언트 구현
- [x] Azure Event Hub Consumer 구현
- [x] Redis 슬라이딩 윈도우 구현
- [x] SSE 스트리밍 구현
- [x] SuggestionService 통합
- [ ] Claude API 키 발급 및 설정
- [ ] 통합 테스트 (STT → AI → SSE)
- [ ] 프론트엔드 연동 테스트
---
**개발 완료**: 2025-10-24
**다음 단계**: Claude API 키 발급 및 통합 테스트
+400
View File
@@ -0,0 +1,400 @@
# AI 샘플 데이터 통합 가이드
## 개요
AI 서비스 개발이 완료되지 않은 상황에서 프론트엔드 개발을 병행하기 위해 **샘플 데이터 자동 발행 기능**을 구현했습니다.
### 목적
- 프론트엔드 개발자가 AI 기능 완성을 기다리지 않고 화면 개발 가능
- 실시간 SSE(Server-Sent Events) 스트리밍 동작 테스트
- 회의 진행 중 AI 제안사항 표시 기능 검증
### 주요 기능
- **백엔드**: AI Service에서 5초마다 샘플 제안사항 3개 자동 발행
- **프론트엔드**: EventSource API를 통한 실시간 데이터 수신 및 화면 표시
---
## 1. 백엔드 구현
### 1.1 수정 파일
- **파일**: `ai/src/main/java/com/unicorn/hgzero/ai/biz/service/SuggestionService.java`
- **수정 내용**: `startMockDataEmission()` 메서드 추가
### 1.2 구현 내용
#### Mock 데이터 자동 발행 메서드
```java
/**
* TODO: AI 개발 완료 후 제거
* Mock 데이터 자동 발행 (프론트엔드 개발용)
* 5초마다 샘플 제안사항을 발행합니다.
*/
private void startMockDataEmission(String meetingId, Sinks.Many<RealtimeSuggestionsDto> sink) {
// 프론트엔드 HTML에 맞춘 샘플 데이터 (3개)
List<SimpleSuggestionDto> mockSuggestions = List.of(
SimpleSuggestionDto.builder()
.id("suggestion-1")
.content("신제품의 타겟 고객층을 20-30대로 설정하고, 모바일 우선 전략을 취하기로 논의 중입니다.")
.timestamp("00:05:23")
.confidence(0.92)
.build(),
// ... 3개의 샘플 데이터
);
// 5초마다 하나씩 발행 (총 3개)
Flux.interval(Duration.ofSeconds(5))
.take(3)
.map(index -> {
SimpleSuggestionDto suggestion = mockSuggestions.get(index.intValue());
return RealtimeSuggestionsDto.builder()
.suggestions(List.of(suggestion))
.build();
})
.subscribe(
suggestions -> {
sink.tryEmitNext(suggestions);
log.info("Mock 제안사항 발행 완료");
}
);
}
```
#### SSE 스트리밍 메서드 수정
```java
@Override
public Flux<RealtimeSuggestionsDto> streamRealtimeSuggestions(String meetingId) {
// Sink 생성
Sinks.Many<RealtimeSuggestionsDto> sink = Sinks.many()
.multicast()
.onBackpressureBuffer();
meetingSinks.put(meetingId, sink);
// TODO: AI 개발 완료 후 제거 - Mock 데이터 자동 발행
startMockDataEmission(meetingId, sink);
return sink.asFlux()
.doOnCancel(() -> {
meetingSinks.remove(meetingId);
cleanupMeetingData(meetingId);
});
}
```
### 1.3 샘플 데이터 구조
```json
{
"suggestions": [
{
"id": "suggestion-1",
"content": "신제품의 타겟 고객층을 20-30대로 설정하고, 모바일 우선 전략을 취하기로 논의 중입니다.",
"timestamp": "00:05:23",
"confidence": 0.92
}
]
}
```
---
## 2. 프론트엔드 구현
### 2.1 수정 파일
- **파일**: `design/uiux/prototype/05-회의진행.html`
- **수정 내용**: SSE 연결 및 실시간 데이터 수신 코드 추가
### 2.2 구현 내용
#### SSE 연결 함수
```javascript
function connectAiSuggestionStream() {
const apiUrl = `http://localhost:8082/api/suggestions/meetings/${meetingId}/stream`;
eventSource = new EventSource(apiUrl);
eventSource.addEventListener('ai-suggestion', function(event) {
const data = JSON.parse(event.data);
const suggestions = data.suggestions;
if (suggestions && suggestions.length > 0) {
suggestions.forEach(suggestion => {
addAiSuggestionToUI(suggestion);
});
}
});
eventSource.onerror = function(error) {
console.error('SSE 연결 오류:', error);
eventSource.close();
};
}
```
#### UI 추가 함수
```javascript
function addAiSuggestionToUI(suggestion) {
const listContainer = document.getElementById('aiSuggestionList');
const cardId = `suggestion-${suggestion.id}`;
// 중복 방지
if (document.getElementById(cardId)) {
return;
}
// AI 제안 카드 HTML 생성
const cardHtml = `
<div class="ai-suggestion-card" id="${cardId}">
<div class="ai-suggestion-header">
<span class="ai-suggestion-time">${suggestion.timestamp}</span>
<button class="ai-suggestion-add-btn"
onclick="addToMemo('${escapeHtml(suggestion.content)}', document.getElementById('${cardId}'))"
title="메모에 추가">
</button>
</div>
<div class="ai-suggestion-text">
${escapeHtml(suggestion.content)}
</div>
</div>
`;
listContainer.insertAdjacentHTML('beforeend', cardHtml);
}
```
#### XSS 방지
```javascript
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
```
---
## 3. 테스트 방법
### 3.1 서비스 실행
#### 1단계: AI 서비스 실행
```bash
# IntelliJ 실행 프로파일 사용
python3 tools/run-intellij-service-profile.py ai
# 또는 직접 실행
cd ai
./gradlew bootRun
```
**실행 확인**:
- 포트: `8082`
- 로그 확인: `ai/logs/ai-service.log`
#### 2단계: 프론트엔드 HTML 열기
```bash
# 브라우저에서 직접 열기
open design/uiux/prototype/05-회의진행.html
# 또는 HTTP 서버 실행
cd design/uiux/prototype
python3 -m http.server 8000
# 브라우저: http://localhost:8000/05-회의진행.html
```
### 3.2 동작 확인
#### 브라우저 콘솔 확인
1. 개발자 도구 열기 (F12)
2. Console 탭 확인
**예상 로그**:
```
AI 제안사항 SSE 스트림 연결됨: http://localhost:8082/api/suggestions/meetings/550e8400-e29b-41d4-a716-446655440000/stream
AI 제안사항 수신: {"suggestions":[{"id":"suggestion-1", ...}]}
AI 제안사항 추가됨: 신제품의 타겟 고객층을 20-30대로 설정하고...
```
#### 화면 동작 확인
1. **페이지 로드**: 회의진행.html 열기
2. **AI 제안 탭 클릭**: "AI 제안" 탭으로 이동
3. **5초 대기**: 첫 번째 제안사항 표시
4. **10초 대기**: 두 번째 제안사항 표시
5. **15초 대기**: 세 번째 제안사항 표시
#### 백엔드 로그 확인
```bash
tail -f ai/logs/ai-service.log
```
**예상 로그**:
```
실시간 AI 제안사항 스트리밍 시작 - meetingId: 550e8400-e29b-41d4-a716-446655440000
Mock 데이터 자동 발행 시작 - meetingId: 550e8400-e29b-41d4-a716-446655440000
Mock 제안사항 발행 - meetingId: 550e8400-e29b-41d4-a716-446655440000, 제안: 신제품의 타겟 고객층...
```
### 3.3 API 직접 테스트 (curl)
```bash
# SSE 스트림 연결
curl -N http://localhost:8082/api/suggestions/meetings/550e8400-e29b-41d4-a716-446655440000/stream
```
**예상 응답**:
```
event: ai-suggestion
id: 123456789
data: {"suggestions":[{"id":"suggestion-1","content":"신제품의 타겟 고객층...","timestamp":"00:05:23","confidence":0.92}]}
event: ai-suggestion
id: 987654321
data: {"suggestions":[{"id":"suggestion-2","content":"개발 일정...","timestamp":"00:08:45","confidence":0.88}]}
```
---
## 4. CORS 설정 (필요 시)
프론트엔드를 다른 포트에서 실행할 경우 CORS 설정이 필요합니다.
### 4.1 application.yml 확인
```yaml
# ai/src/main/resources/application.yml
spring:
web:
cors:
allowed-origins:
- http://localhost:8000
- http://localhost:3000
allowed-methods:
- GET
- POST
- PUT
- DELETE
allowed-headers:
- "*"
allow-credentials: true
```
---
## 5. 주의사항
### 5.1 Mock 데이터 제거 시점
⚠️ **AI 개발 완료 후 반드시 제거해야 할 코드**:
#### 백엔드 (SuggestionService.java)
```java
// TODO: AI 개발 완료 후 제거 - 이 줄 삭제
startMockDataEmission(meetingId, sink);
// TODO: AI 개발 완료 후 제거 - 이 메서드 전체 삭제
private void startMockDataEmission(...) { ... }
```
#### 프론트엔드 (회의진행.html)
- SSE 연결 코드는 **그대로 유지**
- API URL만 실제 환경에 맞게 수정:
```javascript
// 개발 환경
const apiUrl = `http://localhost:8082/api/suggestions/meetings/${meetingId}/stream`;
// 운영 환경 (예시)
const apiUrl = `/api/suggestions/meetings/${meetingId}/stream`;
```
### 5.2 제한사항
1. **회의 ID 고정**
- 현재 테스트용 회의 ID가 하드코딩됨
- 실제 환경에서는 회의 생성 API 응답에서 받아야 함
2. **샘플 데이터 개수**
- 현재 3개로 제한
- 실제 AI는 회의 진행에 따라 동적으로 생성
3. **재연결 처리 없음**
- SSE 연결이 끊어지면 재연결하지 않음
- 실제 환경에서는 재연결 로직 필요
4. **인증/인가 없음**
- 현재 JWT 토큰 검증 없이 테스트
- 실제 환경에서는 인증 헤더 추가 필요
---
## 6. 트러블슈팅
### 문제 1: SSE 연결 안 됨
**증상**: 브라우저 콘솔에 "SSE 연결 오류" 표시
**해결 방법**:
1. AI 서비스가 실행 중인지 확인
```bash
curl http://localhost:8082/actuator/health
```
2. CORS 설정 확인
3. 방화벽/포트 확인
### 문제 2: 제안사항이 표시되지 않음
**증상**: SSE는 연결되지만 화면에 아무것도 표시되지 않음
**해결 방법**:
1. 브라우저 콘솔에서 에러 확인
2. Network 탭에서 SSE 이벤트 확인
3. 백엔드 로그 확인
### 문제 3: 중복 제안사항 표시
**증상**: 같은 제안이 여러 번 표시됨
**해결 방법**:
- 페이지 새로고침 (SSE 연결 재시작)
- 브라우저 캐시 삭제
---
## 7. 다음 단계
### AI 개발 완료 후 작업
1. **Mock 코드 제거**
- `startMockDataEmission()` 메서드 삭제
- 관련 TODO 주석 제거
2. **실제 AI 로직 연결**
- Claude API 연동
- Event Hub 메시지 수신
- Redis 텍스트 축적 및 분석
3. **프론트엔드 개선**
- 재연결 로직 추가
- 에러 핸들링 강화
- 로딩 상태 표시
4. **성능 최적화**
- SSE 연결 풀 관리
- 메모리 누수 방지
- 네트워크 재시도 전략
---
## 8. 관련 문서
- [AI Service API 설계서](../../design/backend/api/spec/ai-service-api-spec.md)
- [SSE 스트리밍 가이드](dev-ai-realtime-streaming.md)
- [백엔드 개발 가이드](dev-backend.md)
---
## 문서 이력
| 버전 | 작성일 | 작성자 | 변경 내용 |
|------|--------|--------|----------|
| 1.0 | 2025-10-27 | 준호 (Backend Developer) | 초안 작성 |