mirror of
https://github.com/hwanny1128/HGZero.git
synced 2026-06-13 03:39:10 +00:00
AI 서비스 SSE 스트리밍 기능 및 테스트 환경 구성 완료
- SSE 스트리밍 방식으로 AI 분석 결과 실시간 전송 구현 - 용어 감지 및 관련 회의록 검색 기능 개선 - API 명세 업데이트 (SSE 엔드포인트 추가) - AI 및 STT 서비스 테스트 환경 구성 문서 작성 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,9 @@ dependencies {
|
||||
implementation "io.github.openfeign:feign-jackson:${feignJacksonVersion}"
|
||||
implementation "io.github.openfeign:feign-okhttp:${feignJacksonVersion}"
|
||||
|
||||
// Spring WebFlux for SSE streaming
|
||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
|
||||
// H2 Database for local development
|
||||
runtimeOnly 'com.h2database:h2'
|
||||
}
|
||||
|
||||
@@ -48,6 +48,11 @@ public class RelatedMinutes {
|
||||
*/
|
||||
private List<String> commonKeywords;
|
||||
|
||||
/**
|
||||
* 회의록 핵심 내용 요약 (1-2문장)
|
||||
*/
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 회의록 링크
|
||||
*/
|
||||
|
||||
@@ -31,10 +31,26 @@ public class Term {
|
||||
private Double confidence;
|
||||
|
||||
/**
|
||||
* 용어 카테고리 (기술, 업무, 도메인)
|
||||
* 용어 카테고리 (기술, 업무, 도메인, 기획, 비즈니스, 전략, 마케팅)
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 용어 정의 (간단한 설명)
|
||||
*/
|
||||
private String definition;
|
||||
|
||||
/**
|
||||
* 용어가 사용된 맥락 (과거 회의록 참조)
|
||||
* 예: "신제품 기획 회의(2024-09-15)에서 언급"
|
||||
*/
|
||||
private String context;
|
||||
|
||||
/**
|
||||
* 관련 회의 ID (용어가 논의된 과거 회의)
|
||||
*/
|
||||
private String relatedMeetingId;
|
||||
|
||||
/**
|
||||
* 하이라이트 여부
|
||||
*/
|
||||
|
||||
@@ -3,10 +3,15 @@ package com.unicorn.hgzero.ai.biz.service;
|
||||
import com.unicorn.hgzero.ai.biz.domain.Suggestion;
|
||||
import com.unicorn.hgzero.ai.biz.gateway.LlmGateway;
|
||||
import com.unicorn.hgzero.ai.biz.usecase.SuggestionUseCase;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.DecisionSuggestionDto;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.DiscussionSuggestionDto;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.RealtimeSuggestionsDto;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -66,4 +71,92 @@ public class SuggestionService implements SuggestionUseCase {
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<RealtimeSuggestionsDto> streamRealtimeSuggestions(String meetingId) {
|
||||
log.info("실시간 AI 제안사항 스트리밍 시작 - meetingId: {}", meetingId);
|
||||
|
||||
// 실시간으로 AI 제안사항을 생성하는 스트림 (10초 간격)
|
||||
return Flux.interval(Duration.ofSeconds(10))
|
||||
.map(sequence -> generateRealtimeSuggestions(meetingId, sequence))
|
||||
.doOnNext(suggestions ->
|
||||
log.debug("AI 제안사항 생성 - meetingId: {}, 논의사항: {}, 결정사항: {}",
|
||||
meetingId,
|
||||
suggestions.getDiscussionTopics() != null ? suggestions.getDiscussionTopics().size() : 0,
|
||||
suggestions.getDecisions() != null ? suggestions.getDecisions().size() : 0))
|
||||
.doOnError(error ->
|
||||
log.error("AI 제안사항 스트리밍 오류 - meetingId: {}", meetingId, error))
|
||||
.doOnComplete(() ->
|
||||
log.info("AI 제안사항 스트리밍 종료 - meetingId: {}", meetingId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 실시간 AI 제안사항 생성 (Mock)
|
||||
* 실제로는 STT 텍스트를 분석하여 AI가 제안사항을 생성
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @param sequence 시퀀스 번호
|
||||
* @return RealtimeSuggestionsDto AI 제안사항
|
||||
*/
|
||||
private RealtimeSuggestionsDto generateRealtimeSuggestions(String meetingId, Long sequence) {
|
||||
// Mock 데이터 - 실제로는 LLM을 통해 STT 텍스트 분석 후 생성
|
||||
List<DiscussionSuggestionDto> discussionTopics = List.of(
|
||||
DiscussionSuggestionDto.builder()
|
||||
.id("disc-" + sequence)
|
||||
.topic(getMockDiscussionTopic(sequence))
|
||||
.reason("회의 안건에 포함되어 있으나 아직 논의되지 않음")
|
||||
.priority(sequence % 2 == 0 ? "HIGH" : "MEDIUM")
|
||||
.relatedAgenda("프로젝트 계획")
|
||||
.estimatedTime(15)
|
||||
.build()
|
||||
);
|
||||
|
||||
List<DecisionSuggestionDto> decisions = List.of(
|
||||
DecisionSuggestionDto.builder()
|
||||
.id("dec-" + sequence)
|
||||
.content(getMockDecisionContent(sequence))
|
||||
.category("기술")
|
||||
.decisionMaker("팀장")
|
||||
.participants(List.of("김철수", "이영희", "박민수"))
|
||||
.confidence(0.85 + (sequence % 15) * 0.01)
|
||||
.extractedFrom("회의 중 결정된 사항")
|
||||
.context("팀원들의 의견을 종합한 결과")
|
||||
.build()
|
||||
);
|
||||
|
||||
return RealtimeSuggestionsDto.builder()
|
||||
.discussionTopics(discussionTopics)
|
||||
.decisions(decisions)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 논의사항 주제 생성
|
||||
*/
|
||||
private String getMockDiscussionTopic(Long sequence) {
|
||||
String[] topics = {
|
||||
"보안 요구사항 검토",
|
||||
"데이터베이스 스키마 설계",
|
||||
"API 인터페이스 정의",
|
||||
"테스트 전략 수립",
|
||||
"배포 일정 조율",
|
||||
"성능 최적화 방안"
|
||||
};
|
||||
return topics[(int) (sequence % topics.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 결정사항 내용 생성
|
||||
*/
|
||||
private String getMockDecisionContent(Long sequence) {
|
||||
String[] decisions = {
|
||||
"React로 프론트엔드 개발하기로 결정",
|
||||
"PostgreSQL을 메인 데이터베이스로 사용",
|
||||
"JWT 토큰 기반 인증 방식 채택",
|
||||
"Docker를 활용한 컨테이너화 진행",
|
||||
"주 1회 스프린트 회고 진행",
|
||||
"코드 리뷰 필수화"
|
||||
};
|
||||
return decisions[(int) (sequence % decisions.length)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.unicorn.hgzero.ai.biz.usecase;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.domain.Suggestion;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.RealtimeSuggestionsDto;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -27,4 +29,13 @@ public interface SuggestionUseCase {
|
||||
* @return 결정사항 제안 목록
|
||||
*/
|
||||
List<Suggestion> suggestDecisions(String meetingId, String transcriptText);
|
||||
|
||||
/**
|
||||
* 실시간 AI 제안사항 스트리밍
|
||||
* 회의 진행 중 실시간으로 논의사항과 결정사항을 분석하여 제안
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return 실시간 제안사항 스트림
|
||||
*/
|
||||
Flux<RealtimeSuggestionsDto> streamRealtimeSuggestions(String meetingId);
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ public class RelationController {
|
||||
.participants(r.getParticipants())
|
||||
.relevanceScore(r.getRelevanceScore())
|
||||
.commonKeywords(r.getCommonKeywords())
|
||||
.summary(r.getSummary())
|
||||
.link(r.getLink())
|
||||
.build())
|
||||
.collect(Collectors.toList()))
|
||||
|
||||
@@ -10,12 +10,16 @@ import com.unicorn.hgzero.ai.infra.dto.common.DiscussionSuggestionDto;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.DecisionSuggestionDto;
|
||||
import com.unicorn.hgzero.common.dto.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
@@ -96,4 +100,33 @@ public class SuggestionController {
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
/**
|
||||
* 실시간 AI 제안사항 스트리밍 (SSE)
|
||||
* 회의 진행 중 실시간으로 AI가 분석한 제안사항을 Server-Sent Events로 스트리밍
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return Flux<ServerSentEvent<RealtimeSuggestionsDto>> AI 제안사항 스트림
|
||||
*/
|
||||
@GetMapping(value = "/meetings/{meetingId}/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
@Operation(
|
||||
summary = "실시간 AI 제안사항 스트리밍",
|
||||
description = "회의 진행 중 실시간으로 AI가 분석한 제안사항을 Server-Sent Events(SSE)로 스트리밍합니다. " +
|
||||
"클라이언트는 EventSource API를 사용하여 연결할 수 있습니다."
|
||||
)
|
||||
public Flux<ServerSentEvent<com.unicorn.hgzero.ai.infra.dto.common.RealtimeSuggestionsDto>> streamRealtimeSuggestions(
|
||||
@Parameter(description = "회의 ID", required = true, example = "550e8400-e29b-41d4-a716-446655440000")
|
||||
@PathVariable String meetingId) {
|
||||
|
||||
log.info("실시간 AI 제안사항 스트리밍 요청 - meetingId: {}", meetingId);
|
||||
|
||||
return suggestionUseCase.streamRealtimeSuggestions(meetingId)
|
||||
.map(suggestions -> ServerSentEvent.<com.unicorn.hgzero.ai.infra.dto.common.RealtimeSuggestionsDto>builder()
|
||||
.id(suggestions.hashCode() + "")
|
||||
.event("ai-suggestion")
|
||||
.data(suggestions)
|
||||
.build())
|
||||
.doOnComplete(() -> log.info("AI 제안사항 스트리밍 완료 - meetingId: {}", meetingId))
|
||||
.doOnError(error -> log.error("AI 제안사항 스트리밍 오류 - meetingId: {}", meetingId, error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,9 @@ public class TermController {
|
||||
.build() : null)
|
||||
.confidence(t.getConfidence())
|
||||
.category(t.getCategory())
|
||||
.definition(t.getDefinition())
|
||||
.context(t.getContext())
|
||||
.relatedMeetingId(t.getRelatedMeetingId())
|
||||
.highlight(t.getHighlight())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@@ -31,10 +31,26 @@ public class DetectedTermDto {
|
||||
private Double confidence;
|
||||
|
||||
/**
|
||||
* 용어 카테고리 (기술, 업무, 도메인)
|
||||
* 용어 카테고리 (기술, 업무, 도메인, 기획, 비즈니스, 전략, 마케팅)
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 용어 정의 (간단한 설명)
|
||||
*/
|
||||
private String definition;
|
||||
|
||||
/**
|
||||
* 용어가 사용된 맥락 (과거 회의록 참조)
|
||||
* 예: "신제품 기획 회의(2024-09-15)에서 언급"
|
||||
*/
|
||||
private String context;
|
||||
|
||||
/**
|
||||
* 관련 회의 ID (용어가 논의된 과거 회의)
|
||||
*/
|
||||
private String relatedMeetingId;
|
||||
|
||||
/**
|
||||
* 하이라이트 여부
|
||||
*/
|
||||
|
||||
@@ -48,6 +48,11 @@ public class RelatedTranscriptDto {
|
||||
*/
|
||||
private List<String> commonKeywords;
|
||||
|
||||
/**
|
||||
* 회의록 핵심 내용 요약 (1-2문장)
|
||||
*/
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 회의록 링크
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user