mirror of
https://github.com/hwanny1128/HGZero.git
synced 2026-06-13 10:39:11 +00:00
Refactor: AI 서비스 Python 구현 및 디렉토리 구조 변경
- ai-python: FastAPI 기반 AI 서비스 구현 - 실시간 회의 제안 기능 추가 - Claude API 통합 - EventHub 및 Redis 연동 - ai-java-back: 기존 Java AI 서비스 백업 디렉토리로 이동 - Spring Boot 기반 구현 보존 - ai 디렉토리: Java 서비스 파일 삭제 처리 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
package com.unicorn.hgzero.ai;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
|
||||
/**
|
||||
* AI Service Application
|
||||
* AI 기반 회의록 자동 작성 및 제안 서비스 메인 클래스
|
||||
* - LLM 기반 회의록 자동 작성
|
||||
* - Todo 자동 추출 및 담당자 식별
|
||||
* - 섹션 AI 요약 재생성
|
||||
* - 전문용어 맥락 기반 설명 (RAG)
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@ComponentScan(basePackages = {"com.unicorn.hgzero.ai", "com.unicorn.hgzero.common"})
|
||||
public class AiApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AiApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.unicorn.hgzero.ai.biz.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 추출된 Todo 도메인 모델
|
||||
* AI가 회의록에서 추출한 Todo 정보
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ExtractedTodo {
|
||||
|
||||
/**
|
||||
* Todo 내용
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 담당자
|
||||
*/
|
||||
private String assignee;
|
||||
|
||||
/**
|
||||
* 마감일
|
||||
*/
|
||||
private LocalDate dueDate;
|
||||
|
||||
/**
|
||||
* 우선순위 (HIGH, MEDIUM, LOW)
|
||||
*/
|
||||
private String priority;
|
||||
|
||||
/**
|
||||
* 관련 회의록 섹션
|
||||
*/
|
||||
private String sectionReference;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.unicorn.hgzero.ai.biz.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 처리된 회의록 도메인 모델
|
||||
* AI가 처리한 회의록 정보를 담는 도메인 객체
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ProcessedTranscript {
|
||||
|
||||
/**
|
||||
* 회의록 ID
|
||||
*/
|
||||
private String transcriptId;
|
||||
|
||||
/**
|
||||
* 회의 ID
|
||||
*/
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* 전체 요약
|
||||
*/
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 논의사항 목록
|
||||
*/
|
||||
private List<DiscussionItem> discussions;
|
||||
|
||||
/**
|
||||
* 결정사항 목록
|
||||
*/
|
||||
private List<DecisionItem> decisions;
|
||||
|
||||
/**
|
||||
* 보류사항 목록
|
||||
*/
|
||||
private List<String> pendingItems;
|
||||
|
||||
/**
|
||||
* 생성 시간
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 상태 (DRAFT, COMPLETED)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 논의사항 아이템
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class DiscussionItem {
|
||||
private String topic;
|
||||
private String speaker;
|
||||
private String content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 결정사항 아이템
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class DecisionItem {
|
||||
private String content;
|
||||
private String decisionMaker;
|
||||
private String category;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.unicorn.hgzero.ai.biz.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 관련 회의록 도메인 모델
|
||||
* RAG 검색으로 찾은 관련 회의록 정보
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RelatedMinutes {
|
||||
|
||||
/**
|
||||
* 회의록 ID
|
||||
*/
|
||||
private String transcriptId;
|
||||
|
||||
/**
|
||||
* 회의 제목
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 회의 날짜
|
||||
*/
|
||||
private LocalDate date;
|
||||
|
||||
/**
|
||||
* 참석자 목록
|
||||
*/
|
||||
private List<String> participants;
|
||||
|
||||
/**
|
||||
* 관련도 점수 (0-100)
|
||||
*/
|
||||
private Double relevanceScore;
|
||||
|
||||
/**
|
||||
* 공통 키워드 목록
|
||||
*/
|
||||
private List<String> commonKeywords;
|
||||
|
||||
/**
|
||||
* 회의록 핵심 내용 요약 (1-2문장)
|
||||
*/
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 회의록 링크
|
||||
*/
|
||||
private String link;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.unicorn.hgzero.ai.biz.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 제안사항 도메인 모델
|
||||
* AI가 제안하는 논의사항 또는 결정사항
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Suggestion {
|
||||
|
||||
/**
|
||||
* 제안 ID
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 제안 유형 (DISCUSSION, DECISION)
|
||||
*/
|
||||
private SuggestionType type;
|
||||
|
||||
/**
|
||||
* 제안 내용
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 우선순위 (HIGH, MEDIUM, LOW)
|
||||
*/
|
||||
private String priority;
|
||||
|
||||
/**
|
||||
* 제안 이유
|
||||
*/
|
||||
private String reason;
|
||||
|
||||
/**
|
||||
* 신뢰도 점수 (0-1)
|
||||
*/
|
||||
private Double confidence;
|
||||
|
||||
/**
|
||||
* 관련 안건
|
||||
*/
|
||||
private String relatedAgenda;
|
||||
|
||||
/**
|
||||
* 예상 소요 시간 (분)
|
||||
*/
|
||||
private Integer estimatedTime;
|
||||
|
||||
/**
|
||||
* 참여자 목록 (결정사항인 경우)
|
||||
*/
|
||||
private List<String> participants;
|
||||
|
||||
/**
|
||||
* 카테고리 (결정사항인 경우: 기술, 일정, 리소스, 정책, 기타)
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 원문 발췌 (결정사항인 경우)
|
||||
*/
|
||||
private String extractedFrom;
|
||||
|
||||
/**
|
||||
* 배경 설명 (결정사항인 경우)
|
||||
*/
|
||||
private String context;
|
||||
|
||||
/**
|
||||
* 제안 유형
|
||||
*/
|
||||
public enum SuggestionType {
|
||||
DISCUSSION, // 논의사항
|
||||
DECISION // 결정사항
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.unicorn.hgzero.ai.biz.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 전문용어 도메인 모델
|
||||
* 회의록에서 감지된 전문용어 정보
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Term {
|
||||
|
||||
/**
|
||||
* 용어명
|
||||
*/
|
||||
private String term;
|
||||
|
||||
/**
|
||||
* 텍스트 위치 정보
|
||||
*/
|
||||
private TextPosition position;
|
||||
|
||||
/**
|
||||
* 신뢰도 점수 (0-1)
|
||||
*/
|
||||
private Double confidence;
|
||||
|
||||
/**
|
||||
* 용어 카테고리 (기술, 업무, 도메인, 기획, 비즈니스, 전략, 마케팅)
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 용어 정의 (간단한 설명)
|
||||
*/
|
||||
private String definition;
|
||||
|
||||
/**
|
||||
* 용어가 사용된 맥락 (과거 회의록 참조)
|
||||
* 예: "신제품 기획 회의(2024-09-15)에서 언급"
|
||||
*/
|
||||
private String context;
|
||||
|
||||
/**
|
||||
* 관련 회의 ID (용어가 논의된 과거 회의)
|
||||
*/
|
||||
private String relatedMeetingId;
|
||||
|
||||
/**
|
||||
* 하이라이트 여부
|
||||
*/
|
||||
private Boolean highlight;
|
||||
|
||||
/**
|
||||
* 텍스트 위치
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class TextPosition {
|
||||
private Integer line;
|
||||
private Integer offset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.unicorn.hgzero.ai.biz.gateway;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* LLM Gateway 인터페이스
|
||||
* OpenAI API 연동을 추상화
|
||||
*/
|
||||
public interface LlmGateway {
|
||||
|
||||
/**
|
||||
* 회의록 자동 작성 (LLM 기반)
|
||||
*
|
||||
* @param transcriptText STT 변환 텍스트
|
||||
* @param title 회의 제목
|
||||
* @param participants 참석자 목록
|
||||
* @param agenda 회의 안건
|
||||
* @return LLM 생성 회의록 (JSON 형식)
|
||||
*/
|
||||
String generateTranscript(String transcriptText, String title, List<String> participants, List<String> agenda);
|
||||
|
||||
/**
|
||||
* Todo 추출 (LLM 기반)
|
||||
*
|
||||
* @param minutesContent 회의록 내용
|
||||
* @return 추출된 Todo JSON
|
||||
*/
|
||||
String extractTodos(String minutesContent);
|
||||
|
||||
/**
|
||||
* 섹션 요약 생성 (LLM 기반)
|
||||
*
|
||||
* @param sectionContent 섹션 내용
|
||||
* @param meetingContext 회의 맥락
|
||||
* @return 생성된 요약 (2-3문장)
|
||||
*/
|
||||
String generateSummary(String sectionContent, String meetingContext);
|
||||
|
||||
/**
|
||||
* 전문용어 감지 (LLM 기반)
|
||||
*
|
||||
* @param text 분석할 텍스트
|
||||
* @param organizationId 조직 ID
|
||||
* @return 감지된 용어 JSON
|
||||
*/
|
||||
String detectTerms(String text, String organizationId);
|
||||
|
||||
/**
|
||||
* 논의사항 제안 (LLM 기반)
|
||||
*
|
||||
* @param transcriptText 현재 회의록 텍스트
|
||||
* @param agenda 회의 안건
|
||||
* @return 논의사항 제안 JSON
|
||||
*/
|
||||
String suggestDiscussions(String transcriptText, List<String> agenda);
|
||||
|
||||
/**
|
||||
* 결정사항 제안 (LLM 기반)
|
||||
*
|
||||
* @param transcriptText 현재 회의록 텍스트
|
||||
* @return 결정사항 제안 JSON
|
||||
*/
|
||||
String suggestDecisions(String transcriptText);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.unicorn.hgzero.ai.biz.gateway;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* RAG 검색 Gateway 인터페이스
|
||||
* Azure AI Search 연동을 추상화
|
||||
*/
|
||||
public interface SearchGateway {
|
||||
|
||||
/**
|
||||
* 관련 회의록 검색 (벡터 유사도 기반)
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @param transcriptId 회의록 ID
|
||||
* @param limit 최대 개수
|
||||
* @return 관련 회의록 JSON
|
||||
*/
|
||||
String searchRelatedTranscripts(String meetingId, String transcriptId, int limit);
|
||||
|
||||
/**
|
||||
* 용어 설명을 위한 문서 검색
|
||||
*
|
||||
* @param term 용어명
|
||||
* @param meetingId 회의 ID
|
||||
* @param context 맥락
|
||||
* @return 관련 문서 JSON
|
||||
*/
|
||||
String searchTermExplanation(String term, String meetingId, String context);
|
||||
|
||||
/**
|
||||
* 회의록 인덱싱 (벡터 임베딩 저장)
|
||||
*
|
||||
* @param transcriptId 회의록 ID
|
||||
* @param content 회의록 내용
|
||||
* @param metadata 메타데이터
|
||||
*/
|
||||
void indexTranscript(String transcriptId, String content, String metadata);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.unicorn.hgzero.ai.biz.gateway;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.domain.ProcessedTranscript;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 회의록 데이터 Gateway 인터페이스
|
||||
* 회의록 영속성 관리를 추상화
|
||||
*/
|
||||
public interface TranscriptGateway {
|
||||
|
||||
/**
|
||||
* 회의록 저장
|
||||
*
|
||||
* @param transcript 처리된 회의록
|
||||
* @return 저장된 회의록
|
||||
*/
|
||||
ProcessedTranscript save(ProcessedTranscript transcript);
|
||||
|
||||
/**
|
||||
* 회의록 ID로 조회
|
||||
*
|
||||
* @param transcriptId 회의록 ID
|
||||
* @return 회의록 (Optional)
|
||||
*/
|
||||
Optional<ProcessedTranscript> findById(String transcriptId);
|
||||
|
||||
/**
|
||||
* 회의 ID로 조회
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return 회의록 (Optional)
|
||||
*/
|
||||
Optional<ProcessedTranscript> findByMeetingId(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의 ID 목록으로 조회
|
||||
*
|
||||
* @param meetingIds 회의 ID 목록
|
||||
* @return 회의록 목록
|
||||
*/
|
||||
List<ProcessedTranscript> findByMeetingIds(List<String> meetingIds);
|
||||
|
||||
/**
|
||||
* 상태로 조회
|
||||
*
|
||||
* @param status 상태
|
||||
* @return 회의록 목록
|
||||
*/
|
||||
List<ProcessedTranscript> findByStatus(String status);
|
||||
|
||||
/**
|
||||
* 회의록 존재 여부 확인
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return 존재 여부
|
||||
*/
|
||||
boolean existsByMeetingId(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의록 삭제
|
||||
*
|
||||
* @param transcriptId 회의록 ID
|
||||
*/
|
||||
void delete(String transcriptId);
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.unicorn.hgzero.ai.biz.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.unicorn.hgzero.ai.biz.domain.RelatedMinutes;
|
||||
import com.unicorn.hgzero.ai.biz.gateway.SearchGateway;
|
||||
import com.unicorn.hgzero.ai.biz.usecase.RelatedTranscriptSearchUseCase;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 관련 회의록 검색 Service
|
||||
* RAG 기반 벡터 유사도 검색
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RelatedTranscriptSearchService implements RelatedTranscriptSearchUseCase {
|
||||
|
||||
private final SearchGateway searchGateway;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public List<RelatedMinutes> findRelatedTranscripts(String meetingId, String transcriptId, int limit) {
|
||||
log.info("Searching related transcripts: meetingId={}, transcriptId={}, limit={}",
|
||||
meetingId, transcriptId, limit);
|
||||
|
||||
// RAG 검색
|
||||
String searchResult = searchGateway.searchRelatedTranscripts(meetingId, transcriptId, limit);
|
||||
|
||||
// TODO: JSON 파싱 및 RelatedMinutes 리스트 생성
|
||||
// 현재는 mock 데이터 반환
|
||||
return List.of(
|
||||
RelatedMinutes.builder()
|
||||
.transcriptId("aa0e8400-e29b-41d4-a716-446655440005")
|
||||
.title("프로젝트 X 주간 회의")
|
||||
.date(LocalDate.of(2025, 1, 15))
|
||||
.participants(List.of("김철수", "이영희"))
|
||||
.relevanceScore(85.5)
|
||||
.commonKeywords(List.of("MSA", "API Gateway", "Spring Boot"))
|
||||
.link("/transcripts/aa0e8400-e29b-41d4-a716-446655440005")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.unicorn.hgzero.ai.biz.service;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.gateway.LlmGateway;
|
||||
import com.unicorn.hgzero.ai.biz.usecase.SectionSummaryUseCase;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 섹션 AI 요약 재생성 Service
|
||||
* LLM 기반 섹션 요약 생성
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SectionSummaryService implements SectionSummaryUseCase {
|
||||
|
||||
private final LlmGateway llmGateway;
|
||||
|
||||
@Override
|
||||
public String regenerateSummary(String sectionId, String sectionContent, String meetingId) {
|
||||
log.info("Regenerating section summary: sectionId={}, meetingId={}", sectionId, meetingId);
|
||||
|
||||
// LLM을 통한 요약 생성
|
||||
String meetingContext = meetingId != null ? "회의 ID: " + meetingId : "";
|
||||
return llmGateway.generateSummary(sectionContent, meetingContext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
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.client.ClaudeApiClient;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.SimpleSuggestionDto;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.RealtimeSuggestionsDto;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Sinks;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 논의사항/결정사항 제안 Service
|
||||
* LLM 기반 실시간 회의 제안
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SuggestionService implements SuggestionUseCase {
|
||||
|
||||
private final LlmGateway llmGateway;
|
||||
private final ClaudeApiClient claudeApiClient;
|
||||
private final RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
// 회의별 실시간 스트림 관리 (회의 ID -> Sink)
|
||||
private final Map<String, Sinks.Many<RealtimeSuggestionsDto>> meetingSinks = new ConcurrentHashMap<>();
|
||||
|
||||
// 분석 임계값 설정 (MVP용 완화)
|
||||
private static final int MIN_SEGMENTS_FOR_ANALYSIS = 5; // 5개 세그먼트 = 약 50-100자 (MVP용 완화)
|
||||
private static final long TEXT_RETENTION_MS = 5 * 60 * 1000; // 5분
|
||||
|
||||
@Override
|
||||
public List<Suggestion> suggestDiscussions(String meetingId, String transcriptText) {
|
||||
log.info("Suggesting discussions: meetingId={}", meetingId);
|
||||
|
||||
// TODO: 회의 안건 조회
|
||||
List<String> agenda = List.of();
|
||||
|
||||
// LLM을 통한 논의사항 제안
|
||||
String llmResponse = llmGateway.suggestDiscussions(transcriptText, agenda);
|
||||
|
||||
// TODO: JSON 파싱 및 Suggestion 리스트 생성
|
||||
return List.of(
|
||||
Suggestion.builder()
|
||||
.id("sugg-001")
|
||||
.type(Suggestion.SuggestionType.DISCUSSION)
|
||||
.content("보안 요구사항 검토")
|
||||
.priority("HIGH")
|
||||
.reason("안건에 포함되어 있으나 아직 논의되지 않음")
|
||||
.confidence(0.9)
|
||||
.relatedAgenda("프로젝트 개요")
|
||||
.estimatedTime(15)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Suggestion> suggestDecisions(String meetingId, String transcriptText) {
|
||||
log.info("Suggesting decisions: meetingId={}", meetingId);
|
||||
|
||||
// LLM을 통한 결정사항 제안
|
||||
String llmResponse = llmGateway.suggestDecisions(transcriptText);
|
||||
|
||||
// TODO: JSON 파싱 및 Suggestion 리스트 생성
|
||||
return List.of(
|
||||
Suggestion.builder()
|
||||
.id("dec-001")
|
||||
.type(Suggestion.SuggestionType.DECISION)
|
||||
.content("React로 프론트엔드 개발")
|
||||
.category("기술")
|
||||
.participants(List.of("김철수", "이영희"))
|
||||
.confidence(0.85)
|
||||
.extractedFrom("프론트엔드는 React로 개발하기로 했습니다")
|
||||
.context("팀원 대부분이 React 경험이 있어 개발 속도가 빠를 것으로 예상")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<RealtimeSuggestionsDto> streamRealtimeSuggestions(String meetingId) {
|
||||
log.info("실시간 AI 제안사항 스트리밍 시작 - meetingId: {}", meetingId);
|
||||
|
||||
// Sink 생성 및 등록 (멀티캐스트 - 여러 클라이언트 동시 지원)
|
||||
Sinks.Many<RealtimeSuggestionsDto> sink = Sinks.many()
|
||||
.multicast()
|
||||
.onBackpressureBuffer();
|
||||
|
||||
meetingSinks.put(meetingId, sink);
|
||||
|
||||
// TODO: AI 개발 완료 후 제거 - 개발 중 프론트엔드 테스트를 위한 Mock 데이터 자동 발행
|
||||
startMockDataEmission(meetingId, sink);
|
||||
|
||||
return sink.asFlux()
|
||||
.doOnCancel(() -> {
|
||||
log.info("SSE 스트림 종료 - meetingId: {}", meetingId);
|
||||
meetingSinks.remove(meetingId);
|
||||
cleanupMeetingData(meetingId);
|
||||
})
|
||||
.doOnError(error ->
|
||||
log.error("AI 제안사항 스트리밍 오류 - meetingId: {}", meetingId, error));
|
||||
}
|
||||
|
||||
/**
|
||||
* Event Hub에서 수신한 실시간 텍스트 처리
|
||||
* STT Service에서 TranscriptSegmentReady 이벤트를 받아 처리
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @param text 변환된 텍스트 세그먼트
|
||||
* @param timestamp 타임스탬프 (ms)
|
||||
*/
|
||||
public void processRealtimeTranscript(String meetingId, String text, Long timestamp) {
|
||||
try {
|
||||
// 1. Redis에 실시간 텍스트 축적 (슬라이딩 윈도우: 최근 5분)
|
||||
String key = "meeting:" + meetingId + ":transcript";
|
||||
String value = timestamp + ":" + text;
|
||||
|
||||
redisTemplate.opsForZSet().add(key, value, timestamp.doubleValue());
|
||||
|
||||
// 5분 이전 데이터 제거
|
||||
long fiveMinutesAgo = System.currentTimeMillis() - TEXT_RETENTION_MS;
|
||||
redisTemplate.opsForZSet().removeRangeByScore(key, 0, fiveMinutesAgo);
|
||||
|
||||
// 2. 누적 텍스트가 임계값 이상이면 AI 분석
|
||||
Long segmentCount = redisTemplate.opsForZSet().size(key);
|
||||
if (segmentCount != null && segmentCount >= MIN_SEGMENTS_FOR_ANALYSIS) {
|
||||
analyzeAndEmitSuggestions(meetingId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("실시간 텍스트 처리 실패 - meetingId: {}", meetingId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 분석 및 SSE 발행
|
||||
*/
|
||||
private void analyzeAndEmitSuggestions(String meetingId) {
|
||||
// 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"));
|
||||
|
||||
// Claude API 분석 (비동기)
|
||||
claudeApiClient.analyzeSuggestions(accumulatedText)
|
||||
.subscribe(
|
||||
suggestions -> {
|
||||
// SSE 스트림으로 전송
|
||||
Sinks.Many<RealtimeSuggestionsDto> sink = meetingSinks.get(meetingId);
|
||||
if (sink != null) {
|
||||
sink.tryEmitNext(suggestions);
|
||||
log.info("AI 제안사항 발행 완료 - meetingId: {}, 제안사항: {}개",
|
||||
meetingId,
|
||||
suggestions.getSuggestions().size());
|
||||
}
|
||||
},
|
||||
error -> log.error("Claude API 분석 실패 - meetingId: {}", meetingId, error)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 종료 시 데이터 정리
|
||||
*/
|
||||
private void cleanupMeetingData(String meetingId) {
|
||||
String key = "meeting:" + meetingId + ":transcript";
|
||||
redisTemplate.delete(key);
|
||||
log.info("회의 데이터 정리 완료 - meetingId: {}", meetingId);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: AI 개발 완료 후 제거
|
||||
* Mock 데이터 자동 발행 (프론트엔드 개발용)
|
||||
* 5초마다 샘플 제안사항을 발행합니다.
|
||||
*/
|
||||
private void startMockDataEmission(String meetingId, Sinks.Many<RealtimeSuggestionsDto> sink) {
|
||||
log.info("Mock 데이터 자동 발행 시작 - meetingId: {}", meetingId);
|
||||
|
||||
// 프론트엔드 HTML에 맞춘 샘플 데이터 (3개)
|
||||
List<SimpleSuggestionDto> mockSuggestions = List.of(
|
||||
SimpleSuggestionDto.builder()
|
||||
.id("suggestion-1")
|
||||
.content("신제품의 타겟 고객층을 20-30대로 설정하고, 모바일 우선 전략을 취하기로 논의 중입니다.")
|
||||
.timestamp("00:05:23")
|
||||
.confidence(0.92)
|
||||
.build(),
|
||||
SimpleSuggestionDto.builder()
|
||||
.id("suggestion-2")
|
||||
.content("개발 일정: 1차 프로토타입은 11월 15일까지 완성, 2차 베타는 12월 1일 론칭")
|
||||
.timestamp("00:08:45")
|
||||
.confidence(0.88)
|
||||
.build(),
|
||||
SimpleSuggestionDto.builder()
|
||||
.id("suggestion-3")
|
||||
.content("마케팅 예산 배분에 대해 SNS 광고 60%, 인플루언서 마케팅 40%로 의견이 나왔으나 추가 검토 필요")
|
||||
.timestamp("00:12:18")
|
||||
.confidence(0.85)
|
||||
.build()
|
||||
);
|
||||
|
||||
// 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 제안사항 발행 - meetingId: {}, 제안: {}",
|
||||
meetingId,
|
||||
suggestions.getSuggestions().get(0).getContent());
|
||||
},
|
||||
error -> log.error("Mock 데이터 발행 오류 - meetingId: {}", meetingId, error),
|
||||
() -> log.info("Mock 데이터 발행 완료 - 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<SimpleSuggestionDto> suggestions = List.of(
|
||||
SimpleSuggestionDto.builder()
|
||||
.id("sugg-" + sequence)
|
||||
.content(getMockSuggestionContent(sequence))
|
||||
.timestamp(getCurrentTimestamp())
|
||||
.confidence(0.85 + (sequence % 15) * 0.01)
|
||||
.build()
|
||||
);
|
||||
|
||||
return RealtimeSuggestionsDto.builder()
|
||||
.suggestions(suggestions)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 제안사항 내용 생성
|
||||
*/
|
||||
private String getMockSuggestionContent(Long sequence) {
|
||||
String[] suggestions = {
|
||||
"신제품의 타겟 고객층을 20-30대로 설정하고, 모바일 우선 전략을 취하기로 논의 중입니다.",
|
||||
"개발 일정: 1차 프로토타입은 11월 15일까지 완성, 2차 베타는 12월 1일 론칭",
|
||||
"마케팅 예산 배분에 대해 SNS 광고 60%, 인플루언서 마케팅 40%로 의견이 나왔으나 추가 검토 필요",
|
||||
"보안 요구사항 검토가 필요하며, 데이터 암호화 방식에 대한 논의가 진행 중입니다.",
|
||||
"React로 프론트엔드 개발하기로 결정되었으며, TypeScript 사용을 권장합니다.",
|
||||
"데이터베이스는 PostgreSQL을 메인으로 사용하고, Redis를 캐시로 활용하기로 했습니다."
|
||||
};
|
||||
return suggestions[(int) (sequence % suggestions.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 타임스탬프 생성 (HH:MM:SS 형식)
|
||||
*/
|
||||
private String getCurrentTimestamp() {
|
||||
java.time.LocalTime now = java.time.LocalTime.now();
|
||||
return String.format("%02d:%02d:%02d",
|
||||
now.getHour(),
|
||||
now.getMinute(),
|
||||
now.getSecond());
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.unicorn.hgzero.ai.biz.service;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.domain.Term;
|
||||
import com.unicorn.hgzero.ai.biz.gateway.LlmGateway;
|
||||
import com.unicorn.hgzero.ai.biz.usecase.TermDetectionUseCase;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 전문용어 감지 Service
|
||||
* LLM 기반 전문용어 자동 감지
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TermDetectionService implements TermDetectionUseCase {
|
||||
|
||||
private final LlmGateway llmGateway;
|
||||
|
||||
@Override
|
||||
public List<Term> detectTerms(String meetingId, String text, String organizationId) {
|
||||
log.info("Detecting terms: meetingId={}, organizationId={}", meetingId, organizationId);
|
||||
|
||||
// LLM을 통한 전문용어 감지
|
||||
String llmResponse = llmGateway.detectTerms(text, organizationId);
|
||||
|
||||
// TODO: JSON 파싱 및 Term 리스트 생성
|
||||
// 현재는 mock 데이터 반환
|
||||
return List.of(
|
||||
Term.builder()
|
||||
.term("MSA")
|
||||
.position(Term.TextPosition.builder().line(5).offset(42).build())
|
||||
.confidence(0.92)
|
||||
.category("기술")
|
||||
.highlight(true)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.unicorn.hgzero.ai.biz.service;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.gateway.SearchGateway;
|
||||
import com.unicorn.hgzero.ai.biz.usecase.TermExplanationUseCase;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 전문용어 설명 Service
|
||||
* RAG 기반 맥락적 용어 설명 생성
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TermExplanationService implements TermExplanationUseCase {
|
||||
|
||||
private final SearchGateway searchGateway;
|
||||
|
||||
@Override
|
||||
public TermExplanationResult explainTerm(String term, String meetingId, String context) {
|
||||
log.info("Explaining term: term={}, meetingId={}", term, meetingId);
|
||||
|
||||
// RAG 검색
|
||||
String searchResult = searchGateway.searchTermExplanation(term, meetingId, context);
|
||||
|
||||
// TODO: JSON 파싱 및 TermExplanationResult 생성
|
||||
// 현재는 mock 데이터 반환
|
||||
return new TermExplanationResult(
|
||||
"MSA",
|
||||
"Microservices Architecture의 약자",
|
||||
"이번 프로젝트에서는 확장성과 독립 배포를 위해 MSA를 적용하기로 결정",
|
||||
List.of(
|
||||
"2024년 프로젝트 X에서 주문/결제/배송 서비스를 독립적으로 구성",
|
||||
"서비스별 독립 배포로 배포 시간 70% 단축"
|
||||
),
|
||||
List.of(new RelatedProject("프로젝트 X", "동일한 MSA 아키텍처 적용")),
|
||||
List.of(new PastDiscussion(
|
||||
LocalDate.of(2024, 12, 15),
|
||||
List.of("김철수", "이영희"),
|
||||
"MSA 아키텍처의 장단점을 비교하고 적용 방안을 논의",
|
||||
"/transcripts/bb0e8400-e29b-41d4-a716-446655440006"
|
||||
)),
|
||||
List.of(new Reference(
|
||||
"MSA 아키텍처 가이드",
|
||||
"위키",
|
||||
"https://wiki.example.com/msa-guide"
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.unicorn.hgzero.ai.biz.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.unicorn.hgzero.ai.biz.domain.ExtractedTodo;
|
||||
import com.unicorn.hgzero.ai.biz.gateway.LlmGateway;
|
||||
import com.unicorn.hgzero.ai.biz.usecase.TodoExtractionUseCase;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Todo 자동 추출 Service
|
||||
* LLM 기반 액션 아이템 추출
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TodoExtractionService implements TodoExtractionUseCase {
|
||||
|
||||
private final LlmGateway llmGateway;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public List<ExtractedTodo> extractTodos(String meetingId, String minutesContent, String userId) {
|
||||
log.info("Extracting todos from minutes: meetingId={}, userId={}", meetingId, userId);
|
||||
|
||||
// LLM을 통한 Todo 추출
|
||||
String llmResponse = llmGateway.extractTodos(minutesContent);
|
||||
|
||||
// TODO: JSON 파싱 및 ExtractedTodo 리스트 생성
|
||||
// 현재는 mock 데이터 반환
|
||||
return List.of(
|
||||
ExtractedTodo.builder()
|
||||
.content("API 설계서 작성")
|
||||
.assignee("박민수")
|
||||
.dueDate(LocalDate.of(2025, 1, 30))
|
||||
.priority("HIGH")
|
||||
.sectionReference("결정사항 #3")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
package com.unicorn.hgzero.ai.biz.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.unicorn.hgzero.ai.biz.domain.ProcessedTranscript;
|
||||
import com.unicorn.hgzero.ai.biz.gateway.LlmGateway;
|
||||
import com.unicorn.hgzero.ai.biz.gateway.SearchGateway;
|
||||
import com.unicorn.hgzero.ai.biz.gateway.TranscriptGateway;
|
||||
import com.unicorn.hgzero.ai.biz.usecase.TranscriptProcessUseCase;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 회의록 자동 작성 Service
|
||||
* LLM 기반 회의록 생성 및 저장
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TranscriptProcessService implements TranscriptProcessUseCase {
|
||||
|
||||
private final LlmGateway llmGateway;
|
||||
private final SearchGateway searchGateway;
|
||||
private final TranscriptGateway transcriptGateway;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ProcessedTranscript processTranscript(
|
||||
String meetingId,
|
||||
String transcriptText,
|
||||
String userId,
|
||||
String userName,
|
||||
String title,
|
||||
List<String> participants,
|
||||
List<String> agenda
|
||||
) {
|
||||
log.info("Processing transcript for meeting: meetingId={}, userId={}", meetingId, userId);
|
||||
|
||||
// 1. LLM을 통한 회의록 자동 생성
|
||||
String llmResponse = llmGateway.generateTranscript(transcriptText, title, participants, agenda);
|
||||
log.debug("LLM response received: length={}", llmResponse.length());
|
||||
|
||||
// 2. LLM 응답 파싱
|
||||
ProcessedTranscript processedTranscript = parseTranscriptFromLlm(llmResponse, meetingId);
|
||||
|
||||
// 3. 회의록 저장
|
||||
ProcessedTranscript saved = transcriptGateway.save(processedTranscript);
|
||||
log.info("Transcript saved: transcriptId={}, meetingId={}", saved.getTranscriptId(), meetingId);
|
||||
|
||||
// 4. RAG 인덱싱 (비동기 처리 고려)
|
||||
indexTranscriptForSearch(saved);
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public ProcessedTranscript getTranscript(String transcriptId) {
|
||||
log.debug("Retrieving transcript: transcriptId={}", transcriptId);
|
||||
return transcriptGateway.findById(transcriptId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Transcript not found: " + transcriptId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public ProcessedTranscript getTranscriptByMeetingId(String meetingId) {
|
||||
log.debug("Retrieving transcript by meetingId: {}", meetingId);
|
||||
return transcriptGateway.findByMeetingId(meetingId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Transcript not found for meeting: " + meetingId));
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM 응답을 ProcessedTranscript 도메인으로 파싱
|
||||
*/
|
||||
private ProcessedTranscript parseTranscriptFromLlm(String llmResponse, String meetingId) {
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(llmResponse);
|
||||
|
||||
// Discussions 파싱
|
||||
List<ProcessedTranscript.DiscussionItem> discussions = new ArrayList<>();
|
||||
if (root.has("discussions")) {
|
||||
root.get("discussions").forEach(node -> {
|
||||
discussions.add(ProcessedTranscript.DiscussionItem.builder()
|
||||
.topic(node.get("topic").asText())
|
||||
.speaker(node.get("speaker").asText())
|
||||
.content(node.get("content").asText())
|
||||
.build());
|
||||
});
|
||||
}
|
||||
|
||||
// Decisions 파싱
|
||||
List<ProcessedTranscript.DecisionItem> decisions = new ArrayList<>();
|
||||
if (root.has("decisions")) {
|
||||
root.get("decisions").forEach(node -> {
|
||||
decisions.add(ProcessedTranscript.DecisionItem.builder()
|
||||
.content(node.get("content").asText())
|
||||
.decisionMaker(node.get("decisionMaker").asText())
|
||||
.category(node.get("category").asText())
|
||||
.build());
|
||||
});
|
||||
}
|
||||
|
||||
// Pending items 파싱
|
||||
List<String> pendingItems = new ArrayList<>();
|
||||
if (root.has("pendingItems")) {
|
||||
root.get("pendingItems").forEach(node -> pendingItems.add(node.asText()));
|
||||
}
|
||||
|
||||
return ProcessedTranscript.builder()
|
||||
.transcriptId(UUID.randomUUID().toString())
|
||||
.meetingId(meetingId)
|
||||
.summary(root.has("summary") ? root.get("summary").asText() : "")
|
||||
.discussions(discussions)
|
||||
.decisions(decisions)
|
||||
.pendingItems(pendingItems)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.status("DRAFT")
|
||||
.build();
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Failed to parse LLM response: {}", llmResponse, e);
|
||||
throw new RuntimeException("Failed to parse transcript from LLM", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RAG 검색을 위한 회의록 인덱싱
|
||||
*/
|
||||
private void indexTranscriptForSearch(ProcessedTranscript transcript) {
|
||||
try {
|
||||
String content = buildSearchableContent(transcript);
|
||||
String metadata = buildMetadata(transcript);
|
||||
|
||||
searchGateway.indexTranscript(transcript.getTranscriptId(), content, metadata);
|
||||
log.debug("Transcript indexed for search: transcriptId={}", transcript.getTranscriptId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to index transcript for search: transcriptId={}",
|
||||
transcript.getTranscriptId(), e);
|
||||
// 인덱싱 실패는 치명적이지 않으므로 예외를 전파하지 않음
|
||||
}
|
||||
}
|
||||
|
||||
private String buildSearchableContent(ProcessedTranscript transcript) {
|
||||
StringBuilder content = new StringBuilder();
|
||||
content.append(transcript.getSummary()).append("\n\n");
|
||||
|
||||
if (transcript.getDiscussions() != null) {
|
||||
transcript.getDiscussions().forEach(d ->
|
||||
content.append(d.getTopic()).append(": ").append(d.getContent()).append("\n")
|
||||
);
|
||||
}
|
||||
|
||||
if (transcript.getDecisions() != null) {
|
||||
transcript.getDecisions().forEach(d ->
|
||||
content.append("결정: ").append(d.getContent()).append("\n")
|
||||
);
|
||||
}
|
||||
|
||||
return content.toString();
|
||||
}
|
||||
|
||||
private String buildMetadata(ProcessedTranscript transcript) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(transcript);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.warn("Failed to serialize transcript metadata", e);
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.unicorn.hgzero.ai.biz.usecase;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.domain.RelatedMinutes;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 관련 회의록 검색 UseCase
|
||||
* RAG 기반 벡터 유사도 검색으로 관련 회의록 조회
|
||||
*/
|
||||
public interface RelatedTranscriptSearchUseCase {
|
||||
|
||||
/**
|
||||
* 관련 회의록 검색
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @param transcriptId 회의록 ID
|
||||
* @param limit 반환할 최대 개수
|
||||
* @return 관련 회의록 목록
|
||||
*/
|
||||
List<RelatedMinutes> findRelatedTranscripts(String meetingId, String transcriptId, int limit);
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.unicorn.hgzero.ai.biz.usecase;
|
||||
|
||||
/**
|
||||
* 섹션 AI 요약 재생성 UseCase
|
||||
* 사용자가 작성한 섹션 내용을 기반으로 AI 요약 재생성
|
||||
*/
|
||||
public interface SectionSummaryUseCase {
|
||||
|
||||
/**
|
||||
* 섹션 요약 재생성
|
||||
*
|
||||
* @param sectionId 섹션 ID
|
||||
* @param sectionContent 섹션 내용 (Markdown 형식)
|
||||
* @param meetingId 회의 ID (선택적, 맥락 이해용)
|
||||
* @return 생성된 AI 요약 (2-3문장)
|
||||
*/
|
||||
String regenerateSummary(String sectionId, String sectionContent, String meetingId);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* 논의사항/결정사항 제안 UseCase
|
||||
* AI 기반 실시간 회의 제안 기능
|
||||
*/
|
||||
public interface SuggestionUseCase {
|
||||
|
||||
/**
|
||||
* 논의사항 제안
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @param transcriptText 현재까지의 회의록 텍스트
|
||||
* @return 논의사항 제안 목록
|
||||
*/
|
||||
List<Suggestion> suggestDiscussions(String meetingId, String transcriptText);
|
||||
|
||||
/**
|
||||
* 결정사항 제안
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @param transcriptText 현재까지의 회의록 텍스트
|
||||
* @return 결정사항 제안 목록
|
||||
*/
|
||||
List<Suggestion> suggestDecisions(String meetingId, String transcriptText);
|
||||
|
||||
/**
|
||||
* 실시간 AI 제안사항 스트리밍
|
||||
* 회의 진행 중 실시간으로 논의사항과 결정사항을 분석하여 제안
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return 실시간 제안사항 스트림
|
||||
*/
|
||||
Flux<RealtimeSuggestionsDto> streamRealtimeSuggestions(String meetingId);
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.unicorn.hgzero.ai.biz.usecase;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.domain.Term;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 전문용어 감지 UseCase
|
||||
* 회의록 텍스트에서 전문용어를 자동으로 감지
|
||||
*/
|
||||
public interface TermDetectionUseCase {
|
||||
|
||||
/**
|
||||
* 전문용어 감지
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @param text 분석할 회의록 텍스트
|
||||
* @param organizationId 조직 ID
|
||||
* @return 감지된 전문용어 목록
|
||||
*/
|
||||
List<Term> detectTerms(String meetingId, String text, String organizationId);
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.unicorn.hgzero.ai.biz.usecase;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 전문용어 설명 UseCase
|
||||
* RAG 기반 맥락적 용어 설명 생성
|
||||
*/
|
||||
public interface TermExplanationUseCase {
|
||||
|
||||
/**
|
||||
* 용어 설명 생성
|
||||
*
|
||||
* @param term 용어명
|
||||
* @param meetingId 회의 ID
|
||||
* @param context 현재 회의 맥락 (선택)
|
||||
* @return 용어 설명 결과
|
||||
*/
|
||||
TermExplanationResult explainTerm(String term, String meetingId, String context);
|
||||
|
||||
/**
|
||||
* 용어 설명 결과
|
||||
*/
|
||||
record TermExplanationResult(
|
||||
String term,
|
||||
String basicDefinition,
|
||||
String contextualMeaning,
|
||||
List<String> useCases,
|
||||
List<RelatedProject> relatedProjects,
|
||||
List<PastDiscussion> pastDiscussions,
|
||||
List<Reference> references
|
||||
) {}
|
||||
|
||||
record RelatedProject(String name, String relevance) {}
|
||||
record PastDiscussion(java.time.LocalDate date, List<String> participants, String summary, String link) {}
|
||||
record Reference(String title, String type, String link) {}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.unicorn.hgzero.ai.biz.usecase;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.domain.ExtractedTodo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Todo 자동 추출 UseCase
|
||||
* 회의록에서 액션 아이템을 자동으로 추출하고 담당자 식별
|
||||
*/
|
||||
public interface TodoExtractionUseCase {
|
||||
|
||||
/**
|
||||
* 회의록에서 Todo 추출
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @param minutesContent 회의록 전체 내용 (Markdown 형식)
|
||||
* @param userId 요청자 ID
|
||||
* @return 추출된 Todo 목록
|
||||
*/
|
||||
List<ExtractedTodo> extractTodos(String meetingId, String minutesContent, String userId);
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.unicorn.hgzero.ai.biz.usecase;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.domain.ProcessedTranscript;
|
||||
|
||||
/**
|
||||
* 회의록 자동 작성 UseCase
|
||||
* STT에서 변환된 텍스트를 받아 LLM 기반으로 회의록 자동 작성
|
||||
*/
|
||||
public interface TranscriptProcessUseCase {
|
||||
|
||||
/**
|
||||
* 회의록 자동 작성
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @param transcriptText STT에서 변환된 텍스트
|
||||
* @param userId 사용자 ID
|
||||
* @param userName 사용자 이름
|
||||
* @param title 회의 제목
|
||||
* @param participants 참석자 목록
|
||||
* @param agenda 회의 안건
|
||||
* @return 처리된 회의록
|
||||
*/
|
||||
ProcessedTranscript processTranscript(
|
||||
String meetingId,
|
||||
String transcriptText,
|
||||
String userId,
|
||||
String userName,
|
||||
String title,
|
||||
java.util.List<String> participants,
|
||||
java.util.List<String> agenda
|
||||
);
|
||||
|
||||
/**
|
||||
* 회의록 조회
|
||||
*
|
||||
* @param transcriptId 회의록 ID
|
||||
* @return 처리된 회의록
|
||||
*/
|
||||
ProcessedTranscript getTranscript(String transcriptId);
|
||||
|
||||
/**
|
||||
* 회의 ID로 회의록 조회
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return 처리된 회의록
|
||||
*/
|
||||
ProcessedTranscript getTranscriptByMeetingId(String meetingId);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package com.unicorn.hgzero.ai.infra.client;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.unicorn.hgzero.ai.infra.config.ClaudeConfig;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.RealtimeSuggestionsDto;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.SimpleSuggestionDto;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Claude API 클라이언트
|
||||
* Anthropic Claude API를 호출하여 AI 제안사항 생성
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ClaudeApiClient {
|
||||
|
||||
private final ClaudeConfig claudeConfig;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final WebClient webClient;
|
||||
|
||||
/**
|
||||
* 실시간 AI 제안사항 분석 (간소화 버전)
|
||||
*
|
||||
* @param transcriptText 누적된 회의록 텍스트
|
||||
* @return AI 제안사항 (논의사항과 결정사항 통합)
|
||||
*/
|
||||
public Mono<RealtimeSuggestionsDto> analyzeSuggestions(String transcriptText) {
|
||||
log.debug("Claude API 호출 - 텍스트 길이: {}", transcriptText.length());
|
||||
|
||||
String systemPrompt = """
|
||||
당신은 회의록 작성 전문 AI 어시스턴트입니다.
|
||||
|
||||
실시간 회의 텍스트를 분석하여 **제안사항을 적극적으로** 추출하세요.
|
||||
|
||||
**추출 대상 (MVP용 - 넓은 기준)**:
|
||||
- 회의 안건 관련 내용
|
||||
- 논의 중인 주제 (확정되지 않아도 OK)
|
||||
- 의견이나 제안
|
||||
- 결정된 사항
|
||||
- 액션 아이템
|
||||
- 계획이나 일정 관련 언급
|
||||
- 검토가 필요한 내용
|
||||
|
||||
**제외할 내용** (최소화):
|
||||
- 명백한 잡담이나 농담
|
||||
- 회의 시작/종료 인사말
|
||||
|
||||
**응답 형식**: JSON만 반환 (다른 설명 없이)
|
||||
{
|
||||
"suggestions": [
|
||||
{
|
||||
"content": "구체적인 제안 내용 (자연스러운 문장으로)",
|
||||
"confidence": 0.7
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
**주의**:
|
||||
- 확신이 없어도 제안사항으로 포함 (confidence 0.6 이상이면 OK)
|
||||
- 회의 내용에서 의미 있는 내용은 모두 제안사항으로 추출
|
||||
- confidence는 0-1 사이 값 (MVP에서는 낮아도 괜찮음)
|
||||
""";
|
||||
|
||||
String userPrompt = String.format("""
|
||||
다음 회의 내용을 분석해주세요:
|
||||
|
||||
%s
|
||||
""", transcriptText);
|
||||
|
||||
// Claude API 요청 페이로드
|
||||
Map<String, Object> requestBody = Map.of(
|
||||
"model", claudeConfig.getModel(),
|
||||
"max_tokens", claudeConfig.getMaxTokens(),
|
||||
"temperature", claudeConfig.getTemperature(),
|
||||
"system", systemPrompt,
|
||||
"messages", List.of(
|
||||
Map.of(
|
||||
"role", "user",
|
||||
"content", userPrompt
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return webClient.post()
|
||||
.uri(claudeConfig.getBaseUrl() + "/v1/messages")
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.header("x-api-key", claudeConfig.getApiKey())
|
||||
.header("anthropic-version", "2023-06-01")
|
||||
.bodyValue(requestBody)
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
.map(this::parseClaudeResponse)
|
||||
.doOnSuccess(result -> log.info("Claude API 응답 성공 - 제안사항: {}개",
|
||||
result.getSuggestions().size()))
|
||||
.doOnError(error -> log.error("Claude API 호출 실패", error))
|
||||
.onErrorResume(error -> Mono.just(RealtimeSuggestionsDto.builder()
|
||||
.suggestions(new ArrayList<>())
|
||||
.build()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Claude API 응답 파싱 (간소화 버전)
|
||||
*/
|
||||
private RealtimeSuggestionsDto parseClaudeResponse(String responseBody) {
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(responseBody);
|
||||
|
||||
// Claude 응답 구조: { "content": [ { "text": "..." } ] }
|
||||
String contentText = root.path("content").get(0).path("text").asText();
|
||||
|
||||
// JSON 부분만 추출 (코드 블록 제거)
|
||||
String jsonText = extractJson(contentText);
|
||||
|
||||
JsonNode suggestionsJson = objectMapper.readTree(jsonText);
|
||||
|
||||
// 제안사항 파싱
|
||||
List<SimpleSuggestionDto> suggestions = new ArrayList<>();
|
||||
JsonNode suggestionsNode = suggestionsJson.path("suggestions");
|
||||
if (suggestionsNode.isArray()) {
|
||||
for (JsonNode node : suggestionsNode) {
|
||||
suggestions.add(SimpleSuggestionDto.builder()
|
||||
.id(UUID.randomUUID().toString())
|
||||
.content(node.path("content").asText())
|
||||
.confidence(node.path("confidence").asDouble(0.8))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
return RealtimeSuggestionsDto.builder()
|
||||
.suggestions(suggestions)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Claude 응답 파싱 실패", e);
|
||||
return RealtimeSuggestionsDto.builder()
|
||||
.suggestions(new ArrayList<>())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 응답에서 JSON 부분만 추출
|
||||
* Claude가 마크다운 코드 블록으로 감싼 경우 처리
|
||||
*/
|
||||
private String extractJson(String text) {
|
||||
// ```json ... ``` 형식 제거
|
||||
if (text.contains("```json")) {
|
||||
int start = text.indexOf("```json") + 7;
|
||||
int end = text.lastIndexOf("```");
|
||||
return text.substring(start, end).trim();
|
||||
}
|
||||
// ``` ... ``` 형식 제거
|
||||
else if (text.contains("```")) {
|
||||
int start = text.indexOf("```") + 3;
|
||||
int end = text.lastIndexOf("```");
|
||||
return text.substring(start, end).trim();
|
||||
}
|
||||
return text.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.unicorn.hgzero.ai.infra.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Claude API 설정
|
||||
*/
|
||||
@Configuration
|
||||
@Getter
|
||||
public class ClaudeConfig {
|
||||
|
||||
@Value("${external.ai.claude.api-key}")
|
||||
private String apiKey;
|
||||
|
||||
@Value("${external.ai.claude.base-url}")
|
||||
private String baseUrl;
|
||||
|
||||
@Value("${external.ai.claude.model}")
|
||||
private String model;
|
||||
|
||||
@Value("${external.ai.claude.max-tokens}")
|
||||
private Integer maxTokens;
|
||||
|
||||
@Value("${external.ai.claude.temperature}")
|
||||
private Double temperature;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.unicorn.hgzero.ai.infra.config;
|
||||
|
||||
import com.azure.messaging.eventhubs.EventProcessorClient;
|
||||
import com.azure.messaging.eventhubs.EventProcessorClientBuilder;
|
||||
import com.azure.messaging.eventhubs.checkpointstore.blob.BlobCheckpointStore;
|
||||
import com.azure.messaging.eventhubs.models.ErrorContext;
|
||||
import com.azure.messaging.eventhubs.models.EventContext;
|
||||
import com.azure.storage.blob.BlobContainerAsyncClient;
|
||||
import com.azure.storage.blob.BlobContainerClientBuilder;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.unicorn.hgzero.ai.biz.service.SuggestionService;
|
||||
import com.unicorn.hgzero.ai.infra.event.TranscriptSegmentReadyEvent;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
/**
|
||||
* Azure Event Hub 설정
|
||||
* STT Service의 TranscriptSegmentReady 이벤트 구독
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class EventHubConfig {
|
||||
|
||||
private final SuggestionService suggestionService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Value("${external.eventhub.connection-string}")
|
||||
private String connectionString;
|
||||
|
||||
@Value("${external.eventhub.eventhub-name}")
|
||||
private String eventHubName;
|
||||
|
||||
@Value("${external.eventhub.consumer-group.transcript}")
|
||||
private String consumerGroup;
|
||||
|
||||
@Value("${external.eventhub.checkpoint-storage-connection-string:}")
|
||||
private String checkpointStorageConnectionString;
|
||||
|
||||
@Value("${external.eventhub.checkpoint-container}")
|
||||
private String checkpointContainer;
|
||||
|
||||
private EventProcessorClient eventProcessorClient;
|
||||
|
||||
@PostConstruct
|
||||
public void startEventProcessor() {
|
||||
log.info("Event Hub Processor 시작 - eventhub: {}, consumerGroup: {}",
|
||||
eventHubName, consumerGroup);
|
||||
|
||||
EventProcessorClientBuilder builder = new EventProcessorClientBuilder()
|
||||
.connectionString(connectionString, eventHubName)
|
||||
.consumerGroup(consumerGroup)
|
||||
.processEvent(this::processEvent)
|
||||
.processError(this::processError);
|
||||
|
||||
// Checkpoint Storage 설정
|
||||
if (checkpointStorageConnectionString != null && !checkpointStorageConnectionString.isEmpty()) {
|
||||
log.info("Checkpoint Storage 활성화 (Azure Blob) - container: {}", checkpointContainer);
|
||||
BlobContainerAsyncClient blobContainerAsyncClient = new BlobContainerClientBuilder()
|
||||
.connectionString(checkpointStorageConnectionString)
|
||||
.containerName(checkpointContainer)
|
||||
.buildAsyncClient();
|
||||
builder.checkpointStore(new BlobCheckpointStore(blobContainerAsyncClient));
|
||||
} else {
|
||||
log.warn("⚠️ Checkpoint Storage 미설정 - 체크포인트 저장 안 함 (재시작 시 처음부터 읽음)");
|
||||
log.warn("⚠️ 프로덕션 환경에서는 AZURE_BLOB_CONNECTION_STRING 설정 필요");
|
||||
// Checkpoint Store 없이 실행 (재시작 시 처음부터 읽음)
|
||||
}
|
||||
|
||||
eventProcessorClient = builder.buildEventProcessorClient();
|
||||
eventProcessorClient.start();
|
||||
|
||||
log.info("Event Hub Processor 시작 완료");
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stopEventProcessor() {
|
||||
if (eventProcessorClient != null) {
|
||||
log.info("Event Hub Processor 종료");
|
||||
eventProcessorClient.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 처리 핸들러
|
||||
*/
|
||||
private void processEvent(EventContext eventContext) {
|
||||
try {
|
||||
String eventData = eventContext.getEventData().getBodyAsString();
|
||||
log.debug("이벤트 수신: {}", eventData);
|
||||
|
||||
// JSON 역직렬화
|
||||
TranscriptSegmentReadyEvent event = objectMapper.readValue(
|
||||
eventData,
|
||||
TranscriptSegmentReadyEvent.class
|
||||
);
|
||||
|
||||
log.info("실시간 텍스트 수신 - meetingId: {}, text: {}",
|
||||
event.getMeetingId(), event.getText());
|
||||
|
||||
// SuggestionService로 전달하여 AI 분석 트리거
|
||||
suggestionService.processRealtimeTranscript(
|
||||
event.getMeetingId(),
|
||||
event.getText(),
|
||||
event.getTimestamp()
|
||||
);
|
||||
|
||||
// 체크포인트 업데이트
|
||||
eventContext.updateCheckpoint();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("이벤트 처리 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 처리 핸들러
|
||||
*/
|
||||
private void processError(ErrorContext errorContext) {
|
||||
log.error("Event Hub 에러 - partition: {}, error: {}",
|
||||
errorContext.getPartitionContext().getPartitionId(),
|
||||
errorContext.getThrowable().getMessage(),
|
||||
errorContext.getThrowable());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.unicorn.hgzero.ai.infra.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Spring Security 설정
|
||||
* CORS 설정 및 API 보안 설정 (인증 없음)
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Value("${cors.allowed-origins:http://localhost:3000,http://localhost:8080,http://localhost:8081,http://localhost:8082,http://localhost:8083,http://localhost:8084}")
|
||||
private String allowedOrigins;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
return http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// 모든 요청 허용 (인증 없음)
|
||||
.anyRequest().permitAll()
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
|
||||
// 환경변수에서 허용할 Origin 패턴 설정
|
||||
String[] origins = allowedOrigins.split(",");
|
||||
configuration.setAllowedOriginPatterns(Arrays.asList(origins));
|
||||
|
||||
// 허용할 HTTP 메소드
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
|
||||
|
||||
// 허용할 헤더
|
||||
configuration.setAllowedHeaders(Arrays.asList(
|
||||
"Authorization", "Content-Type", "X-Requested-With", "Accept",
|
||||
"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers",
|
||||
"X-User-Id", "X-User-Name"
|
||||
));
|
||||
|
||||
// 자격 증명 허용
|
||||
configuration.setAllowCredentials(true);
|
||||
|
||||
// Pre-flight 요청 캐시 시간
|
||||
configuration.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.unicorn.hgzero.ai.infra.config;
|
||||
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Swagger/OpenAPI 설정
|
||||
* AI Service API 문서화를 위한 설정
|
||||
*/
|
||||
@Configuration
|
||||
public class SwaggerConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI openAPI() {
|
||||
return new OpenAPI()
|
||||
.info(apiInfo())
|
||||
.addServersItem(new Server()
|
||||
.url("http://localhost:8083")
|
||||
.description("Local Development"))
|
||||
.addServersItem(new Server()
|
||||
.url("{protocol}://{host}:{port}")
|
||||
.description("Custom Server")
|
||||
.variables(new io.swagger.v3.oas.models.servers.ServerVariables()
|
||||
.addServerVariable("protocol", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||
._default("http")
|
||||
.description("Protocol (http or https)")
|
||||
.addEnumItem("http")
|
||||
.addEnumItem("https"))
|
||||
.addServerVariable("host", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||
._default("localhost")
|
||||
.description("Server host"))
|
||||
.addServerVariable("port", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||
._default("8083")
|
||||
.description("Server port"))))
|
||||
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
|
||||
.components(new Components()
|
||||
.addSecuritySchemes("Bearer Authentication", createAPIKeyScheme()));
|
||||
}
|
||||
|
||||
private Info apiInfo() {
|
||||
return new Info()
|
||||
.title("AI Service API")
|
||||
.description("AI 기반 회의록 자동 작성 및 분석 서비스 API")
|
||||
.version("1.0.0")
|
||||
.contact(new Contact()
|
||||
.name("HGZero Development Team")
|
||||
.email("dev@hgzero.com"));
|
||||
}
|
||||
|
||||
private SecurityScheme createAPIKeyScheme() {
|
||||
return new SecurityScheme()
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.bearerFormat("JWT")
|
||||
.scheme("bearer");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.unicorn.hgzero.ai.infra.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* WebClient 설정
|
||||
* 외부 API 호출을 위한 WebClient 빈 생성
|
||||
*/
|
||||
@Configuration
|
||||
public class WebClientConfig {
|
||||
|
||||
@Bean
|
||||
public WebClient webClient() {
|
||||
return WebClient.builder()
|
||||
.codecs(configurer -> configurer
|
||||
.defaultCodecs()
|
||||
.maxInMemorySize(10 * 1024 * 1024)) // 10MB
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package com.unicorn.hgzero.ai.infra.controller;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.usecase.TermExplanationUseCase;
|
||||
import com.unicorn.hgzero.ai.infra.dto.response.TermExplanationResponse;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.*;
|
||||
import com.unicorn.hgzero.common.dto.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 전문용어 설명 Controller
|
||||
* GET /api/terms/{term}/explain
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/ai/terms")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Tag(name = "Term", description = "전문용어 감지 및 설명 API")
|
||||
public class ExplanationController {
|
||||
|
||||
private final TermExplanationUseCase termExplanationUseCase;
|
||||
|
||||
@GetMapping("/{term}/explain")
|
||||
@Operation(summary = "맥락 기반 용어 설명", description = "전문용어에 대한 맥락 기반 설명을 생성합니다")
|
||||
public ResponseEntity<ApiResponse<TermExplanationResponse>> explainTerm(
|
||||
@PathVariable String term,
|
||||
@RequestParam String meetingId,
|
||||
@RequestParam(required = false) String context) {
|
||||
|
||||
log.info("용어 설명 요청 - term: {}, meetingId: {}", term, meetingId);
|
||||
|
||||
TermExplanationUseCase.TermExplanationResult result = termExplanationUseCase.explainTerm(
|
||||
term,
|
||||
meetingId,
|
||||
context
|
||||
);
|
||||
|
||||
TermExplanationResponse response = TermExplanationResponse.builder()
|
||||
.term(result.term())
|
||||
.basicDefinition(result.basicDefinition())
|
||||
.contextualMeaning(result.contextualMeaning())
|
||||
.useCases(result.useCases())
|
||||
.relatedProjects(result.relatedProjects().stream()
|
||||
.map(p -> RelatedProjectDto.builder()
|
||||
.name(p.name())
|
||||
.relevance(p.relevance())
|
||||
.build())
|
||||
.collect(Collectors.toList()))
|
||||
.pastDiscussions(result.pastDiscussions().stream()
|
||||
.map(d -> PastDiscussionDto.builder()
|
||||
.date(d.date())
|
||||
.participants(d.participants())
|
||||
.summary(d.summary())
|
||||
.link(d.link())
|
||||
.build())
|
||||
.collect(Collectors.toList()))
|
||||
.references(result.references().stream()
|
||||
.map(r -> ReferenceDto.builder()
|
||||
.title(r.title())
|
||||
.type(r.type())
|
||||
.link(r.link())
|
||||
.build())
|
||||
.collect(Collectors.toList()))
|
||||
.build();
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.unicorn.hgzero.ai.infra.controller;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.domain.RelatedMinutes;
|
||||
import com.unicorn.hgzero.ai.biz.usecase.RelatedTranscriptSearchUseCase;
|
||||
import com.unicorn.hgzero.ai.infra.dto.response.RelatedTranscriptsResponse;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.RelatedTranscriptDto;
|
||||
import com.unicorn.hgzero.common.dto.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 관련 회의록 조회 Controller
|
||||
* GET /api/transcripts/{meetingId}/related
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/ai/transcripts")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Tag(name = "Relation", description = "관련 회의록 조회 API")
|
||||
public class RelationController {
|
||||
|
||||
private final RelatedTranscriptSearchUseCase relatedTranscriptSearchUseCase;
|
||||
|
||||
@GetMapping("/{meetingId}/related")
|
||||
@Operation(summary = "관련 회의록 조회", description = "벡터 유사도 검색을 통해 관련된 회의록을 찾아 반환합니다")
|
||||
public ResponseEntity<ApiResponse<RelatedTranscriptsResponse>> findRelatedTranscripts(
|
||||
@PathVariable String meetingId,
|
||||
@RequestParam String transcriptId,
|
||||
@RequestParam(defaultValue = "5") int limit) {
|
||||
|
||||
log.info("관련 회의록 조회 요청 - meetingId: {}, transcriptId: {}, limit: {}", meetingId, transcriptId, limit);
|
||||
|
||||
List<RelatedMinutes> relatedMinutes = relatedTranscriptSearchUseCase.findRelatedTranscripts(
|
||||
meetingId,
|
||||
transcriptId,
|
||||
limit
|
||||
);
|
||||
|
||||
RelatedTranscriptsResponse response = RelatedTranscriptsResponse.builder()
|
||||
.relatedTranscripts(relatedMinutes.stream()
|
||||
.map(r -> RelatedTranscriptDto.builder()
|
||||
.transcriptId(r.getTranscriptId())
|
||||
.title(r.getTitle())
|
||||
.date(r.getDate())
|
||||
.participants(r.getParticipants())
|
||||
.relevanceScore(r.getRelevanceScore())
|
||||
.commonKeywords(r.getCommonKeywords())
|
||||
.summary(r.getSummary())
|
||||
.link(r.getLink())
|
||||
.build())
|
||||
.collect(Collectors.toList()))
|
||||
.totalCount(relatedMinutes.size())
|
||||
.build();
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.unicorn.hgzero.ai.infra.controller;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.usecase.SectionSummaryUseCase;
|
||||
import com.unicorn.hgzero.ai.infra.dto.request.SectionSummaryRequest;
|
||||
import com.unicorn.hgzero.ai.infra.dto.response.SectionSummaryResponse;
|
||||
import com.unicorn.hgzero.common.dto.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 섹션 AI 요약 재생성 Controller
|
||||
* POST /api/sections/{sectionId}/regenerate-summary
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/ai/sections")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Tag(name = "Section", description = "섹션 AI 요약 재생성 API")
|
||||
public class SectionController {
|
||||
|
||||
private final SectionSummaryUseCase sectionSummaryUseCase;
|
||||
|
||||
@PostMapping("/{sectionId}/regenerate-summary")
|
||||
@Operation(summary = "섹션 AI 요약 재생성", description = "사용자가 작성한 섹션 내용을 기반으로 AI 요약을 재생성합니다")
|
||||
public ResponseEntity<ApiResponse<SectionSummaryResponse>> regenerateSummary(
|
||||
@PathVariable String sectionId,
|
||||
@Valid @RequestBody SectionSummaryRequest request) {
|
||||
|
||||
log.info("섹션 요약 재생성 요청 - sectionId: {}, meetingId: {}", sectionId, request.getMeetingId());
|
||||
|
||||
String summary = sectionSummaryUseCase.regenerateSummary(
|
||||
sectionId,
|
||||
request.getSectionContent(),
|
||||
request.getMeetingId()
|
||||
);
|
||||
|
||||
SectionSummaryResponse response = SectionSummaryResponse.builder()
|
||||
.summary(summary)
|
||||
.generatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package com.unicorn.hgzero.ai.infra.controller;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.domain.Suggestion;
|
||||
import com.unicorn.hgzero.ai.biz.usecase.SuggestionUseCase;
|
||||
import com.unicorn.hgzero.ai.infra.dto.request.DiscussionSuggestionRequest;
|
||||
import com.unicorn.hgzero.ai.infra.dto.request.DecisionSuggestionRequest;
|
||||
import com.unicorn.hgzero.ai.infra.dto.response.DiscussionSuggestionResponse;
|
||||
import com.unicorn.hgzero.ai.infra.dto.response.DecisionSuggestionResponse;
|
||||
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;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 논의사항/결정사항 제안 Controller
|
||||
* POST /api/suggestions/discussion
|
||||
* POST /api/suggestions/decision
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/ai/suggestions")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Tag(name = "Suggestion", description = "논의사항/결정사항 제안 API")
|
||||
public class SuggestionController {
|
||||
|
||||
private final SuggestionUseCase suggestionUseCase;
|
||||
|
||||
@PostMapping("/discussion")
|
||||
@Operation(summary = "논의사항 제안", description = "현재 회의 진행 상황을 분석하여 추가로 논의하면 좋을 주제를 제안합니다")
|
||||
public ResponseEntity<ApiResponse<DiscussionSuggestionResponse>> suggestDiscussion(
|
||||
@Valid @RequestBody DiscussionSuggestionRequest request) {
|
||||
|
||||
log.info("논의사항 제안 요청 - meetingId: {}", request.getMeetingId());
|
||||
|
||||
List<Suggestion> suggestions = suggestionUseCase.suggestDiscussions(
|
||||
request.getMeetingId(),
|
||||
request.getTranscriptText()
|
||||
);
|
||||
|
||||
DiscussionSuggestionResponse response = DiscussionSuggestionResponse.builder()
|
||||
.suggestions(suggestions.stream()
|
||||
.map(s -> DiscussionSuggestionDto.builder()
|
||||
.id(s.getId())
|
||||
.topic(s.getContent())
|
||||
.reason(s.getReason())
|
||||
.priority(s.getPriority())
|
||||
.relatedAgenda(s.getRelatedAgenda())
|
||||
.estimatedTime(s.getEstimatedTime())
|
||||
.build())
|
||||
.collect(Collectors.toList()))
|
||||
.totalCount(suggestions.size())
|
||||
.timestamp(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
@PostMapping("/decision")
|
||||
@Operation(summary = "결정사항 제안", description = "회의록 텍스트에서 결정사항 패턴을 감지하여 제안합니다")
|
||||
public ResponseEntity<ApiResponse<DecisionSuggestionResponse>> suggestDecision(
|
||||
@Valid @RequestBody DecisionSuggestionRequest request) {
|
||||
|
||||
log.info("결정사항 제안 요청 - meetingId: {}", request.getMeetingId());
|
||||
|
||||
List<Suggestion> suggestions = suggestionUseCase.suggestDecisions(
|
||||
request.getMeetingId(),
|
||||
request.getTranscriptText()
|
||||
);
|
||||
|
||||
DecisionSuggestionResponse response = DecisionSuggestionResponse.builder()
|
||||
.suggestions(suggestions.stream()
|
||||
.map(s -> DecisionSuggestionDto.builder()
|
||||
.id(s.getId())
|
||||
.content(s.getContent())
|
||||
.category(s.getCategory())
|
||||
.decisionMaker("") // TODO: Extract from suggestion
|
||||
.participants(s.getParticipants())
|
||||
.confidence(s.getConfidence())
|
||||
.extractedFrom(s.getExtractedFrom())
|
||||
.context(s.getContext())
|
||||
.build())
|
||||
.collect(Collectors.toList()))
|
||||
.totalCount(suggestions.size())
|
||||
.timestamp(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.unicorn.hgzero.ai.infra.controller;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.domain.Term;
|
||||
import com.unicorn.hgzero.ai.biz.usecase.TermDetectionUseCase;
|
||||
import com.unicorn.hgzero.ai.infra.dto.request.TermDetectionRequest;
|
||||
import com.unicorn.hgzero.ai.infra.dto.response.TermDetectionResponse;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.DetectedTermDto;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.HighlightInfoDto;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.TextPositionDto;
|
||||
import com.unicorn.hgzero.common.dto.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 전문용어 감지 Controller
|
||||
* POST /api/terms/detect
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/ai/terms")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Tag(name = "Term", description = "전문용어 감지 및 설명 API")
|
||||
public class TermController {
|
||||
|
||||
private final TermDetectionUseCase termDetectionUseCase;
|
||||
|
||||
@PostMapping("/detect")
|
||||
@Operation(summary = "전문용어 감지", description = "회의록 텍스트에서 전문용어를 자동으로 감지합니다")
|
||||
public ResponseEntity<ApiResponse<TermDetectionResponse>> detectTerms(
|
||||
@Valid @RequestBody TermDetectionRequest request) {
|
||||
|
||||
log.info("전문용어 감지 요청 - meetingId: {}, organizationId: {}",
|
||||
request.getMeetingId(), request.getOrganizationId());
|
||||
|
||||
List<Term> terms = termDetectionUseCase.detectTerms(
|
||||
request.getMeetingId(),
|
||||
request.getText(),
|
||||
request.getOrganizationId()
|
||||
);
|
||||
|
||||
List<DetectedTermDto> detectedTerms = terms.stream()
|
||||
.map(t -> DetectedTermDto.builder()
|
||||
.term(t.getTerm())
|
||||
.position(t.getPosition() != null ? TextPositionDto.builder()
|
||||
.line(t.getPosition().getLine())
|
||||
.offset(t.getPosition().getOffset())
|
||||
.build() : null)
|
||||
.confidence(t.getConfidence())
|
||||
.category(t.getCategory())
|
||||
.definition(t.getDefinition())
|
||||
.context(t.getContext())
|
||||
.relatedMeetingId(t.getRelatedMeetingId())
|
||||
.highlight(t.getHighlight())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<HighlightInfoDto> highlightInfo = detectedTerms.stream()
|
||||
.filter(t -> Boolean.TRUE.equals(t.getHighlight()))
|
||||
.map(t -> HighlightInfoDto.builder()
|
||||
.term(t.getTerm())
|
||||
.position(t.getPosition())
|
||||
.style("background-color: yellow")
|
||||
.tooltip("용어 설명 로딩 중...")
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
TermDetectionResponse response = TermDetectionResponse.builder()
|
||||
.detectedTerms(detectedTerms)
|
||||
.totalCount(detectedTerms.size())
|
||||
.highlightInfo(highlightInfo)
|
||||
.build();
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.unicorn.hgzero.ai.infra.controller;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.domain.ExtractedTodo;
|
||||
import com.unicorn.hgzero.ai.biz.usecase.TodoExtractionUseCase;
|
||||
import com.unicorn.hgzero.ai.infra.dto.request.TodoExtractionRequest;
|
||||
import com.unicorn.hgzero.ai.infra.dto.response.TodoExtractionResponse;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.ExtractedTodoDto;
|
||||
import com.unicorn.hgzero.common.dto.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Todo 자동 추출 Controller
|
||||
* POST /api/todos/extract
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/ai/todos")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Tag(name = "Todo", description = "Todo 자동 추출 API")
|
||||
public class TodoController {
|
||||
|
||||
private final TodoExtractionUseCase todoExtractionUseCase;
|
||||
|
||||
@PostMapping("/extract")
|
||||
@Operation(summary = "Todo 자동 추출", description = "회의록에서 액션 아이템을 자동으로 추출하고 담당자를 식별합니다")
|
||||
public ResponseEntity<ApiResponse<TodoExtractionResponse>> extractTodos(
|
||||
@RequestHeader("X-User-Id") String userId,
|
||||
@Valid @RequestBody TodoExtractionRequest request) {
|
||||
|
||||
log.info("Todo 추출 요청 - meetingId: {}, userId: {}", request.getMeetingId(), userId);
|
||||
|
||||
List<ExtractedTodo> todos = todoExtractionUseCase.extractTodos(
|
||||
request.getMeetingId(),
|
||||
request.getMinutesContent(),
|
||||
request.getUserId() != null ? request.getUserId() : userId
|
||||
);
|
||||
|
||||
TodoExtractionResponse response = TodoExtractionResponse.builder()
|
||||
.meetingId(request.getMeetingId())
|
||||
.todos(todos.stream()
|
||||
.map(t -> ExtractedTodoDto.builder()
|
||||
.content(t.getContent())
|
||||
.assignee(t.getAssignee())
|
||||
.dueDate(t.getDueDate())
|
||||
.priority(t.getPriority())
|
||||
.sectionReference(t.getSectionReference())
|
||||
.build())
|
||||
.collect(Collectors.toList()))
|
||||
.totalCount(todos.size())
|
||||
.extractedAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package com.unicorn.hgzero.ai.infra.controller;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.domain.ProcessedTranscript;
|
||||
import com.unicorn.hgzero.ai.biz.usecase.TranscriptProcessUseCase;
|
||||
import com.unicorn.hgzero.ai.infra.dto.request.TranscriptProcessRequest;
|
||||
import com.unicorn.hgzero.ai.infra.dto.response.TranscriptProcessResponse;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.*;
|
||||
import com.unicorn.hgzero.common.dto.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 회의록 자동 작성 Controller
|
||||
* POST /api/transcripts/process
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/ai/transcripts")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Tag(name = "Transcript", description = "회의록 자동 작성 API")
|
||||
public class TranscriptController {
|
||||
|
||||
private final TranscriptProcessUseCase transcriptProcessUseCase;
|
||||
|
||||
@PostMapping("/process")
|
||||
@Operation(summary = "회의록 자동 작성", description = "STT에서 변환된 텍스트를 받아 LLM 기반으로 회의록을 자동 작성합니다")
|
||||
public ResponseEntity<ApiResponse<TranscriptProcessResponse>> processTranscript(
|
||||
@RequestHeader("X-User-Id") String userId,
|
||||
@RequestHeader("X-User-Name") String userName,
|
||||
@Valid @RequestBody TranscriptProcessRequest request) {
|
||||
|
||||
log.info("회의록 자동 작성 요청 - meetingId: {}, userId: {}", request.getMeetingId(), userId);
|
||||
|
||||
ProcessedTranscript result = transcriptProcessUseCase.processTranscript(
|
||||
request.getMeetingId(),
|
||||
request.getTranscriptText(),
|
||||
request.getUserId() != null ? request.getUserId() : userId,
|
||||
request.getUserName() != null ? request.getUserName() : userName,
|
||||
request.getContext() != null ? request.getContext().getTitle() : "",
|
||||
request.getContext() != null ? request.getContext().getParticipants() : null,
|
||||
request.getContext() != null ? request.getContext().getAgenda() : null
|
||||
);
|
||||
|
||||
TranscriptProcessResponse response = mapToResponse(result);
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
private TranscriptProcessResponse mapToResponse(ProcessedTranscript domain) {
|
||||
return TranscriptProcessResponse.builder()
|
||||
.transcriptId(domain.getTranscriptId())
|
||||
.meetingId(domain.getMeetingId())
|
||||
.content(mapContent(domain))
|
||||
.suggestions(null) // TODO: 실시간 제안 기능 구현 시 추가
|
||||
.createdAt(domain.getCreatedAt())
|
||||
.status(domain.getStatus())
|
||||
.build();
|
||||
}
|
||||
|
||||
private TranscriptContentDto mapContent(ProcessedTranscript domain) {
|
||||
return TranscriptContentDto.builder()
|
||||
.summary(domain.getSummary())
|
||||
.discussions(domain.getDiscussions().stream()
|
||||
.map(d -> DiscussionItemDto.builder()
|
||||
.topic(d.getTopic())
|
||||
.speaker(d.getSpeaker())
|
||||
.content(d.getContent())
|
||||
.build())
|
||||
.collect(Collectors.toList()))
|
||||
.decisions(domain.getDecisions().stream()
|
||||
.map(d -> DecisionItemDto.builder()
|
||||
.content(d.getContent())
|
||||
.decisionMaker(d.getDecisionMaker())
|
||||
.category(d.getCategory())
|
||||
.build())
|
||||
.collect(Collectors.toList()))
|
||||
.pendingItems(domain.getPendingItems())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 결정사항 아이템 DTO
|
||||
* 결정 내용, 결정자, 카테고리 포함
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DecisionItemDto {
|
||||
|
||||
/**
|
||||
* 결정 내용
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 결정자
|
||||
*/
|
||||
private String decisionMaker;
|
||||
|
||||
/**
|
||||
* 결정 카테고리 (기술, 일정, 리소스, 정책, 기타)
|
||||
*/
|
||||
private String category;
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 결정사항 제안 DTO
|
||||
* AI가 감지한 결정사항 패턴 정보
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DecisionSuggestionDto {
|
||||
|
||||
/**
|
||||
* 제안 ID
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 결정 내용
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 결정 카테고리 (기술, 일정, 리소스, 정책, 기타)
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 결정자
|
||||
*/
|
||||
private String decisionMaker;
|
||||
|
||||
/**
|
||||
* 참여자 목록
|
||||
*/
|
||||
private List<String> participants;
|
||||
|
||||
/**
|
||||
* 신뢰도 점수 (0-1)
|
||||
*/
|
||||
private Double confidence;
|
||||
|
||||
/**
|
||||
* 원문 발췌
|
||||
*/
|
||||
private String extractedFrom;
|
||||
|
||||
/**
|
||||
* 결정 배경
|
||||
*/
|
||||
private String context;
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 감지된 전문용어 DTO
|
||||
* 회의록에서 감지된 전문용어 정보
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DetectedTermDto {
|
||||
|
||||
/**
|
||||
* 용어명
|
||||
*/
|
||||
private String term;
|
||||
|
||||
/**
|
||||
* 텍스트 위치 정보
|
||||
*/
|
||||
private TextPositionDto position;
|
||||
|
||||
/**
|
||||
* 신뢰도 점수 (0-1)
|
||||
*/
|
||||
private Double confidence;
|
||||
|
||||
/**
|
||||
* 용어 카테고리 (기술, 업무, 도메인, 기획, 비즈니스, 전략, 마케팅)
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 용어 정의 (간단한 설명)
|
||||
*/
|
||||
private String definition;
|
||||
|
||||
/**
|
||||
* 용어가 사용된 맥락 (과거 회의록 참조)
|
||||
* 예: "신제품 기획 회의(2024-09-15)에서 언급"
|
||||
*/
|
||||
private String context;
|
||||
|
||||
/**
|
||||
* 관련 회의 ID (용어가 논의된 과거 회의)
|
||||
*/
|
||||
private String relatedMeetingId;
|
||||
|
||||
/**
|
||||
* 하이라이트 여부
|
||||
*/
|
||||
private Boolean highlight;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 논의사항 아이템 DTO
|
||||
* 논의 주제, 발언자, 논의 내용 포함
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DiscussionItemDto {
|
||||
|
||||
/**
|
||||
* 논의 주제
|
||||
*/
|
||||
private String topic;
|
||||
|
||||
/**
|
||||
* 발언자
|
||||
*/
|
||||
private String speaker;
|
||||
|
||||
/**
|
||||
* 논의 내용
|
||||
*/
|
||||
private String content;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 논의사항 제안 DTO
|
||||
* AI가 추천하는 논의 주제 정보
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DiscussionSuggestionDto {
|
||||
|
||||
/**
|
||||
* 제안 ID
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 논의 주제
|
||||
*/
|
||||
private String topic;
|
||||
|
||||
/**
|
||||
* 제안 이유
|
||||
*/
|
||||
private String reason;
|
||||
|
||||
/**
|
||||
* 우선순위 (HIGH, MEDIUM, LOW)
|
||||
*/
|
||||
private String priority;
|
||||
|
||||
/**
|
||||
* 관련 안건
|
||||
*/
|
||||
private String relatedAgenda;
|
||||
|
||||
/**
|
||||
* 예상 소요 시간 (분)
|
||||
*/
|
||||
private Integer estimatedTime;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 에러 응답 DTO
|
||||
* API 에러 발생 시 통일된 형식으로 에러 정보 반환
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ErrorResponseDto {
|
||||
|
||||
/**
|
||||
* 에러 코드
|
||||
*/
|
||||
private String error;
|
||||
|
||||
/**
|
||||
* 에러 메시지
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 에러 발생 시각
|
||||
*/
|
||||
private LocalDateTime timestamp;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 추출된 Todo DTO
|
||||
* AI가 회의록에서 추출한 Todo 정보
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ExtractedTodoDto {
|
||||
|
||||
/**
|
||||
* Todo 내용
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 담당자
|
||||
*/
|
||||
private String assignee;
|
||||
|
||||
/**
|
||||
* 마감일
|
||||
*/
|
||||
private LocalDate dueDate;
|
||||
|
||||
/**
|
||||
* 우선순위 (HIGH, MEDIUM, LOW)
|
||||
*/
|
||||
private String priority;
|
||||
|
||||
/**
|
||||
* 관련 회의록 섹션
|
||||
*/
|
||||
private String sectionReference;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 하이라이트 정보 DTO
|
||||
* 용어 하이라이트 스타일과 툴팁 정보
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class HighlightInfoDto {
|
||||
|
||||
/**
|
||||
* 용어명
|
||||
*/
|
||||
private String term;
|
||||
|
||||
/**
|
||||
* 텍스트 위치 정보
|
||||
*/
|
||||
private TextPositionDto position;
|
||||
|
||||
/**
|
||||
* 하이라이트 스타일
|
||||
*/
|
||||
private String style;
|
||||
|
||||
/**
|
||||
* 툴팁 텍스트
|
||||
*/
|
||||
private String tooltip;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의 맥락 정보 DTO
|
||||
* 회의 제목, 참석자, 안건, 이전 회의록 등의 맥락 정보를 전달
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MeetingContextDto {
|
||||
|
||||
/**
|
||||
* 회의 제목
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 참석자 목록
|
||||
*/
|
||||
private List<String> participants;
|
||||
|
||||
/**
|
||||
* 회의 안건 목록
|
||||
*/
|
||||
private List<String> agenda;
|
||||
|
||||
/**
|
||||
* 이전 회의록 내용
|
||||
*/
|
||||
private String previousContent;
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 과거 논의 DTO
|
||||
* 전문용어 관련 과거 논의 정보
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PastDiscussionDto {
|
||||
|
||||
/**
|
||||
* 논의 날짜
|
||||
*/
|
||||
private LocalDate date;
|
||||
|
||||
/**
|
||||
* 참석자 목록
|
||||
*/
|
||||
private List<String> participants;
|
||||
|
||||
/**
|
||||
* 논의 요약
|
||||
*/
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 회의록 링크
|
||||
*/
|
||||
private String link;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 실시간 추천사항 DTO (간소화 버전)
|
||||
* 논의사항과 결정사항을 구분하지 않고 통합 제공
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RealtimeSuggestionsDto {
|
||||
|
||||
/**
|
||||
* AI 제안사항 목록 (논의사항 + 결정사항 통합)
|
||||
*/
|
||||
private List<SimpleSuggestionDto> suggestions;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 참조 문서 DTO
|
||||
* 전문용어 관련 참조 문서 정보
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ReferenceDto {
|
||||
|
||||
/**
|
||||
* 문서 제목
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 문서 유형 (위키, 매뉴얼, 회의록, 보고서)
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 문서 URL
|
||||
*/
|
||||
private String link;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 관련 프로젝트 DTO
|
||||
* 전문용어와 관련된 프로젝트 정보
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RelatedProjectDto {
|
||||
|
||||
/**
|
||||
* 프로젝트명
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 연관성 설명
|
||||
*/
|
||||
private String relevance;
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 관련 회의록 DTO
|
||||
* RAG 검색으로 찾은 관련 회의록 정보
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RelatedTranscriptDto {
|
||||
|
||||
/**
|
||||
* 회의록 ID
|
||||
*/
|
||||
private String transcriptId;
|
||||
|
||||
/**
|
||||
* 회의 제목
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 회의 날짜
|
||||
*/
|
||||
private LocalDate date;
|
||||
|
||||
/**
|
||||
* 참석자 목록
|
||||
*/
|
||||
private List<String> participants;
|
||||
|
||||
/**
|
||||
* 관련도 점수 (0-100%)
|
||||
*/
|
||||
private Double relevanceScore;
|
||||
|
||||
/**
|
||||
* 공통 키워드 목록
|
||||
*/
|
||||
private List<String> commonKeywords;
|
||||
|
||||
/**
|
||||
* 회의록 핵심 내용 요약 (1-2문장)
|
||||
*/
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 회의록 링크
|
||||
*/
|
||||
private String link;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 간소화된 AI 제안사항 DTO
|
||||
* 논의사항과 결정사항을 구분하지 않고 통합 제공
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SimpleSuggestionDto {
|
||||
|
||||
/**
|
||||
* 제안 ID
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 제안 내용 (논의사항 또는 결정사항)
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 타임스탬프 (초 단위, 예: 00:05:23)
|
||||
*/
|
||||
private String timestamp;
|
||||
|
||||
/**
|
||||
* 신뢰도 점수 (0-1)
|
||||
*/
|
||||
private Double confidence;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 텍스트 위치 정보 DTO
|
||||
* 줄 번호와 오프셋 정보를 포함
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TextPositionDto {
|
||||
|
||||
/**
|
||||
* 줄 번호
|
||||
*/
|
||||
private Integer line;
|
||||
|
||||
/**
|
||||
* 시작 오프셋
|
||||
*/
|
||||
private Integer offset;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의록 내용 DTO
|
||||
* 전체 요약, 논의사항, 결정사항, 보류사항 포함
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TranscriptContentDto {
|
||||
|
||||
/**
|
||||
* 전체 요약
|
||||
*/
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 논의사항 목록
|
||||
*/
|
||||
private List<DiscussionItemDto> discussions;
|
||||
|
||||
/**
|
||||
* 결정사항 목록
|
||||
*/
|
||||
private List<DecisionItemDto> decisions;
|
||||
|
||||
/**
|
||||
* 보류사항 목록
|
||||
*/
|
||||
private List<String> pendingItems;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.request;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 결정사항 제안 요청 DTO
|
||||
* 회의록 텍스트에서 결정사항 패턴을 감지하여 제안 요청
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DecisionSuggestionRequest {
|
||||
|
||||
/**
|
||||
* 회의 ID (필수)
|
||||
*/
|
||||
@NotBlank(message = "회의 ID는 필수입니다")
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* 현재까지의 회의록 텍스트 (필수)
|
||||
*/
|
||||
@NotBlank(message = "회의록 텍스트는 필수입니다")
|
||||
private String transcriptText;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.request;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 논의사항 제안 요청 DTO
|
||||
* 현재 회의 진행 상황을 분석하여 추가 논의 주제 제안 요청
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DiscussionSuggestionRequest {
|
||||
|
||||
/**
|
||||
* 회의 ID (필수)
|
||||
*/
|
||||
@NotBlank(message = "회의 ID는 필수입니다")
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* 현재까지의 회의록 텍스트 (필수)
|
||||
*/
|
||||
@NotBlank(message = "회의록 텍스트는 필수입니다")
|
||||
private String transcriptText;
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.request;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 섹션 AI 요약 재생성 요청 DTO
|
||||
* 사용자가 작성한 섹션 내용을 기반으로 AI 요약 재생성 요청
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SectionSummaryRequest {
|
||||
|
||||
/**
|
||||
* 사용자가 작성/수정한 섹션 내용 (필수, Markdown 형식)
|
||||
*/
|
||||
@NotBlank(message = "섹션 내용은 필수입니다")
|
||||
private String sectionContent;
|
||||
|
||||
/**
|
||||
* 회의 ID (맥락 이해용, 선택적)
|
||||
*/
|
||||
private String meetingId;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.request;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 전문용어 감지 요청 DTO
|
||||
* 회의록 텍스트에서 전문용어를 자동으로 감지 요청
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TermDetectionRequest {
|
||||
|
||||
/**
|
||||
* 회의 ID (필수)
|
||||
*/
|
||||
@NotBlank(message = "회의 ID는 필수입니다")
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* 분석할 회의록 텍스트 (필수)
|
||||
*/
|
||||
@NotBlank(message = "분석할 텍스트는 필수입니다")
|
||||
private String text;
|
||||
|
||||
/**
|
||||
* 조직 ID
|
||||
*/
|
||||
private String organizationId;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.request;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* Todo 자동 추출 요청 DTO
|
||||
* 회의록에서 액션 아이템을 자동으로 추출하고 담당자 식별 요청
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TodoExtractionRequest {
|
||||
|
||||
/**
|
||||
* 회의 ID (필수)
|
||||
*/
|
||||
@NotBlank(message = "회의 ID는 필수입니다")
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* 요청자 ID
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 회의록 전체 내용 (필수, Markdown 형식)
|
||||
*/
|
||||
@NotBlank(message = "회의록 내용은 필수입니다")
|
||||
private String minutesContent;
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.request;
|
||||
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.MeetingContextDto;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 회의록 자동 작성 요청 DTO
|
||||
* STT에서 변환된 텍스트를 받아 LLM 기반 회의록 자동 작성 요청
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TranscriptProcessRequest {
|
||||
|
||||
/**
|
||||
* 회의 ID (필수)
|
||||
*/
|
||||
@NotBlank(message = "회의 ID는 필수입니다")
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* STT에서 변환된 텍스트 (필수)
|
||||
*/
|
||||
@NotBlank(message = "회의록 텍스트는 필수입니다")
|
||||
private String transcriptText;
|
||||
|
||||
/**
|
||||
* 사용자 ID
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 사용자 이름
|
||||
*/
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 회의 맥락 정보
|
||||
*/
|
||||
private MeetingContextDto context;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.response;
|
||||
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.DecisionSuggestionDto;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 결정사항 제안 응답 DTO
|
||||
* AI가 감지한 결정사항 패턴 목록 반환
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DecisionSuggestionResponse {
|
||||
|
||||
/**
|
||||
* 결정사항 제안 목록
|
||||
*/
|
||||
private List<DecisionSuggestionDto> suggestions;
|
||||
|
||||
/**
|
||||
* 제안 개수
|
||||
*/
|
||||
private Integer totalCount;
|
||||
|
||||
/**
|
||||
* 생성 시각
|
||||
*/
|
||||
private LocalDateTime timestamp;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.response;
|
||||
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.DiscussionSuggestionDto;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 논의사항 제안 응답 DTO
|
||||
* AI가 제안하는 추가 논의 주제 목록 반환
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DiscussionSuggestionResponse {
|
||||
|
||||
/**
|
||||
* 논의사항 제안 목록
|
||||
*/
|
||||
private List<DiscussionSuggestionDto> suggestions;
|
||||
|
||||
/**
|
||||
* 제안 개수
|
||||
*/
|
||||
private Integer totalCount;
|
||||
|
||||
/**
|
||||
* 생성 시각
|
||||
*/
|
||||
private LocalDateTime timestamp;
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.response;
|
||||
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.RelatedTranscriptDto;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 관련 회의록 조회 응답 DTO
|
||||
* RAG 검색으로 찾은 관련 회의록 목록 반환
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RelatedTranscriptsResponse {
|
||||
|
||||
/**
|
||||
* 관련 회의록 목록
|
||||
*/
|
||||
private List<RelatedTranscriptDto> relatedTranscripts;
|
||||
|
||||
/**
|
||||
* 관련 회의록 개수
|
||||
*/
|
||||
private Integer totalCount;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 섹션 요약 응답 DTO
|
||||
* AI가 생성한 섹션 요약 반환
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SectionSummaryResponse {
|
||||
|
||||
/**
|
||||
* 생성된 AI 요약 (2-3문장)
|
||||
*/
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 생성 시간
|
||||
*/
|
||||
private LocalDateTime generatedAt;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.response;
|
||||
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.DetectedTermDto;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.HighlightInfoDto;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 전문용어 감지 응답 DTO
|
||||
* 감지된 전문용어 목록과 하이라이트 정보 반환
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TermDetectionResponse {
|
||||
|
||||
/**
|
||||
* 감지된 용어 목록
|
||||
*/
|
||||
private List<DetectedTermDto> detectedTerms;
|
||||
|
||||
/**
|
||||
* 감지된 용어 개수
|
||||
*/
|
||||
private Integer totalCount;
|
||||
|
||||
/**
|
||||
* 하이라이트 정보 목록
|
||||
*/
|
||||
private List<HighlightInfoDto> highlightInfo;
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.response;
|
||||
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.PastDiscussionDto;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.ReferenceDto;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.RelatedProjectDto;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 전문용어 설명 응답 DTO
|
||||
* RAG 기반 맥락적 용어 설명 반환
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TermExplanationResponse {
|
||||
|
||||
/**
|
||||
* 용어명
|
||||
*/
|
||||
private String term;
|
||||
|
||||
/**
|
||||
* 간단한 정의
|
||||
*/
|
||||
private String basicDefinition;
|
||||
|
||||
/**
|
||||
* 현재 회의 맥락에서의 의미
|
||||
*/
|
||||
private String contextualMeaning;
|
||||
|
||||
/**
|
||||
* 실제 사용 사례 목록
|
||||
*/
|
||||
private List<String> useCases;
|
||||
|
||||
/**
|
||||
* 관련 프로젝트 목록
|
||||
*/
|
||||
private List<RelatedProjectDto> relatedProjects;
|
||||
|
||||
/**
|
||||
* 과거 논의 목록
|
||||
*/
|
||||
private List<PastDiscussionDto> pastDiscussions;
|
||||
|
||||
/**
|
||||
* 참조 문서 목록
|
||||
*/
|
||||
private List<ReferenceDto> references;
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.response;
|
||||
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.ExtractedTodoDto;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Todo 추출 응답 DTO
|
||||
* AI가 추출한 Todo 목록 반환
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TodoExtractionResponse {
|
||||
|
||||
/**
|
||||
* 회의 ID
|
||||
*/
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* 추출된 Todo 목록
|
||||
*/
|
||||
private List<ExtractedTodoDto> todos;
|
||||
|
||||
/**
|
||||
* 추출된 Todo 개수
|
||||
*/
|
||||
private Integer totalCount;
|
||||
|
||||
/**
|
||||
* 추출 시간
|
||||
*/
|
||||
private LocalDateTime extractedAt;
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.unicorn.hgzero.ai.infra.dto.response;
|
||||
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.RealtimeSuggestionsDto;
|
||||
import com.unicorn.hgzero.ai.infra.dto.common.TranscriptContentDto;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 회의록 자동 작성 응답 DTO
|
||||
* LLM 기반으로 생성된 회의록 정보 반환
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TranscriptProcessResponse {
|
||||
|
||||
/**
|
||||
* 생성된 회의록 ID
|
||||
*/
|
||||
private String transcriptId;
|
||||
|
||||
/**
|
||||
* 회의 ID
|
||||
*/
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* 회의록 내용
|
||||
*/
|
||||
private TranscriptContentDto content;
|
||||
|
||||
/**
|
||||
* 실시간 추천사항
|
||||
*/
|
||||
private RealtimeSuggestionsDto suggestions;
|
||||
|
||||
/**
|
||||
* 생성 시간
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 회의록 상태 (DRAFT, COMPLETED)
|
||||
*/
|
||||
private String status;
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.unicorn.hgzero.ai.infra.gateway;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.domain.ProcessedTranscript;
|
||||
import com.unicorn.hgzero.ai.biz.gateway.TranscriptGateway;
|
||||
import com.unicorn.hgzero.ai.infra.gateway.entity.ProcessedTranscriptEntity;
|
||||
import com.unicorn.hgzero.ai.infra.gateway.repository.ProcessedTranscriptJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 회의록 Gateway 구현체
|
||||
* JPA Repository를 사용한 회의록 영속성 관리
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class TranscriptGatewayImpl implements TranscriptGateway {
|
||||
|
||||
private final ProcessedTranscriptJpaRepository repository;
|
||||
|
||||
@Override
|
||||
public ProcessedTranscript save(ProcessedTranscript transcript) {
|
||||
log.debug("Saving transcript: transcriptId={}, meetingId={}",
|
||||
transcript.getTranscriptId(), transcript.getMeetingId());
|
||||
|
||||
ProcessedTranscriptEntity entity = ProcessedTranscriptEntity.fromDomain(transcript);
|
||||
ProcessedTranscriptEntity saved = repository.save(entity);
|
||||
|
||||
log.info("Transcript saved successfully: transcriptId={}", saved.getTranscriptId());
|
||||
return saved.toDomain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ProcessedTranscript> findById(String transcriptId) {
|
||||
log.debug("Finding transcript by id: {}", transcriptId);
|
||||
return repository.findById(transcriptId)
|
||||
.map(ProcessedTranscriptEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ProcessedTranscript> findByMeetingId(String meetingId) {
|
||||
log.debug("Finding transcript by meetingId: {}", meetingId);
|
||||
return repository.findByMeetingId(meetingId)
|
||||
.map(ProcessedTranscriptEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProcessedTranscript> findByMeetingIds(List<String> meetingIds) {
|
||||
log.debug("Finding transcripts by meetingIds: count={}", meetingIds.size());
|
||||
return repository.findByMeetingIdIn(meetingIds).stream()
|
||||
.map(ProcessedTranscriptEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProcessedTranscript> findByStatus(String status) {
|
||||
log.debug("Finding transcripts by status: {}", status);
|
||||
return repository.findByStatus(status).stream()
|
||||
.map(ProcessedTranscriptEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsByMeetingId(String meetingId) {
|
||||
log.debug("Checking transcript existence by meetingId: {}", meetingId);
|
||||
return repository.existsByMeetingId(meetingId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String transcriptId) {
|
||||
log.debug("Deleting transcript: {}", transcriptId);
|
||||
repository.deleteById(transcriptId);
|
||||
log.info("Transcript deleted successfully: {}", transcriptId);
|
||||
}
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
package com.unicorn.hgzero.ai.infra.gateway.entity;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.unicorn.hgzero.ai.biz.domain.ProcessedTranscript;
|
||||
import com.unicorn.hgzero.common.entity.BaseTimeEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 처리된 회의록 Entity
|
||||
* AI가 처리한 회의록 정보를 데이터베이스에 영속화
|
||||
*/
|
||||
@Slf4j
|
||||
@Entity
|
||||
@Table(name = "processed_transcripts")
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ProcessedTranscriptEntity extends BaseTimeEntity {
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Id
|
||||
@Column(name = "transcript_id", length = 50)
|
||||
private String transcriptId;
|
||||
|
||||
@Column(name = "meeting_id", length = 50, nullable = false)
|
||||
private String meetingId;
|
||||
|
||||
@Column(name = "summary", columnDefinition = "TEXT")
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 논의사항 목록 (JSON 형식)
|
||||
*/
|
||||
@Column(name = "discussions", columnDefinition = "TEXT")
|
||||
private String discussions;
|
||||
|
||||
/**
|
||||
* 결정사항 목록 (JSON 형식)
|
||||
*/
|
||||
@Column(name = "decisions", columnDefinition = "TEXT")
|
||||
private String decisions;
|
||||
|
||||
/**
|
||||
* 보류사항 목록 (콤마 구분)
|
||||
*/
|
||||
@Column(name = "pending_items", columnDefinition = "TEXT")
|
||||
private String pendingItems;
|
||||
|
||||
@Column(name = "status", length = 20, nullable = false)
|
||||
@Builder.Default
|
||||
private String status = "DRAFT";
|
||||
|
||||
/**
|
||||
* Entity를 Domain 모델로 변환
|
||||
*/
|
||||
public ProcessedTranscript toDomain() {
|
||||
return ProcessedTranscript.builder()
|
||||
.transcriptId(this.transcriptId)
|
||||
.meetingId(this.meetingId)
|
||||
.summary(this.summary)
|
||||
.discussions(parseDiscussions(this.discussions))
|
||||
.decisions(parseDecisions(this.decisions))
|
||||
.pendingItems(parsePendingItems(this.pendingItems))
|
||||
.createdAt(this.getCreatedAt())
|
||||
.status(this.status)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain 모델에서 Entity로 변환
|
||||
*/
|
||||
public static ProcessedTranscriptEntity fromDomain(ProcessedTranscript domain) {
|
||||
return ProcessedTranscriptEntity.builder()
|
||||
.transcriptId(domain.getTranscriptId())
|
||||
.meetingId(domain.getMeetingId())
|
||||
.summary(domain.getSummary())
|
||||
.discussions(formatDiscussions(domain.getDiscussions()))
|
||||
.decisions(formatDecisions(domain.getDecisions()))
|
||||
.pendingItems(formatPendingItems(domain.getPendingItems()))
|
||||
.status(domain.getStatus())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 업데이트
|
||||
*/
|
||||
public void updateStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 요약 업데이트
|
||||
*/
|
||||
public void updateSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Private Helper Methods - JSON 변환
|
||||
// ========================================
|
||||
|
||||
private static List<ProcessedTranscript.DiscussionItem> parseDiscussions(String json) {
|
||||
if (json == null || json.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(json,
|
||||
new TypeReference<List<ProcessedTranscript.DiscussionItem>>() {});
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Failed to parse discussions JSON: {}", json, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
private static String formatDiscussions(List<ProcessedTranscript.DiscussionItem> discussions) {
|
||||
if (discussions == null || discussions.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return objectMapper.writeValueAsString(discussions);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Failed to format discussions to JSON", e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static List<ProcessedTranscript.DecisionItem> parseDecisions(String json) {
|
||||
if (json == null || json.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(json,
|
||||
new TypeReference<List<ProcessedTranscript.DecisionItem>>() {});
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Failed to parse decisions JSON: {}", json, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
private static String formatDecisions(List<ProcessedTranscript.DecisionItem> decisions) {
|
||||
if (decisions == null || decisions.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return objectMapper.writeValueAsString(decisions);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Failed to format decisions to JSON", e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> parsePendingItems(String items) {
|
||||
if (items == null || items.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return Arrays.asList(items.split(","));
|
||||
}
|
||||
|
||||
private static String formatPendingItems(List<String> items) {
|
||||
if (items == null || items.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return String.join(",", items);
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.unicorn.hgzero.ai.infra.gateway.repository;
|
||||
|
||||
import com.unicorn.hgzero.ai.infra.gateway.entity.ProcessedTranscriptEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 처리된 회의록 JPA Repository
|
||||
* 회의록 데이터 영속성 관리
|
||||
*/
|
||||
@Repository
|
||||
public interface ProcessedTranscriptJpaRepository extends JpaRepository<ProcessedTranscriptEntity, String> {
|
||||
|
||||
/**
|
||||
* 회의 ID로 회의록 조회
|
||||
*/
|
||||
Optional<ProcessedTranscriptEntity> findByMeetingId(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의 ID 목록으로 회의록 목록 조회
|
||||
*/
|
||||
List<ProcessedTranscriptEntity> findByMeetingIdIn(List<String> meetingIds);
|
||||
|
||||
/**
|
||||
* 상태로 회의록 목록 조회
|
||||
*/
|
||||
List<ProcessedTranscriptEntity> findByStatus(String status);
|
||||
|
||||
/**
|
||||
* 회의 ID와 상태로 회의록 조회
|
||||
*/
|
||||
Optional<ProcessedTranscriptEntity> findByMeetingIdAndStatus(String meetingId, String status);
|
||||
|
||||
/**
|
||||
* 회의 ID로 회의록 존재 여부 확인
|
||||
*/
|
||||
boolean existsByMeetingId(String meetingId);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.unicorn.hgzero.ai.infra.llm;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.gateway.LlmGateway;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* OpenAI LLM Gateway 구현체
|
||||
* OpenAI API를 사용한 LLM 연동
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class OpenAiLlmGateway implements LlmGateway {
|
||||
|
||||
// TODO: OpenAI API 클라이언트 주입
|
||||
// private final OpenAiClient openAiClient;
|
||||
|
||||
@Override
|
||||
public String generateTranscript(String transcriptText, String title, List<String> participants, List<String> agenda) {
|
||||
log.info("Generating transcript using OpenAI: title={}", title);
|
||||
|
||||
// TODO: OpenAI API 호출
|
||||
// 1. 프롬프트 구성 (회의록 자동 작성 프롬프트)
|
||||
// 2. GPT-4 호출
|
||||
// 3. 응답 JSON 파싱
|
||||
// 4. 반환
|
||||
|
||||
// 임시 mock 응답
|
||||
return """
|
||||
{
|
||||
"summary": "회의록 자동 생성 요약",
|
||||
"discussions": [
|
||||
{
|
||||
"topic": "프로젝트 진행 상황",
|
||||
"speaker": "김철수",
|
||||
"content": "현재 80% 진행 중"
|
||||
}
|
||||
],
|
||||
"decisions": [
|
||||
{
|
||||
"content": "React로 프론트엔드 개발",
|
||||
"decisionMaker": "이영희",
|
||||
"category": "기술"
|
||||
}
|
||||
],
|
||||
"pendingItems": ["추가 예산 검토", "외주 업체 선정"]
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String extractTodos(String minutesContent) {
|
||||
log.info("Extracting todos using OpenAI");
|
||||
|
||||
// TODO: OpenAI API 호출 (Todo 추출 프롬프트)
|
||||
return """
|
||||
{
|
||||
"todos": [
|
||||
{
|
||||
"content": "API 설계서 작성",
|
||||
"assignee": "박민수",
|
||||
"dueDate": "2025-01-30",
|
||||
"priority": "HIGH",
|
||||
"sectionReference": "결정사항 #3"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateSummary(String sectionContent, String meetingContext) {
|
||||
log.info("Generating section summary using OpenAI");
|
||||
|
||||
// TODO: OpenAI API 호출 (섹션 요약 프롬프트)
|
||||
return "AI 기반 회의록 자동화 서비스로 결정. 타겟은 중소기업 및 스타트업이며, 주요 기능은 음성인식, AI 요약, Todo 추출입니다.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String detectTerms(String text, String organizationId) {
|
||||
log.info("Detecting terms using OpenAI: organizationId={}", organizationId);
|
||||
|
||||
// TODO: OpenAI API 호출 (전문용어 감지 프롬프트)
|
||||
return """
|
||||
{
|
||||
"terms": [
|
||||
{
|
||||
"term": "MSA",
|
||||
"position": {"line": 5, "offset": 42},
|
||||
"confidence": 0.92,
|
||||
"category": "기술",
|
||||
"highlight": true
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String suggestDiscussions(String transcriptText, List<String> agenda) {
|
||||
log.info("Suggesting discussions using OpenAI");
|
||||
|
||||
// TODO: OpenAI API 호출 (논의사항 제안 프롬프트)
|
||||
return """
|
||||
{
|
||||
"suggestions": [
|
||||
{
|
||||
"id": "sugg-001",
|
||||
"topic": "보안 요구사항 검토",
|
||||
"reason": "안건에 포함되어 있으나 아직 논의되지 않음",
|
||||
"priority": "HIGH",
|
||||
"relatedAgenda": "프로젝트 개요",
|
||||
"estimatedTime": 15
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String suggestDecisions(String transcriptText) {
|
||||
log.info("Suggesting decisions using OpenAI");
|
||||
|
||||
// TODO: OpenAI API 호출 (결정사항 제안 프롬프트)
|
||||
return """
|
||||
{
|
||||
"suggestions": [
|
||||
{
|
||||
"id": "dec-001",
|
||||
"content": "React로 프론트엔드 개발",
|
||||
"category": "기술",
|
||||
"decisionMaker": "김철수",
|
||||
"participants": ["김철수", "이영희"],
|
||||
"confidence": 0.85,
|
||||
"extractedFrom": "프론트엔드는 React로 개발하기로 했습니다",
|
||||
"context": "팀원 대부분이 React 경험이 있어 개발 속도가 빠를 것으로 예상"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package com.unicorn.hgzero.ai.infra.search;
|
||||
|
||||
import com.unicorn.hgzero.ai.biz.gateway.SearchGateway;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Azure AI Search Gateway 구현체
|
||||
* RAG 기반 벡터 검색 기능 제공
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AzureAiSearchGateway implements SearchGateway {
|
||||
|
||||
// TODO: Azure AI Search 클라이언트 주입
|
||||
// private final SearchClient searchClient;
|
||||
|
||||
@Override
|
||||
public String searchRelatedTranscripts(String meetingId, String transcriptId, int limit) {
|
||||
log.info("Searching related transcripts: meetingId={}, transcriptId={}, limit={}",
|
||||
meetingId, transcriptId, limit);
|
||||
|
||||
// TODO: Azure AI Search 벡터 검색
|
||||
// 1. 회의록 내용으로 임베딩 생성
|
||||
// 2. 벡터 유사도 검색
|
||||
// 3. 상위 N개 결과 반환
|
||||
|
||||
// 임시 mock 응답
|
||||
return """
|
||||
{
|
||||
"relatedTranscripts": [
|
||||
{
|
||||
"transcriptId": "aa0e8400-e29b-41d4-a716-446655440005",
|
||||
"title": "프로젝트 X 주간 회의",
|
||||
"date": "2025-01-15",
|
||||
"participants": ["김철수", "이영희"],
|
||||
"relevanceScore": 85.5,
|
||||
"commonKeywords": ["MSA", "API Gateway", "Spring Boot"],
|
||||
"link": "/transcripts/aa0e8400-e29b-41d4-a716-446655440005"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String searchTermExplanation(String term, String meetingId, String context) {
|
||||
log.info("Searching term explanation: term={}, meetingId={}", term, meetingId);
|
||||
|
||||
// TODO: Azure AI Search 문서 검색
|
||||
// 1. 용어와 맥락으로 검색 쿼리 구성
|
||||
// 2. 과거 회의록, 위키, 매뉴얼 검색
|
||||
// 3. 관련 문서 반환
|
||||
|
||||
// 임시 mock 응답
|
||||
return """
|
||||
{
|
||||
"term": "MSA",
|
||||
"basicDefinition": "Microservices Architecture의 약자",
|
||||
"contextualMeaning": "이번 프로젝트에서는 확장성과 독립 배포를 위해 MSA를 적용하기로 결정",
|
||||
"useCases": [
|
||||
"2024년 프로젝트 X에서 주문/결제/배송 서비스를 독립적으로 구성",
|
||||
"서비스별 독립 배포로 배포 시간 70% 단축"
|
||||
],
|
||||
"relatedProjects": [
|
||||
{"name": "프로젝트 X", "relevance": "동일한 MSA 아키텍처 적용"}
|
||||
],
|
||||
"pastDiscussions": [
|
||||
{
|
||||
"date": "2024-12-15",
|
||||
"participants": ["김철수", "이영희"],
|
||||
"summary": "MSA 아키텍처의 장단점을 비교하고 적용 방안을 논의",
|
||||
"link": "/transcripts/bb0e8400-e29b-41d4-a716-446655440006"
|
||||
}
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"title": "MSA 아키텍처 가이드",
|
||||
"type": "위키",
|
||||
"link": "https://wiki.example.com/msa-guide"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void indexTranscript(String transcriptId, String content, String metadata) {
|
||||
log.info("Indexing transcript: transcriptId={}", transcriptId);
|
||||
|
||||
// TODO: Azure AI Search 인덱싱
|
||||
// 1. 회의록 내용 임베딩 생성
|
||||
// 2. 벡터와 메타데이터를 인덱스에 저장
|
||||
// 3. 검색 가능 상태로 만들기
|
||||
|
||||
log.debug("Transcript indexed successfully: {}", transcriptId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
spring:
|
||||
application:
|
||||
name: ai
|
||||
|
||||
# Database Configuration
|
||||
datasource:
|
||||
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:20.249.153.213}:${DB_PORT:5432}/${DB_NAME:aidb}
|
||||
username: ${DB_USERNAME:hgzerouser}
|
||||
password: ${DB_PASSWORD:Hi5Jessica!}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
leak-detection-threshold: 60000
|
||||
|
||||
# JPA Configuration
|
||||
jpa:
|
||||
show-sql: ${SHOW_SQL:true}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
use_sql_comments: true
|
||||
hibernate:
|
||||
ddl-auto: ${DDL_AUTO:update}
|
||||
|
||||
# Flyway Configuration
|
||||
flyway:
|
||||
enabled: false
|
||||
|
||||
# Redis Configuration
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:20.249.177.114}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:Hi5Jessica!}
|
||||
timeout: 2000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
max-wait: -1ms
|
||||
database: ${REDIS_DATABASE:4}
|
||||
|
||||
# Server Configuration
|
||||
server:
|
||||
port: ${SERVER_PORT:8083}
|
||||
servlet:
|
||||
context-path: ${CONTEXT_PATH:}
|
||||
|
||||
# JWT Configuration
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:dev-jwt-secret-key-for-development-only}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:1800}
|
||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:86400}
|
||||
|
||||
# CORS Configuration
|
||||
cors:
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:*}
|
||||
|
||||
# Azure OpenAI Configuration
|
||||
azure:
|
||||
openai:
|
||||
api-key: ${AZURE_OPENAI_API_KEY:}
|
||||
endpoint: ${AZURE_OPENAI_ENDPOINT:}
|
||||
deployment-name: ${AZURE_OPENAI_DEPLOYMENT:gpt-4o}
|
||||
embedding-deployment: ${AZURE_OPENAI_EMBEDDING_DEPLOYMENT:text-embedding-3-large}
|
||||
max-tokens: ${AZURE_OPENAI_MAX_TOKENS:2000}
|
||||
temperature: ${AZURE_OPENAI_TEMPERATURE:0.3}
|
||||
|
||||
# External AI API Configuration
|
||||
external:
|
||||
ai:
|
||||
claude:
|
||||
api-key: ${CLAUDE_API_KEY:}
|
||||
base-url: ${CLAUDE_BASE_URL:https://api.anthropic.com}
|
||||
model: ${CLAUDE_MODEL:claude-3-5-sonnet-20241022}
|
||||
max-tokens: ${CLAUDE_MAX_TOKENS:2000}
|
||||
temperature: ${CLAUDE_TEMPERATURE:0.3}
|
||||
openai:
|
||||
api-key: ${OPENAI_API_KEY:}
|
||||
base-url: ${OPENAI_BASE_URL:https://api.openai.com}
|
||||
openweather:
|
||||
api-key: ${OPENWEATHER_API_KEY:}
|
||||
base-url: ${OPENWEATHER_BASE_URL:https://api.openweathermap.org}
|
||||
kakao:
|
||||
api-key: ${KAKAO_API_KEY:}
|
||||
base-url: ${KAKAO_BASE_URL:https://dapi.kakao.com}
|
||||
|
||||
# Azure AI Search Configuration
|
||||
ai-search:
|
||||
endpoint: ${AZURE_AI_SEARCH_ENDPOINT:}
|
||||
api-key: ${AZURE_AI_SEARCH_API_KEY:}
|
||||
index-name: ${AZURE_AI_SEARCH_INDEX:meeting-transcripts}
|
||||
|
||||
# Azure Event Hubs Configuration
|
||||
eventhub:
|
||||
connection-string: ${AZURE_EVENTHUB_CONNECTION_STRING:Endpoint=sb://hgzero-eventhub-ns.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=VUqZ9vFgu35E3c6RiUzoOGVUP8IZpFvlV+AEhC6sUpo=}
|
||||
namespace: ${AZURE_EVENTHUB_NAMESPACE:hgzero-eventhub-ns}
|
||||
eventhub-name: ${AZURE_EVENTHUB_NAME:hgzero-eventhub-name}
|
||||
checkpoint-storage-connection-string: ${AZURE_CHECKPOINT_STORAGE_CONNECTION_STRING:}
|
||||
checkpoint-container: ${AZURE_CHECKPOINT_CONTAINER:hgzero-checkpoints}
|
||||
consumer-group:
|
||||
transcript: ${AZURE_EVENTHUB_CONSUMER_GROUP_TRANSCRIPT:ai-transcript-group}
|
||||
meeting: ${AZURE_EVENTHUB_CONSUMER_GROUP_MEETING:ai-meeting-group}
|
||||
|
||||
# Actuator Configuration
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
base-path: /actuator
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
show-components: always
|
||||
health:
|
||||
livenessState:
|
||||
enabled: true
|
||||
readinessState:
|
||||
enabled: true
|
||||
|
||||
# OpenAPI Documentation
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
tags-sorter: alpha
|
||||
operations-sorter: alpha
|
||||
show-actuator: false
|
||||
|
||||
# Logging Configuration
|
||||
logging:
|
||||
level:
|
||||
root: ${LOG_LEVEL_ROOT:INFO}
|
||||
com.unicorn.hgzero.ai: ${LOG_LEVEL_APP:DEBUG}
|
||||
org.springframework.web: ${LOG_LEVEL_WEB:INFO}
|
||||
org.springframework.security: ${LOG_LEVEL_SECURITY:DEBUG}
|
||||
org.hibernate.SQL: ${LOG_LEVEL_SQL:DEBUG}
|
||||
org.hibernate.type: ${LOG_LEVEL_SQL_TYPE:TRACE}
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file:
|
||||
name: ${LOG_FILE:logs/ai-service.log}
|
||||
logback:
|
||||
rollingpolicy:
|
||||
max-file-size: ${LOG_MAX_FILE_SIZE:10MB}
|
||||
max-history: ${LOG_MAX_HISTORY:7}
|
||||
total-size-cap: ${LOG_TOTAL_SIZE_CAP:100MB}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user