Merge feature/stt-ai into main

주요 변경사항:
- EventHub 공유 액세스 정책 재설정 (send-policy, listen-policy)
- Redis DB 2번 읽기 전용 문제 해결
- AI-Python 서비스 추가 (FastAPI 기반)
- STT WebSocket 실시간 스트리밍 구현
- AI 제안사항 실시간 추출 기능 구현
- 테스트 페이지 추가 (stt-test-wav.html)
- 개발 가이드 문서 추가

🤖 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-29 16:01:47 +09:00
81 changed files with 10269 additions and 276 deletions
+18 -1
View File
@@ -857,6 +857,10 @@ components:
type: string
description: 공통 키워드
example: ["MSA", "API Gateway", "Spring Boot"]
summary:
type: string
description: 회의록 핵심 내용 요약 (1-2문장)
example: "MSA 아키텍처 설계 및 API Gateway 도입을 결정. 서비스별 독립 배포 전략 수립."
link:
type: string
description: 회의록 링크
@@ -880,9 +884,22 @@ components:
example: 0.92
category:
type: string
enum: [기술, 업무, 도메인]
enum: [기술, 업무, 도메인, 기획, 비즈니스, 전략, 마케팅]
description: 용어 카테고리
example: "기술"
definition:
type: string
description: 용어 정의 (간단한 설명)
example: "Microservices Architecture의 약자. 애플리케이션을 작은 독립적인 서비스로 나누는 아키텍처 패턴"
context:
type: string
description: 용어가 사용된 맥락 (과거 회의록 참조)
example: "신제품 기획 회의(2024-09-15)에서 언급"
relatedMeetingId:
type: string
format: uuid
description: 관련 회의 ID (용어가 논의된 과거 회의)
example: "bb0e8400-e29b-41d4-a716-446655440006"
highlight:
type: boolean
description: 하이라이트 여부
+188
View File
@@ -0,0 +1,188 @@
/**
* AI 제안사항 SSE 연동 예시
* 05-회의진행.html에 추가할 JavaScript 코드
*/
// ============================================
// 1. 전역 변수 선언
// ============================================
let eventSource = null;
const meetingId = "test-meeting-001"; // 실제로는 URL 파라미터에서 가져옴
// ============================================
// 2. SSE 연결 초기화
// ============================================
function initializeAiSuggestions() {
console.log('AI 제안사항 SSE 연결 시작 - meetingId:', meetingId);
// SSE 연결
eventSource = new EventSource(
`http://localhost:8083/api/suggestions/meetings/${meetingId}/stream`
);
// 연결 성공
eventSource.onopen = function() {
console.log('SSE 연결 성공');
};
// AI 제안사항 수신
eventSource.addEventListener('ai-suggestion', function(event) {
console.log('AI 제안사항 수신:', event.data);
try {
const data = JSON.parse(event.data);
handleAiSuggestions(data);
} catch (error) {
console.error('JSON 파싱 오류:', error);
}
});
// 연결 오류
eventSource.onerror = function(error) {
console.error('SSE 연결 오류:', error);
// 자동 재연결은 브라우저가 처리
// 필요시 수동 재연결 로직 추가 가능
};
}
// ============================================
// 3. AI 제안사항 처리
// ============================================
function handleAiSuggestions(data) {
console.log('AI 제안사항 처리:', data);
// data 형식:
// {
// "suggestions": [
// {
// "id": "sugg-001",
// "content": "신제품의 타겟 고객층을...",
// "timestamp": "00:05:23",
// "confidence": 0.92
// }
// ]
// }
if (data.suggestions && data.suggestions.length > 0) {
data.suggestions.forEach(suggestion => {
addSuggestionCard(suggestion);
});
}
}
// ============================================
// 4. 제안사항 카드 추가
// ============================================
function addSuggestionCard(suggestion) {
const card = document.createElement('div');
card.className = 'ai-suggestion-card';
card.id = 'suggestion-' + suggestion.id;
// 타임스탬프 (있으면 사용, 없으면 현재 시간)
const timestamp = suggestion.timestamp || getCurrentRecordingTime();
card.innerHTML = `
<div class="ai-suggestion-header">
<span class="ai-suggestion-time">${timestamp}</span>
<button class="ai-suggestion-add-btn"
onclick="addToMemo('${escapeHtml(suggestion.content)}', document.getElementById('suggestion-${suggestion.id}'))"
title="메모에 추가">
</button>
</div>
<div class="ai-suggestion-text">
${escapeHtml(suggestion.content)}
</div>
${suggestion.confidence ? `
<div class="ai-suggestion-confidence">
<span style="font-size: 11px; color: var(--gray-500);">
신뢰도: ${Math.round(suggestion.confidence * 100)}%
</span>
</div>
` : ''}
`;
// aiSuggestionList의 맨 위에 추가 (최신 항목이 위로)
const listElement = document.getElementById('aiSuggestionList');
if (listElement) {
listElement.insertBefore(card, listElement.firstChild);
// 부드러운 등장 애니메이션
setTimeout(() => {
card.style.opacity = '0';
card.style.transform = 'translateY(-10px)';
card.style.transition = 'all 0.3s ease';
setTimeout(() => {
card.style.opacity = '1';
card.style.transform = 'translateY(0)';
}, 10);
}, 0);
} else {
console.error('aiSuggestionList 엘리먼트를 찾을 수 없습니다.');
}
}
// ============================================
// 5. 유틸리티 함수
// ============================================
/**
* 현재 녹음 시간 가져오기 (HH:MM 형식)
*/
function getCurrentRecordingTime() {
const timerElement = document.getElementById('recordingTime');
if (timerElement) {
const time = timerElement.textContent;
return time.substring(0, 5); // "00:05:23" -> "00:05"
}
return "00:00";
}
/**
* HTML 이스케이프 (XSS 방지)
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* SSE 연결 종료
*/
function closeAiSuggestions() {
if (eventSource) {
console.log('SSE 연결 종료');
eventSource.close();
eventSource = null;
}
}
// ============================================
// 6. 페이지 로드 시 자동 시작
// ============================================
document.addEventListener('DOMContentLoaded', function() {
console.log('페이지 로드 완료 - AI 제안사항 초기화');
// SSE 연결 시작
initializeAiSuggestions();
// 페이지 닫을 때 SSE 연결 종료
window.addEventListener('beforeunload', function() {
closeAiSuggestions();
});
});
// ============================================
// 7. 회의 종료 시 SSE 연결 종료
// ============================================
// 기존 endMeeting 함수 수정
const originalEndMeeting = window.endMeeting;
window.endMeeting = function() {
closeAiSuggestions(); // SSE 연결 종료
if (originalEndMeeting) {
originalEndMeeting(); // 기존 로직 실행
}
};