mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 11:26:25 +00:00
notification 실행환경 설정
This commit is contained in:
parent
0dc0e0cee6
commit
16caafd7c8
File diff suppressed because one or more lines are too long
@ -0,0 +1,138 @@
|
|||||||
|
package com.unicorn.hgzero.common.security;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.ExpiredJwtException;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import io.jsonwebtoken.MalformedJwtException;
|
||||||
|
import io.jsonwebtoken.UnsupportedJwtException;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import io.jsonwebtoken.security.SecurityException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 토큰 제공자
|
||||||
|
* JWT 토큰의 생성, 검증, 파싱을 담당
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class JwtTokenProvider {
|
||||||
|
|
||||||
|
private final SecretKey secretKey;
|
||||||
|
private final long tokenValidityInMilliseconds;
|
||||||
|
|
||||||
|
public JwtTokenProvider(@Value("${jwt.secret}") String secret,
|
||||||
|
@Value("${jwt.access-token-validity:3600}") long tokenValidityInSeconds) {
|
||||||
|
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||||
|
this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP 요청에서 JWT 토큰 추출
|
||||||
|
*/
|
||||||
|
public String resolveToken(HttpServletRequest request) {
|
||||||
|
String bearerToken = request.getHeader("Authorization");
|
||||||
|
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
|
||||||
|
return bearerToken.substring(7);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 토큰 유효성 검증
|
||||||
|
*/
|
||||||
|
public boolean validateToken(String token) {
|
||||||
|
try {
|
||||||
|
Jwts.parser()
|
||||||
|
.setSigningKey(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token);
|
||||||
|
return true;
|
||||||
|
} catch (SecurityException | MalformedJwtException e) {
|
||||||
|
log.debug("Invalid JWT signature: {}", e.getMessage());
|
||||||
|
} catch (ExpiredJwtException e) {
|
||||||
|
log.debug("Expired JWT token: {}", e.getMessage());
|
||||||
|
} catch (UnsupportedJwtException e) {
|
||||||
|
log.debug("Unsupported JWT token: {}", e.getMessage());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.debug("JWT token compact of handler are invalid: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 토큰에서 사용자 ID 추출
|
||||||
|
*/
|
||||||
|
public String getUserId(String token) {
|
||||||
|
Claims claims = Jwts.parser()
|
||||||
|
.setSigningKey(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
|
||||||
|
return claims.getSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 토큰에서 사용자명 추출
|
||||||
|
*/
|
||||||
|
public String getUsername(String token) {
|
||||||
|
Claims claims = Jwts.parser()
|
||||||
|
.setSigningKey(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
|
||||||
|
return claims.get("username", String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 토큰에서 권한 정보 추출
|
||||||
|
*/
|
||||||
|
public String getAuthority(String token) {
|
||||||
|
Claims claims = Jwts.parser()
|
||||||
|
.setSigningKey(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
|
||||||
|
return claims.get("authority", String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토큰 만료 시간 확인
|
||||||
|
*/
|
||||||
|
public boolean isTokenExpired(String token) {
|
||||||
|
try {
|
||||||
|
Claims claims = Jwts.parser()
|
||||||
|
.setSigningKey(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
|
||||||
|
return claims.getExpiration().before(new Date());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토큰에서 만료 시간 추출
|
||||||
|
*/
|
||||||
|
public Date getExpirationDate(String token) {
|
||||||
|
Claims claims = Jwts.parser()
|
||||||
|
.setSigningKey(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
|
||||||
|
return claims.getExpiration();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
package com.unicorn.hgzero.common.security;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증된 사용자 정보
|
||||||
|
* JWT 토큰에서 추출된 사용자 정보를 담는 Principal 객체
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserPrincipal {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 고유 ID
|
||||||
|
*/
|
||||||
|
private final String userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자명
|
||||||
|
*/
|
||||||
|
private final String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 권한
|
||||||
|
*/
|
||||||
|
private final String authority;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 ID 반환 (별칭)
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관리자 권한 여부 확인
|
||||||
|
*/
|
||||||
|
public boolean isAdmin() {
|
||||||
|
return "ADMIN".equals(authority);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일반 사용자 권한 여부 확인
|
||||||
|
*/
|
||||||
|
public boolean isUser() {
|
||||||
|
return "USER".equals(authority) || authority == null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
package com.unicorn.hgzero.common.security.filter;
|
||||||
|
|
||||||
|
import com.unicorn.hgzero.common.security.JwtTokenProvider;
|
||||||
|
import com.unicorn.hgzero.common.security.UserPrincipal;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 인증 필터
|
||||||
|
* HTTP 요청에서 JWT 토큰을 추출하여 인증을 수행
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
|
||||||
|
String token = jwtTokenProvider.resolveToken(request);
|
||||||
|
|
||||||
|
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
|
||||||
|
String userId = jwtTokenProvider.getUserId(token);
|
||||||
|
String username = null;
|
||||||
|
String authority = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
username = jwtTokenProvider.getUsername(token);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("JWT에 username 클레임이 없음: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
authority = jwtTokenProvider.getAuthority(token);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("JWT에 authority 클레임이 없음: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.hasText(userId)) {
|
||||||
|
// UserPrincipal 객체 생성 (username과 authority가 없어도 동작)
|
||||||
|
UserPrincipal userPrincipal = UserPrincipal.builder()
|
||||||
|
.userId(userId)
|
||||||
|
.username(username != null ? username : "unknown")
|
||||||
|
.authority(authority != null ? authority : "USER")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
UsernamePasswordAuthenticationToken authentication =
|
||||||
|
new UsernamePasswordAuthenticationToken(
|
||||||
|
userPrincipal,
|
||||||
|
null,
|
||||||
|
Collections.singletonList(new SimpleGrantedAuthority(authority != null ? authority : "USER"))
|
||||||
|
);
|
||||||
|
|
||||||
|
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
|
||||||
|
log.debug("인증된 사용자: {} ({})", userPrincipal.getUsername(), userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||||
|
String path = request.getRequestURI();
|
||||||
|
return path.startsWith("/actuator") ||
|
||||||
|
path.startsWith("/swagger-ui") ||
|
||||||
|
path.startsWith("/v3/api-docs") ||
|
||||||
|
path.equals("/health");
|
||||||
|
}
|
||||||
|
}
|
||||||
560
develop/dev/dev-ai-backend.md
Normal file
560
develop/dev/dev-ai-backend.md
Normal file
@ -0,0 +1,560 @@
|
|||||||
|
# AI Service 백엔드 개발 결과서
|
||||||
|
|
||||||
|
## 1. 개요
|
||||||
|
|
||||||
|
### 1.1 서비스 정보
|
||||||
|
- **서비스명**: AI Service
|
||||||
|
- **포트**: 8083
|
||||||
|
- **역할**: 회의록 AI 자동 작성 및 분석 서비스
|
||||||
|
- **아키텍처**: Clean Architecture (Layered + UseCase 패턴)
|
||||||
|
- **언어/프레임워크**: Java 21, Spring Boot 3.3.0
|
||||||
|
|
||||||
|
### 1.2 개발 범위
|
||||||
|
- ✅ Domain 엔티티 및 DTO 구현 (35개 파일)
|
||||||
|
- ✅ Repository 계층 구현 (1 Entity + 1 Repository)
|
||||||
|
- ✅ Service 계층 구현 (7 UseCase + 7 Service + 3 Gateway + 3 Gateway 구현체)
|
||||||
|
- ✅ Controller 계층 구현 (8개 REST API)
|
||||||
|
- ✅ Security 구현 (SecurityConfig, JWT 인증 처리)
|
||||||
|
- ✅ Swagger 설정 구현 (OpenAPI 문서화)
|
||||||
|
- ✅ 설정 파일 작성 (application.yml, 메인 클래스)
|
||||||
|
- ✅ 빌드 성공 (./gradlew ai:build)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 아키텍처 구조
|
||||||
|
|
||||||
|
### 2.1 Clean Architecture 계층
|
||||||
|
|
||||||
|
```
|
||||||
|
ai/
|
||||||
|
├── biz/ # 비즈니스 계층
|
||||||
|
│ ├── domain/ # Domain 모델 (5개)
|
||||||
|
│ │ ├── ProcessedTranscript.java
|
||||||
|
│ │ ├── ExtractedTodo.java
|
||||||
|
│ │ ├── RelatedMinutes.java
|
||||||
|
│ │ ├── Term.java
|
||||||
|
│ │ └── Suggestion.java
|
||||||
|
│ ├── usecase/ # UseCase 인터페이스 (7개)
|
||||||
|
│ │ ├── TranscriptProcessUseCase.java
|
||||||
|
│ │ ├── TodoExtractionUseCase.java
|
||||||
|
│ │ ├── SectionSummaryUseCase.java
|
||||||
|
│ │ ├── RelatedTranscriptSearchUseCase.java
|
||||||
|
│ │ ├── TermDetectionUseCase.java
|
||||||
|
│ │ ├── TermExplanationUseCase.java
|
||||||
|
│ │ └── SuggestionUseCase.java
|
||||||
|
│ ├── service/ # Service 구현체 (7개)
|
||||||
|
│ │ ├── TranscriptProcessService.java
|
||||||
|
│ │ ├── TodoExtractionService.java
|
||||||
|
│ │ ├── SectionSummaryService.java
|
||||||
|
│ │ ├── RelatedTranscriptSearchService.java
|
||||||
|
│ │ ├── TermDetectionService.java
|
||||||
|
│ │ ├── TermExplanationService.java
|
||||||
|
│ │ └── SuggestionService.java
|
||||||
|
│ └── gateway/ # Gateway 인터페이스 (3개)
|
||||||
|
│ ├── LlmGateway.java
|
||||||
|
│ ├── SearchGateway.java
|
||||||
|
│ └── TranscriptGateway.java
|
||||||
|
└── infra/ # 인프라 계층
|
||||||
|
├── controller/ # REST Controllers (7개 클래스, 8개 API)
|
||||||
|
│ ├── TranscriptController.java
|
||||||
|
│ ├── TodoController.java
|
||||||
|
│ ├── SectionController.java
|
||||||
|
│ ├── RelationController.java
|
||||||
|
│ ├── TermController.java
|
||||||
|
│ ├── ExplanationController.java
|
||||||
|
│ └── SuggestionController.java
|
||||||
|
├── dto/ # DTO (30개)
|
||||||
|
│ ├── request/ # Request DTOs (6개)
|
||||||
|
│ ├── response/ # Response DTOs (8개)
|
||||||
|
│ └── common/ # Common DTOs (16개)
|
||||||
|
├── gateway/ # Gateway 구현체 (4개)
|
||||||
|
│ ├── entity/ # JPA Entity (1개)
|
||||||
|
│ │ └── ProcessedTranscriptEntity.java
|
||||||
|
│ ├── repository/ # JPA Repository (1개)
|
||||||
|
│ │ └── ProcessedTranscriptJpaRepository.java
|
||||||
|
│ └── TranscriptGatewayImpl.java
|
||||||
|
├── llm/ # LLM 클라이언트 (1개)
|
||||||
|
│ └── OpenAiLlmGateway.java
|
||||||
|
└── search/ # RAG 검색 클라이언트 (1개)
|
||||||
|
└── AzureAiSearchGateway.java
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 패키지 구조 특징
|
||||||
|
- **biz/domain**: 순수 비즈니스 Domain 객체 (외부 의존성 없음)
|
||||||
|
- **biz/usecase**: 비즈니스 유스케이스 인터페이스 정의
|
||||||
|
- **biz/service**: UseCase 인터페이스 구현체
|
||||||
|
- **biz/gateway**: 외부 시스템 연동 추상화
|
||||||
|
- **infra/**: 모든 외부 의존성 (Controller, DTO, Gateway 구현, 외부 API 클라이언트)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 구현 상세
|
||||||
|
|
||||||
|
### 3.1 Domain 모델 (5개)
|
||||||
|
|
||||||
|
| Domain | 설명 | 주요 필드 |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| ProcessedTranscript | 처리된 회의록 | transcriptId, meetingId, summary, discussions, decisions, pendingItems |
|
||||||
|
| ExtractedTodo | 추출된 Todo | content, assignee, dueDate, priority, sectionReference |
|
||||||
|
| RelatedMinutes | 관련 회의록 | transcriptId, title, date, participants, relevanceScore, commonKeywords |
|
||||||
|
| Term | 전문용어 | term, position, confidence, category, highlight |
|
||||||
|
| Suggestion | AI 제안사항 | id, type (DISCUSSION/DECISION), content, priority, reason, confidence |
|
||||||
|
|
||||||
|
### 3.2 UseCase 및 Service (7개)
|
||||||
|
|
||||||
|
#### 3.2.1 TranscriptProcessService ⭐️
|
||||||
|
**역할**: 회의록 자동 작성 (핵심 기능)
|
||||||
|
|
||||||
|
**주요 메서드**:
|
||||||
|
- `processTranscript()`: STT 텍스트 → LLM 회의록 생성 → DB 저장 → RAG 인덱싱
|
||||||
|
- `getTranscript()`: 회의록 ID로 조회
|
||||||
|
- `getTranscriptByMeetingId()`: 회의 ID로 조회
|
||||||
|
|
||||||
|
**구현 로직**:
|
||||||
|
1. LLM Gateway를 통한 회의록 생성 (OpenAI GPT-4)
|
||||||
|
2. JSON 응답 파싱 및 Domain 객체 변환
|
||||||
|
3. Transaction 내에서 DB 저장
|
||||||
|
4. Azure AI Search 인덱싱 (비동기 처리 고려)
|
||||||
|
|
||||||
|
#### 3.2.2 TodoExtractionService
|
||||||
|
**역할**: 회의록에서 Todo 자동 추출
|
||||||
|
|
||||||
|
**주요 메서드**:
|
||||||
|
- `extractTodos()`: 회의록 내용 → LLM Todo 추출 → Todo 목록 반환
|
||||||
|
|
||||||
|
#### 3.2.3 SectionSummaryService
|
||||||
|
**역할**: 섹션 AI 요약 재생성
|
||||||
|
|
||||||
|
**주요 메서드**:
|
||||||
|
- `regenerateSummary()`: 섹션 내용 → LLM 요약 생성 (2-3문장)
|
||||||
|
|
||||||
|
#### 3.2.4 RelatedTranscriptSearchService
|
||||||
|
**역할**: RAG 기반 관련 회의록 검색
|
||||||
|
|
||||||
|
**주요 메서드**:
|
||||||
|
- `findRelatedTranscripts()`: 벡터 유사도 검색 → 관련 회의록 반환
|
||||||
|
|
||||||
|
#### 3.2.5 TermDetectionService
|
||||||
|
**역할**: 전문용어 자동 감지
|
||||||
|
|
||||||
|
**주요 메서드**:
|
||||||
|
- `detectTerms()`: 텍스트 분석 → 전문용어 목록 반환
|
||||||
|
|
||||||
|
#### 3.2.6 TermExplanationService
|
||||||
|
**역할**: RAG 기반 용어 설명 생성
|
||||||
|
|
||||||
|
**주요 메서드**:
|
||||||
|
- `explainTerm()`: 용어 + 맥락 → RAG 검색 → 상세 설명 반환
|
||||||
|
|
||||||
|
#### 3.2.7 SuggestionService
|
||||||
|
**역할**: 논의사항/결정사항 실시간 제안
|
||||||
|
|
||||||
|
**주요 메서드**:
|
||||||
|
- `suggestDiscussions()`: 현재 회의록 → 추가 논의 주제 제안
|
||||||
|
- `suggestDecisions()`: 현재 회의록 → 결정사항 패턴 감지 및 제안
|
||||||
|
|
||||||
|
### 3.3 Gateway 구현 (3개)
|
||||||
|
|
||||||
|
#### 3.3.1 TranscriptGatewayImpl
|
||||||
|
**역할**: 회의록 데이터 영속성 관리
|
||||||
|
|
||||||
|
**구현**: ProcessedTranscriptJpaRepository 래핑
|
||||||
|
|
||||||
|
**주요 메서드**: save, findById, findByMeetingId, findByMeetingIds, findByStatus, existsByMeetingId, delete
|
||||||
|
|
||||||
|
#### 3.3.2 OpenAiLlmGateway
|
||||||
|
**역할**: OpenAI API 연동
|
||||||
|
|
||||||
|
**구현 상태**: 스켈레톤 (Mock 응답 반환)
|
||||||
|
|
||||||
|
**TODO**: 실제 OpenAI API 호출 로직 구현 필요
|
||||||
|
|
||||||
|
**주요 메서드**: generateTranscript, extractTodos, generateSummary, detectTerms, suggestDiscussions, suggestDecisions
|
||||||
|
|
||||||
|
#### 3.3.3 AzureAiSearchGateway
|
||||||
|
**역할**: Azure AI Search RAG 연동
|
||||||
|
|
||||||
|
**구현 상태**: 스켈레톤 (Mock 응답 반환)
|
||||||
|
|
||||||
|
**TODO**: 실제 Azure AI Search API 호출 로직 구현 필요
|
||||||
|
|
||||||
|
**주요 메서드**: searchRelatedTranscripts, searchTermExplanation, indexTranscript
|
||||||
|
|
||||||
|
### 3.4 REST API (8개)
|
||||||
|
|
||||||
|
| 엔드포인트 | Method | Controller | 설명 |
|
||||||
|
|-----------|--------|------------|------|
|
||||||
|
| `/api/transcripts/process` | POST | TranscriptController | 회의록 자동 작성 |
|
||||||
|
| `/api/todos/extract` | POST | TodoController | Todo 자동 추출 |
|
||||||
|
| `/api/sections/{sectionId}/regenerate-summary` | POST | SectionController | 섹션 요약 재생성 |
|
||||||
|
| `/api/transcripts/{meetingId}/related` | GET | RelationController | 관련 회의록 조회 |
|
||||||
|
| `/api/terms/detect` | POST | TermController | 전문용어 감지 |
|
||||||
|
| `/api/terms/{term}/explain` | GET | ExplanationController | 용어 설명 |
|
||||||
|
| `/api/suggestions/discussion` | POST | SuggestionController | 논의사항 제안 |
|
||||||
|
| `/api/suggestions/decision` | POST | SuggestionController | 결정사항 제안 |
|
||||||
|
|
||||||
|
### 3.5 Repository 및 Entity
|
||||||
|
|
||||||
|
#### ProcessedTranscriptEntity
|
||||||
|
**테이블명**: `processed_transcripts`
|
||||||
|
|
||||||
|
**주요 컬럼**:
|
||||||
|
- `transcript_id` (PK, VARCHAR(50))
|
||||||
|
- `meeting_id` (VARCHAR(50), NOT NULL)
|
||||||
|
- `summary` (TEXT)
|
||||||
|
- `discussions` (TEXT, JSON 형식)
|
||||||
|
- `decisions` (TEXT, JSON 형식)
|
||||||
|
- `pending_items` (TEXT, 콤마 구분)
|
||||||
|
- `status` (VARCHAR(20), DEFAULT 'DRAFT')
|
||||||
|
- `created_at`, `updated_at` (BaseTimeEntity 상속)
|
||||||
|
|
||||||
|
**특징**:
|
||||||
|
- Jackson ObjectMapper를 사용한 JSON 직렬화/역직렬화
|
||||||
|
- `toDomain()`, `fromDomain()` 메서드로 Entity ↔ Domain 변환
|
||||||
|
- BaseTimeEntity 상속으로 생성일시/수정일시 자동 관리
|
||||||
|
|
||||||
|
#### ProcessedTranscriptJpaRepository
|
||||||
|
**타입**: JpaRepository<ProcessedTranscriptEntity, String>
|
||||||
|
|
||||||
|
**Custom 쿼리**:
|
||||||
|
- `findByMeetingId(String meetingId)`
|
||||||
|
- `findByMeetingIdIn(List<String> meetingIds)`
|
||||||
|
- `findByStatus(String status)`
|
||||||
|
- `findByMeetingIdAndStatus(String meetingId, String status)`
|
||||||
|
- `existsByMeetingId(String meetingId)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 설정 및 의존성
|
||||||
|
|
||||||
|
### 4.1 application.yml 주요 설정
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Server
|
||||||
|
server.port: 8083
|
||||||
|
|
||||||
|
# Database
|
||||||
|
spring.datasource.url: jdbc:postgresql://20.249.153.213:5432/aidb
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
spring.data.redis.host: 20.249.177.114
|
||||||
|
spring.data.redis.database: 4
|
||||||
|
|
||||||
|
# OpenAI
|
||||||
|
azure.openai.deployment-name: gpt-4o
|
||||||
|
azure.openai.embedding-deployment: text-embedding-3-large
|
||||||
|
azure.openai.max-tokens: 2000
|
||||||
|
azure.openai.temperature: 0.3
|
||||||
|
|
||||||
|
# Azure AI Search
|
||||||
|
external.ai-search.index-name: meeting-transcripts
|
||||||
|
|
||||||
|
# Event Hub (Kafka 대체)
|
||||||
|
external.eventhub.eventhub-name: hgzero-eventhub-name
|
||||||
|
external.eventhub.consumer-group.transcript: ai-transcript-group
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 주요 의존성 (build.gradle)
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
dependencies {
|
||||||
|
// Common 모듈
|
||||||
|
implementation project(':common')
|
||||||
|
|
||||||
|
// Spring Boot
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
|
|
||||||
|
// Database
|
||||||
|
runtimeOnly 'org.postgresql:postgresql'
|
||||||
|
|
||||||
|
// OpenAI
|
||||||
|
implementation 'com.azure:azure-ai-openai:1.0.0-beta.6'
|
||||||
|
|
||||||
|
// Azure AI Search
|
||||||
|
implementation 'com.azure:azure-search-documents:11.5.0'
|
||||||
|
|
||||||
|
// Jackson
|
||||||
|
implementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||||
|
|
||||||
|
// Lombok
|
||||||
|
compileOnly 'org.projectlombok:lombok'
|
||||||
|
annotationProcessor 'org.projectlombok:lombok'
|
||||||
|
|
||||||
|
// Swagger
|
||||||
|
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 개발 현황 및 TODO
|
||||||
|
|
||||||
|
### 5.1 완료된 작업 ✅
|
||||||
|
1. ✅ 패키지 구조 설계 및 문서화
|
||||||
|
2. ✅ Domain 엔티티 5개 구현
|
||||||
|
3. ✅ DTO 35개 구현 (Request 6개 + Response 8개 + Common 16개 + Error 1개)
|
||||||
|
4. ✅ Repository 계층 구현 (Entity 1개 + Repository 1개)
|
||||||
|
5. ✅ UseCase 인터페이스 7개 정의
|
||||||
|
6. ✅ Service 구현체 7개 작성
|
||||||
|
7. ✅ Gateway 인터페이스 3개 정의
|
||||||
|
8. ✅ Gateway 구현체 3개 작성 (TranscriptGateway 완성, LLM/Search 스켈레톤)
|
||||||
|
9. ✅ Controller 7개 클래스 구현 (8개 REST API)
|
||||||
|
10. ✅ 메인 클래스 작성 (AiServiceApplication.java)
|
||||||
|
11. ✅ 설정 파일 완성 (application.yml)
|
||||||
|
|
||||||
|
### 5.2 미완성 작업 (TODO)
|
||||||
|
|
||||||
|
#### 5.2.1 OpenAI LLM 연동 🔧
|
||||||
|
**파일**: `ai/infra/llm/OpenAiLlmGateway.java`
|
||||||
|
|
||||||
|
**현재 상태**: Mock 응답 반환
|
||||||
|
|
||||||
|
**필요 작업**:
|
||||||
|
1. OpenAI API 클라이언트 주입 설정
|
||||||
|
2. 각 기능별 프롬프트 엔지니어링:
|
||||||
|
- 회의록 자동 작성 프롬프트
|
||||||
|
- Todo 추출 프롬프트
|
||||||
|
- 섹션 요약 프롬프트
|
||||||
|
- 전문용어 감지 프롬프트
|
||||||
|
- 논의사항 제안 프롬프트
|
||||||
|
- 결정사항 제안 프롬프트
|
||||||
|
3. GPT-4 API 호출 및 응답 처리
|
||||||
|
4. 에러 핸들링 및 재시도 로직
|
||||||
|
|
||||||
|
#### 5.2.2 Azure AI Search RAG 연동 🔧
|
||||||
|
**파일**: `ai/infra/search/AzureAiSearchGateway.java`
|
||||||
|
|
||||||
|
**현재 상태**: Mock 응답 반환
|
||||||
|
|
||||||
|
**필요 작업**:
|
||||||
|
1. Azure AI Search 클라이언트 설정
|
||||||
|
2. 벡터 임베딩 생성 (text-embedding-3-large)
|
||||||
|
3. 인덱스 스키마 정의 및 생성
|
||||||
|
4. 벡터 검색 쿼리 구현
|
||||||
|
5. 검색 결과 파싱 및 Domain 변환
|
||||||
|
|
||||||
|
#### 5.2.3 MQ 이벤트 소비 🔧
|
||||||
|
**필요 작업**:
|
||||||
|
1. Meeting Service로부터 회의 종료 이벤트 수신
|
||||||
|
2. 회의록 자동 생성 트리거
|
||||||
|
3. Todo 추출 후 Meeting Service로 발행
|
||||||
|
4. Event Hub (Kafka) Consumer 구현
|
||||||
|
|
||||||
|
#### 5.2.4 ~~SecurityConfig 및 JWT~~ ✅ **완료**
|
||||||
|
**구현 완료**:
|
||||||
|
1. ✅ JWT 검증 필터 구현 (JwtAuthenticationFilter)
|
||||||
|
2. ✅ JWT 토큰 제공자 구현 (JwtTokenProvider)
|
||||||
|
3. ✅ 사용자 Principal 구현 (UserPrincipal)
|
||||||
|
4. ✅ SecurityConfig 작성 (인증/인가 규칙)
|
||||||
|
5. ✅ CORS 설정 적용
|
||||||
|
6. ✅ SwaggerConfig 작성 (OpenAPI 문서화)
|
||||||
|
|
||||||
|
#### 5.2.5 Service 로직 완성 🔧
|
||||||
|
**현재 상태**: 스켈레톤 구현 (Mock 데이터 반환)
|
||||||
|
|
||||||
|
**필요 작업**:
|
||||||
|
1. TodoExtractionService: LLM JSON 파싱 로직 구현
|
||||||
|
2. RelatedTranscriptSearchService: RAG JSON 파싱 로직 구현
|
||||||
|
3. TermDetectionService: LLM JSON 파싱 로직 구현
|
||||||
|
4. TermExplanationService: RAG JSON 파싱 로직 구현
|
||||||
|
5. SuggestionService: LLM JSON 파싱 로직 구현
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 빌드 및 실행
|
||||||
|
|
||||||
|
### 6.1 로컬 빌드
|
||||||
|
```bash
|
||||||
|
cd /Users/daewoong/home/workspace/HGZero/ai
|
||||||
|
./gradlew clean build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 실행
|
||||||
|
```bash
|
||||||
|
java -jar build/libs/ai-0.0.1-SNAPSHOT.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 환경 변수 설정
|
||||||
|
```bash
|
||||||
|
export DB_HOST=20.249.153.213
|
||||||
|
export DB_PORT=5432
|
||||||
|
export DB_NAME=aidb
|
||||||
|
export DB_USERNAME=hgzerouser
|
||||||
|
export DB_PASSWORD=Hi5Jessica!
|
||||||
|
|
||||||
|
export REDIS_HOST=20.249.177.114
|
||||||
|
export REDIS_PORT=6379
|
||||||
|
export REDIS_PASSWORD=Hi5Jessica!
|
||||||
|
|
||||||
|
export AZURE_OPENAI_API_KEY=your-openai-key
|
||||||
|
export AZURE_OPENAI_ENDPOINT=your-openai-endpoint
|
||||||
|
|
||||||
|
export AZURE_AI_SEARCH_ENDPOINT=your-search-endpoint
|
||||||
|
export AZURE_AI_SEARCH_API_KEY=your-search-key
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 Swagger UI
|
||||||
|
- URL: http://localhost:8083/swagger-ui.html
|
||||||
|
- API Docs: http://localhost:8083/v3/api-docs
|
||||||
|
|
||||||
|
### 6.5 Security 및 Swagger 상세 구현
|
||||||
|
|
||||||
|
#### 6.5.1 JWT 인증 처리 (Common 모듈)
|
||||||
|
**파일 위치**: `common/src/main/java/com/unicorn/hgzero/common/security/`
|
||||||
|
|
||||||
|
**JwtTokenProvider** (`JwtTokenProvider.java`)
|
||||||
|
- JWT 토큰 검증 및 파싱
|
||||||
|
- 사용자 ID, 사용자명, 권한 정보 추출
|
||||||
|
- 토큰 만료 시간 확인
|
||||||
|
- HMAC-SHA256 알고리즘 사용
|
||||||
|
|
||||||
|
**JwtAuthenticationFilter** (`filter/JwtAuthenticationFilter.java`)
|
||||||
|
- HTTP 요청에서 JWT 토큰 추출 (`Authorization: Bearer {token}`)
|
||||||
|
- 토큰 유효성 검증 후 SecurityContext에 인증 정보 설정
|
||||||
|
- Actuator, Swagger 경로는 인증 제외 처리
|
||||||
|
|
||||||
|
**UserPrincipal** (`UserPrincipal.java`)
|
||||||
|
- 인증된 사용자 정보 담는 Principal 객체
|
||||||
|
- userId, username, authority 정보 보유
|
||||||
|
- 관리자/사용자 권한 확인 메서드 제공
|
||||||
|
|
||||||
|
#### 6.5.2 Security 설정 (AI 서비스)
|
||||||
|
**파일 위치**: `ai/src/main/java/com/unicorn/hgzero/ai/infra/config/SecurityConfig.java`
|
||||||
|
|
||||||
|
**주요 기능**:
|
||||||
|
- CSRF 비활성화 (Stateless API)
|
||||||
|
- CORS 설정 (환경변수 기반)
|
||||||
|
- Stateless 세션 관리
|
||||||
|
- JWT 필터 적용 (UsernamePasswordAuthenticationFilter 이전)
|
||||||
|
- 인증 제외 경로: `/actuator/**`, `/swagger-ui/**`, `/v3/api-docs/**`, `/health`
|
||||||
|
- 기타 모든 요청: 인증 필수
|
||||||
|
|
||||||
|
#### 6.5.3 Swagger 설정 (AI 서비스)
|
||||||
|
**파일 위치**: `ai/src/main/java/com/unicorn/hgzero/ai/infra/config/SwaggerConfig.java`
|
||||||
|
|
||||||
|
**주요 기능**:
|
||||||
|
- OpenAPI 3.0 문서 생성
|
||||||
|
- Bearer Authentication 보안 스킴 설정
|
||||||
|
- 서버 URL 설정 (로컬: http://localhost:8083)
|
||||||
|
- 커스텀 서버 URL 변수 지원 (protocol, host, port)
|
||||||
|
- API 정보: 제목, 설명, 버전, 연락처
|
||||||
|
|
||||||
|
#### 6.5.4 빌드 결과
|
||||||
|
```bash
|
||||||
|
$ ./gradlew ai:build
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 2s
|
||||||
|
10 actionable tasks: 6 executed, 4 up-to-date
|
||||||
|
```
|
||||||
|
|
||||||
|
**생성된 JAR 파일**:
|
||||||
|
- `ai/build/libs/ai.jar` (실행 가능한 JAR)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 테스트 시나리오
|
||||||
|
|
||||||
|
### 7.1 회의록 자동 작성 테스트
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8083/api/transcripts/process \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-User-Id: user123" \
|
||||||
|
-H "X-User-Name: 김철수" \
|
||||||
|
-d '{
|
||||||
|
"meetingId": "meeting-001",
|
||||||
|
"transcriptText": "안녕하세요. 오늘 회의는 신규 프로젝트 킥오프 미팅입니다...",
|
||||||
|
"context": {
|
||||||
|
"title": "신규 프로젝트 킥오프",
|
||||||
|
"participants": ["김철수", "이영희", "박민수"],
|
||||||
|
"agenda": ["프로젝트 개요", "일정 논의", "역할 분담"]
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Todo 추출 테스트
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8083/api/todos/extract \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-User-Id: user123" \
|
||||||
|
-d '{
|
||||||
|
"meetingId": "meeting-001",
|
||||||
|
"minutesContent": "## 결정사항\n1. API 설계서는 박민수님이 1월 30일까지 작성..."
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 주요 특징 및 기술적 의사결정
|
||||||
|
|
||||||
|
### 8.1 Clean Architecture 적용
|
||||||
|
- **비즈니스 로직 독립성**: biz 계층은 외부 의존성 없음
|
||||||
|
- **의존성 역전**: UseCase 인터페이스를 통한 의존성 역전
|
||||||
|
- **테스트 용이성**: Mock Gateway를 통한 단위 테스트 가능
|
||||||
|
|
||||||
|
### 8.2 JSON 데이터 저장 전략
|
||||||
|
- **복잡한 구조 (discussions, decisions)**: JSON TEXT 컬럼 저장
|
||||||
|
- **Jackson ObjectMapper**: 직렬화/역직렬화 자동 처리
|
||||||
|
- **이점**: 스키마 변경 유연성, 복잡한 조인 회피
|
||||||
|
|
||||||
|
### 8.3 Gateway 패턴
|
||||||
|
- **LlmGateway**: OpenAI API 추상화
|
||||||
|
- **SearchGateway**: Azure AI Search 추상화
|
||||||
|
- **TranscriptGateway**: 데이터 영속성 추상화
|
||||||
|
- **이점**: 외부 의존성 교체 용이, 테스트 Mock 작성 간편
|
||||||
|
|
||||||
|
### 8.4 스켈레톤 구현 전략
|
||||||
|
- **핵심 로직만 우선 구현**: TranscriptProcessService 상세 구현
|
||||||
|
- **나머지 스켈레톤 작성**: 인터페이스 정의 완료, Mock 데이터 반환
|
||||||
|
- **이점**: 전체 구조 파악 가능, 점진적 구현 가능
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 다음 단계 권장사항
|
||||||
|
|
||||||
|
### 9.1 우선순위 1 (즉시)
|
||||||
|
1. **OpenAI API 연동 완성**: OpenAiLlmGateway 실제 API 호출 구현
|
||||||
|
2. **Service JSON 파싱**: Mock 데이터 대신 실제 LLM/RAG 응답 파싱
|
||||||
|
3. ~~**컴파일 테스트**: Gradle 빌드 및 에러 수정~~ ✅ **완료**
|
||||||
|
|
||||||
|
### 9.2 우선순위 2 (단기)
|
||||||
|
1. **Azure AI Search 연동**: RAG 기능 구현
|
||||||
|
2. **MQ Event Consumer**: Meeting Service 연동
|
||||||
|
3. ~~**SecurityConfig**: JWT 인증/인가~~ ✅ **완료**
|
||||||
|
|
||||||
|
### 9.3 우선순위 3 (중기)
|
||||||
|
1. **통합 테스트**: API 엔드포인트 테스트
|
||||||
|
2. **성능 최적화**: LLM 호출 최적화, 캐싱
|
||||||
|
3. **모니터링**: 로깅, Actuator 메트릭
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 결론
|
||||||
|
|
||||||
|
### 10.1 개발 성과
|
||||||
|
- **총 작성 파일 수**: 약 86개 (Domain 5 + DTO 35 + Service/Gateway 23 + Controller 7 + Entity/Repository 2 + Security 4 + Config 2 + 기타)
|
||||||
|
- **API 엔드포인트**: 8개 (모두 Swagger 문서화 완료)
|
||||||
|
- **코드 라인 수**: 약 4,500 라인
|
||||||
|
- **아키텍처 준수**: Clean Architecture 완전 적용
|
||||||
|
- **빌드 상태**: ✅ 성공 (./gradlew ai:build)
|
||||||
|
- **Security**: ✅ JWT 인증/인가 구현 완료
|
||||||
|
|
||||||
|
### 10.2 핵심 가치
|
||||||
|
1. **확장 가능한 구조**: Clean Architecture로 비즈니스 로직 독립성 확보
|
||||||
|
2. **AI 연동 준비**: LLM/RAG Gateway 추상화로 다양한 AI 엔진 교체 가능
|
||||||
|
3. **테스트 용이성**: Interface 기반 설계로 Mock 테스트 간편
|
||||||
|
4. **표준화**: OpenAPI 3.0 명세 완벽 준수
|
||||||
|
|
||||||
|
### 10.3 개발 시 주의사항
|
||||||
|
- **LLM 비용**: OpenAI API 호출 비용 고려 필요 (캐싱 전략 필수)
|
||||||
|
- **RAG 인덱싱**: 대용량 회의록 처리 시 비동기 인덱싱 필수
|
||||||
|
- **토큰 제한**: GPT-4 토큰 제한 (8K/32K) 고려한 회의록 분할 전략
|
||||||
|
- **실시간 성능**: LLM 응답 시간 3-10초 고려한 UX 설계
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**개발 완료일**: 2025-10-24
|
||||||
|
**개발자**: AI Backend Team
|
||||||
|
**버전**: v1.0.0 (MVP)
|
||||||
File diff suppressed because it is too large
Load Diff
BIN
meeting/logs/meeting-service.log.2025-10-23.0.gz
Normal file
BIN
meeting/logs/meeting-service.log.2025-10-23.0.gz
Normal file
Binary file not shown.
@ -44,7 +44,7 @@
|
|||||||
<entry key="AZURE_EVENTHUB_CONSUMER_GROUP" value="$Default" />
|
<entry key="AZURE_EVENTHUB_CONSUMER_GROUP" value="$Default" />
|
||||||
|
|
||||||
<!-- Azure Storage Configuration -->
|
<!-- Azure Storage Configuration -->
|
||||||
<entry key="AZURE_STORAGE_CONNECTION_STRING" value="xOQGJhDT6sqOGyTohS7K5dMgGNlryuaQSg8dNCJ40sdGpYok5T5Z88M3xVlk39oeFKiQdGYCihqC+AStBsoBPw==" />
|
<entry key="AZURE_STORAGE_CONNECTION_STRING" value="DefaultEndpointsProtocol=https;AccountName=hgzerostorage;AccountKey=xOQGJhDT6sqOGyTohS7K5dMgGNlryuaQSg8dNCJ40sdGpYok5T5Z88M3xVlk39oeFKiQdGYCihqC+AStBsoBPw==;EndpointSuffix=core.windows.net" />
|
||||||
<entry key="AZURE_STORAGE_CONTAINER_NAME" value="eventhub-checkpoints" />
|
<entry key="AZURE_STORAGE_CONTAINER_NAME" value="eventhub-checkpoints" />
|
||||||
|
|
||||||
<!-- Notification Configuration -->
|
<!-- Notification Configuration -->
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
BIN
notification/logs/notification-service.log.2025-10-23.0.gz
Normal file
BIN
notification/logs/notification-service.log.2025-10-23.0.gz
Normal file
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
package com.unicorn.hgzero.notification.event;
|
package com.unicorn.hgzero.notification.event;
|
||||||
|
|
||||||
import com.azure.messaging.eventhubs.models.PartitionEvent;
|
import com.azure.messaging.eventhubs.models.EventContext;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.unicorn.hgzero.notification.event.event.MeetingCreatedEvent;
|
import com.unicorn.hgzero.notification.event.event.MeetingCreatedEvent;
|
||||||
import com.unicorn.hgzero.notification.event.event.TodoAssignedEvent;
|
import com.unicorn.hgzero.notification.event.event.TodoAssignedEvent;
|
||||||
@ -26,7 +26,7 @@ import java.util.function.Consumer;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class EventHandler implements Consumer<PartitionEvent> {
|
public class EventHandler implements Consumer<EventContext> {
|
||||||
|
|
||||||
private final NotificationService notificationService;
|
private final NotificationService notificationService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
@ -35,13 +35,13 @@ public class EventHandler implements Consumer<PartitionEvent> {
|
|||||||
/**
|
/**
|
||||||
* Event Hub 이벤트 처리
|
* Event Hub 이벤트 처리
|
||||||
*
|
*
|
||||||
* @param partitionEvent Event Hub 파티션 이벤트
|
* @param eventContext Event Hub 이벤트 컨텍스트
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void accept(PartitionEvent partitionEvent) {
|
public void accept(EventContext eventContext) {
|
||||||
try {
|
try {
|
||||||
// 이벤트 데이터 추출
|
// 이벤트 데이터 추출
|
||||||
var eventData = partitionEvent.getData();
|
var eventData = eventContext.getEventData();
|
||||||
|
|
||||||
// 이벤트 속성 추출
|
// 이벤트 속성 추출
|
||||||
Map<String, Object> properties = eventData.getProperties();
|
Map<String, Object> properties = eventData.getProperties();
|
||||||
@ -63,8 +63,7 @@ public class EventHandler implements Consumer<PartitionEvent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 체크포인트 업데이트 (처리 성공 시)
|
// 체크포인트 업데이트 (처리 성공 시)
|
||||||
// TODO: Azure Event Hubs 5.x API에 맞게 체크포인트 업데이트 구현 필요
|
eventContext.updateCheckpoint();
|
||||||
// partitionEvent.getPartitionContext().updateCheckpointAsync().block();
|
|
||||||
log.info("이벤트 처리 완료");
|
log.info("이벤트 처리 완료");
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user