mirror of
https://github.com/hwanny1128/HGZero.git
synced 2026-06-12 22:59:10 +00:00
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:
@@ -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. ✅ **통합 테스트**: 전체 플로우 동작 확인
|
||||
@@ -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에 스크립트만 추가하면 바로 사용 가능합니다.
|
||||
@@ -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 키 발급 및 통합 테스트
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
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) | 초안 작성 |
|
||||
Reference in New Issue
Block a user