mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 20:46:23 +00:00
[포트 통일] - ai-python 서비스 포트를 8087로 완전 통일 - 모든 문서에서 8086 참조 제거 - README.md, 개발 가이드 문서 전부 8087로 업데이트 변경 파일: - ai-python/README.md - develop/dev/ai-frontend-integration-guide.md - develop/dev/dev-*.md (5개 파일) [meeting-ai 테스트] 테스트 완료 항목: ✓ 회의록 통합 및 취합 ✓ AI 한줄 요약/상세 요약 생성 ✓ 회의 전체 결정사항 추출 ✓ TODO 자동 추출 (9개) ✓ 통계 정보 생성 ✓ 주요 키워드 추출 (10개) 테스트 파일: - develop/test/meeting-ai-test-data.json (테스트 데이터) - develop/test/consolidate-response.json (API 응답) - develop/test/meeting-ai-test-result.md (상세 결과서)
483 lines
12 KiB
Markdown
483 lines
12 KiB
Markdown
# AI 서비스 프론트엔드 통합 가이드
|
||
|
||
## 개요
|
||
|
||
AI 서비스의 실시간 제안사항 API를 프론트엔드에서 사용하기 위한 통합 가이드입니다.
|
||
|
||
**⚠️ 중요**: AI 서비스가 **Python (FastAPI)**로 마이그레이션 되었습니다.
|
||
- **기존 포트**: 8083 (Java Spring Boot) → **새 포트**: 8086 (Python FastAPI)
|
||
- **엔드포인트 경로**: `/api/suggestions/...` → `/api/v1/ai/suggestions/...`
|
||
|
||
---
|
||
|
||
## 1. API 정보
|
||
|
||
### 엔드포인트
|
||
```
|
||
GET /api/v1/ai/suggestions/meetings/{meetingId}/stream
|
||
```
|
||
|
||
**변경 사항**:
|
||
- ✅ **새 경로** (Python): `/api/v1/ai/suggestions/meetings/{meetingId}/stream`
|
||
- ❌ **구 경로** (Java): `/api/suggestions/meetings/{meetingId}/stream`
|
||
|
||
### 메서드
|
||
- **HTTP Method**: GET
|
||
- **Content-Type**: text/event-stream (SSE)
|
||
- **인증**: 개발 환경에서는 불필요 (운영 환경에서는 JWT 필요)
|
||
|
||
### 파라미터
|
||
| 파라미터 | 타입 | 필수 | 설명 |
|
||
|---------|------|------|------|
|
||
| meetingId | string (UUID) | 필수 | 회의 고유 ID |
|
||
|
||
### 예시
|
||
```
|
||
# Python (새 버전)
|
||
http://localhost:8087/api/v1/ai/suggestions/meetings/550e8400-e29b-41d4-a716-446655440000/stream
|
||
|
||
# Java (구 버전 - 사용 중단 예정)
|
||
http://localhost:8083/api/suggestions/meetings/550e8400-e29b-41d4-a716-446655440000/stream
|
||
```
|
||
|
||
---
|
||
|
||
## 2. 응답 데이터 구조
|
||
|
||
### SSE 이벤트 형식
|
||
```
|
||
event: ai-suggestion
|
||
id: 123456789
|
||
data: {"suggestions":[...]}
|
||
```
|
||
|
||
### 데이터 스키마 (JSON)
|
||
```typescript
|
||
interface RealtimeSuggestionsDto {
|
||
suggestions: SimpleSuggestionDto[];
|
||
}
|
||
|
||
interface SimpleSuggestionDto {
|
||
id: string; // 제안 고유 ID (예: "suggestion-1")
|
||
content: string; // 제안 내용 (예: "신제품의 타겟 고객층...")
|
||
timestamp: string; // 시간 정보 (HH:MM:SS 형식, 예: "00:05:23")
|
||
confidence: number; // 신뢰도 점수 (0.0 ~ 1.0)
|
||
}
|
||
```
|
||
|
||
### 샘플 응답
|
||
```json
|
||
{
|
||
"suggestions": [
|
||
{
|
||
"id": "suggestion-1",
|
||
"content": "신제품의 타겟 고객층을 20-30대로 설정하고, 모바일 우선 전략을 취하기로 논의 중입니다.",
|
||
"timestamp": "00:05:23",
|
||
"confidence": 0.92
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 프론트엔드 구현 방법
|
||
|
||
### 3.1 EventSource로 연결
|
||
|
||
```javascript
|
||
// 회의 ID (실제로는 회의 생성 API에서 받아야 함)
|
||
const meetingId = '550e8400-e29b-41d4-a716-446655440000';
|
||
|
||
// SSE 연결 (Python 버전)
|
||
const apiUrl = `http://localhost:8087/api/v1/ai/suggestions/meetings/${meetingId}/stream`;
|
||
const eventSource = new EventSource(apiUrl);
|
||
|
||
// 연결 성공
|
||
eventSource.onopen = function(event) {
|
||
console.log('SSE 연결 성공');
|
||
};
|
||
|
||
// ai-suggestion 이벤트 수신
|
||
eventSource.addEventListener('ai-suggestion', function(event) {
|
||
const data = JSON.parse(event.data);
|
||
const suggestions = data.suggestions;
|
||
|
||
suggestions.forEach(suggestion => {
|
||
console.log('제안:', suggestion.content);
|
||
addSuggestionToUI(suggestion);
|
||
});
|
||
});
|
||
|
||
// 에러 처리
|
||
eventSource.onerror = function(error) {
|
||
console.error('SSE 연결 오류:', error);
|
||
eventSource.close();
|
||
};
|
||
```
|
||
|
||
### 3.2 UI에 제안사항 추가
|
||
|
||
```javascript
|
||
function addSuggestionToUI(suggestion) {
|
||
const container = document.getElementById('aiSuggestionList');
|
||
|
||
// 중복 방지
|
||
if (document.getElementById(`suggestion-${suggestion.id}`)) {
|
||
return;
|
||
}
|
||
|
||
// HTML 생성
|
||
const html = `
|
||
<div class="ai-suggestion-card" id="suggestion-${suggestion.id}">
|
||
<div class="ai-suggestion-header">
|
||
<span class="ai-suggestion-time">${escapeHtml(suggestion.timestamp)}</span>
|
||
<button onclick="handleAddToMemo('${escapeHtml(suggestion.content)}')">
|
||
➕
|
||
</button>
|
||
</div>
|
||
<div class="ai-suggestion-text">
|
||
${escapeHtml(suggestion.content)}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
container.insertAdjacentHTML('beforeend', html);
|
||
}
|
||
```
|
||
|
||
### 3.3 XSS 방지
|
||
|
||
```javascript
|
||
function escapeHtml(text) {
|
||
const map = {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
};
|
||
return text.replace(/[&<>"']/g, m => map[m]);
|
||
}
|
||
```
|
||
|
||
### 3.4 연결 종료
|
||
|
||
```javascript
|
||
// 페이지 종료 시 또는 회의 종료 시
|
||
window.addEventListener('beforeunload', function() {
|
||
if (eventSource) {
|
||
eventSource.close();
|
||
}
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 4. React 통합 예시
|
||
|
||
### 4.1 Custom Hook
|
||
|
||
```typescript
|
||
import { useEffect, useState } from 'react';
|
||
|
||
interface Suggestion {
|
||
id: string;
|
||
content: string;
|
||
timestamp: string;
|
||
confidence: number;
|
||
}
|
||
|
||
export function useAiSuggestions(meetingId: string) {
|
||
const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
|
||
const [isConnected, setIsConnected] = useState(false);
|
||
const [error, setError] = useState<Error | null>(null);
|
||
|
||
useEffect(() => {
|
||
const apiUrl = `http://localhost:8087/api/v1/ai/suggestions/meetings/${meetingId}/stream`;
|
||
const eventSource = new EventSource(apiUrl);
|
||
|
||
eventSource.onopen = () => {
|
||
setIsConnected(true);
|
||
setError(null);
|
||
};
|
||
|
||
eventSource.addEventListener('ai-suggestion', (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
setSuggestions(prev => [...prev, ...data.suggestions]);
|
||
} catch (err) {
|
||
setError(err as Error);
|
||
}
|
||
});
|
||
|
||
eventSource.onerror = (err) => {
|
||
setError(new Error('SSE connection failed'));
|
||
setIsConnected(false);
|
||
eventSource.close();
|
||
};
|
||
|
||
return () => {
|
||
eventSource.close();
|
||
setIsConnected(false);
|
||
};
|
||
}, [meetingId]);
|
||
|
||
return { suggestions, isConnected, error };
|
||
}
|
||
```
|
||
|
||
### 4.2 Component 사용
|
||
|
||
```typescript
|
||
function MeetingPage({ meetingId }: { meetingId: string }) {
|
||
const { suggestions, isConnected, error } = useAiSuggestions(meetingId);
|
||
|
||
if (error) {
|
||
return <div>Error: {error.message}</div>;
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<div>연결 상태: {isConnected ? '연결됨' : '연결 안 됨'}</div>
|
||
|
||
<div className="suggestions-list">
|
||
{suggestions.map(suggestion => (
|
||
<div key={suggestion.id} className="suggestion-card">
|
||
<span className="timestamp">{suggestion.timestamp}</span>
|
||
<p>{suggestion.content}</p>
|
||
<button onClick={() => addToMemo(suggestion.content)}>
|
||
메모에 추가
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 환경별 설정
|
||
|
||
### 5.1 개발 환경
|
||
```javascript
|
||
// Python 버전 (권장)
|
||
const API_BASE_URL = 'http://localhost:8087';
|
||
|
||
// Java 버전 (구버전 - 사용 중단 예정)
|
||
// const API_BASE_URL = 'http://localhost:8083';
|
||
```
|
||
|
||
### 5.2 테스트 환경
|
||
```javascript
|
||
const API_BASE_URL = 'https://test-api.hgzero.com';
|
||
```
|
||
|
||
### 5.3 운영 환경
|
||
```javascript
|
||
// 같은 도메인에서 실행될 경우
|
||
const API_BASE_URL = '';
|
||
|
||
// 또는 환경변수 사용
|
||
const API_BASE_URL = process.env.REACT_APP_AI_API_URL;
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 에러 처리
|
||
|
||
### 6.1 연결 실패
|
||
|
||
```javascript
|
||
eventSource.onerror = function(error) {
|
||
console.error('SSE 연결 실패:', error);
|
||
|
||
// 사용자에게 알림
|
||
showErrorNotification('AI 제안사항을 받을 수 없습니다. 다시 시도해주세요.');
|
||
|
||
// 재연결 시도 (옵션)
|
||
setTimeout(() => {
|
||
reconnect();
|
||
}, 5000);
|
||
};
|
||
```
|
||
|
||
### 6.2 파싱 오류
|
||
|
||
```javascript
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
} catch (error) {
|
||
console.error('데이터 파싱 오류:', error);
|
||
console.error('원본 데이터:', event.data);
|
||
|
||
// Sentry 등 에러 모니터링 서비스에 전송
|
||
reportError(error, { eventData: event.data });
|
||
}
|
||
```
|
||
|
||
### 6.3 네트워크 오류
|
||
|
||
```javascript
|
||
// Timeout 설정 (EventSource는 기본 타임아웃 없음)
|
||
const connectionTimeout = setTimeout(() => {
|
||
if (!isConnected) {
|
||
console.error('연결 타임아웃');
|
||
eventSource.close();
|
||
handleConnectionTimeout();
|
||
}
|
||
}, 10000); // 10초
|
||
|
||
eventSource.onopen = function() {
|
||
clearTimeout(connectionTimeout);
|
||
setIsConnected(true);
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 운영 환경 배포 시 변경 사항
|
||
|
||
### 7.1 인증 헤더 추가 (운영 환경)
|
||
|
||
⚠️ **중요**: 개발 환경에서는 인증이 해제되어 있지만, **운영 환경에서는 JWT 토큰이 필요**합니다.
|
||
|
||
```javascript
|
||
// EventSource는 헤더를 직접 설정할 수 없으므로 URL에 토큰 포함
|
||
const token = getAccessToken();
|
||
const apiUrl = `${API_BASE_URL}/api/suggestions/meetings/${meetingId}/stream?token=${token}`;
|
||
|
||
// 또는 fetch API + ReadableStream 사용 (권장)
|
||
const response = await fetch(apiUrl, {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
}
|
||
});
|
||
|
||
const reader = response.body.getReader();
|
||
// SSE 파싱 로직 구현
|
||
```
|
||
|
||
### 7.2 CORS 설정 확인
|
||
|
||
운영 환경 도메인이 백엔드 CORS 설정에 포함되어 있는지 확인:
|
||
|
||
```yaml
|
||
# application.yml
|
||
cors:
|
||
allowed-origins: https://your-production-domain.com
|
||
```
|
||
|
||
---
|
||
|
||
## 8. AI 개발 완료 후 변경 사항
|
||
|
||
### 8.1 제거할 백엔드 코드
|
||
- [SuggestionService.java:102](ai/src/main/java/com/unicorn/hgzero/ai/biz/service/SuggestionService.java:102) - Mock 데이터 발행 호출
|
||
- [SuggestionService.java:192-236](ai/src/main/java/com/unicorn/hgzero/ai/biz/service/SuggestionService.java:192-236) - Mock 메서드 전체
|
||
- [SecurityConfig.java:49](ai/src/main/java/com/unicorn/hgzero/ai/infra/config/SecurityConfig.java:49) - 인증 해제 설정
|
||
|
||
### 8.2 프론트엔드는 변경 불필요
|
||
- SSE 연결 코드는 그대로 유지
|
||
- API URL만 운영 환경에 맞게 수정
|
||
- JWT 토큰 추가 (위 7.1 참고)
|
||
|
||
### 8.3 실제 AI 동작 방식 (예상)
|
||
```
|
||
STT 텍스트 생성 → Event Hub 전송 → AI 서비스 수신 →
|
||
텍스트 축적 (Redis) → 임계값 도달 → Claude API 분석 →
|
||
SSE로 제안사항 발행 → 프론트엔드 수신
|
||
```
|
||
|
||
현재 Mock은 **5초, 10초, 15초**에 발행하지만, 실제 AI는 **회의 진행 상황에 따라 동적으로** 발행됩니다.
|
||
|
||
---
|
||
|
||
## 9. 알려진 제한사항
|
||
|
||
### 9.1 브라우저 호환성
|
||
- **EventSource는 IE 미지원** (Edge, Chrome, Firefox, Safari는 지원)
|
||
- 필요 시 Polyfill 사용: `event-source-polyfill`
|
||
|
||
### 9.2 연결 제한
|
||
- 동일 도메인에 대한 SSE 연결은 브라우저당 **6개로 제한**
|
||
- 여러 탭에서 동시 접속 시 주의
|
||
|
||
### 9.3 재연결
|
||
- EventSource는 자동 재연결을 시도하지만, 서버에서 연결을 끊으면 재연결 안 됨
|
||
- 수동 재연결 로직 구현 권장
|
||
|
||
### 9.4 Mock 데이터 특성
|
||
- **개발 환경 전용**: 3개 제안 후 자동 종료
|
||
- **실제 AI**: 회의 진행 중 계속 발행, 회의 종료 시까지 연결 유지
|
||
|
||
---
|
||
|
||
## 10. 테스트 방법
|
||
|
||
### 10.1 로컬 테스트
|
||
```bash
|
||
# 1. AI 서비스 실행
|
||
python3 tools/run-intellij-service-profile.py ai
|
||
|
||
# 2. HTTP 서버 실행 (file:// 프로토콜은 CORS 제한)
|
||
cd design/uiux/prototype
|
||
python3 -m http.server 8000
|
||
|
||
# 3. 브라우저에서 접속
|
||
open http://localhost:8000/05-회의진행.html
|
||
```
|
||
|
||
### 10.2 디버깅
|
||
```javascript
|
||
// 브라우저 개발자 도구 Console 탭에서 확인
|
||
// [DEBUG] 로그로 상세 정보 출력
|
||
// [ERROR] 로그로 에러 추적
|
||
```
|
||
|
||
### 10.3 curl 테스트
|
||
```bash
|
||
# Python 버전 (새 포트)
|
||
curl -N http://localhost:8087/api/v1/ai/suggestions/meetings/test-meeting/stream
|
||
|
||
# Java 버전 (구 포트 - 사용 중단 예정)
|
||
# curl -N http://localhost:8083/api/suggestions/meetings/550e8400-e29b-41d4-a716-446655440000/stream
|
||
```
|
||
|
||
---
|
||
|
||
## 11. 참고 문서
|
||
|
||
- [AI Service API 설계서](../../design/backend/api/spec/ai-service-api-spec.md)
|
||
- [AI 샘플 데이터 통합 가이드](dev-ai-sample-data-guide.md)
|
||
- [SSE 스트리밍 가이드](dev-ai-realtime-streaming.md)
|
||
|
||
---
|
||
|
||
## 12. FAQ
|
||
|
||
### Q1. 왜 EventSource를 사용하나요?
|
||
**A**: WebSocket보다 단방향 통신에 적합하고, 자동 재연결 기능이 있으며, 구현이 간단합니다.
|
||
|
||
### Q2. 제안사항이 중복으로 표시되는 경우?
|
||
**A**: `addSuggestionToUI` 함수에 중복 체크 로직이 있는지 확인하세요.
|
||
|
||
### Q3. 연결은 되는데 데이터가 안 오는 경우?
|
||
**A**:
|
||
1. 백엔드 로그 확인 (`ai/logs/ai-service.log`)
|
||
2. Network 탭에서 `stream` 요청 확인
|
||
3. `ai-suggestion` 이벤트 리스너가 등록되었는지 확인
|
||
|
||
### Q4. 운영 환경에서 401 Unauthorized 에러?
|
||
**A**: JWT 토큰이 필요합니다. 7.1절 "인증 헤더 추가" 참고.
|
||
|
||
---
|
||
|
||
## 문서 이력
|
||
|
||
| 버전 | 작성일 | 작성자 | 변경 내용 |
|
||
|------|--------|--------|----------|
|
||
| 1.0 | 2025-10-27 | 준호 (Backend), 유진 (Frontend) | 초안 작성 |
|