Event Service 개발 환경 구축 및 타입 시스템 개선
주요 변경사항: - UserPrincipal 및 JWT 인증 시스템을 Long에서 UUID로 변경 - Event 엔티티 JPA 설정 최적화 (Lazy loading 및 fetch 전략 개선) - 개발 환경용 DevAuthenticationFilter 추가 (User Service 구현 전까지 임시 사용) - EventServiceApplication 문법 오류 수정 - Hibernate multiple bags 문제 해결 (List를 Set으로 변경) 기술 세부사항: - common/UserPrincipal: Long → UUID 타입 변경, @Builder 어노테이션 추가 - common/JwtTokenProvider: UUID 지원 추가 - event-service/Event: Set 컬렉션 사용, Lazy loading 최적화 - event-service/EventService: Hibernate.initialize()로 컬렉션 초기화 - event-service/EventRepository: fetch join 쿼리 최적화 - event-service/SecurityConfig: DevAuthenticationFilter 통합 테스트 결과: - 모든 Event CRUD API 정상 작동 확인 - PostgreSQL 연결 정상 - 비즈니스 로직 검증 정상 작동 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
55c7b838dd
commit
4d180c2a9f
@ -12,6 +12,7 @@ import javax.crypto.SecretKey;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JWT 토큰 생성 및 검증 제공자
|
* JWT 토큰 생성 및 검증 제공자
|
||||||
@ -55,13 +56,13 @@ public class JwtTokenProvider {
|
|||||||
* @param roles 역할 목록
|
* @param roles 역할 목록
|
||||||
* @return Access Token
|
* @return Access Token
|
||||||
*/
|
*/
|
||||||
public String createAccessToken(Long userId, Long storeId, String email, String name, List<String> roles) {
|
public String createAccessToken(UUID userId, UUID storeId, String email, String name, List<String> roles) {
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
Date expiryDate = new Date(now.getTime() + accessTokenValidityMs);
|
Date expiryDate = new Date(now.getTime() + accessTokenValidityMs);
|
||||||
|
|
||||||
return Jwts.builder()
|
return Jwts.builder()
|
||||||
.subject(userId.toString())
|
.subject(userId.toString())
|
||||||
.claim("storeId", storeId)
|
.claim("storeId", storeId.toString())
|
||||||
.claim("email", email)
|
.claim("email", email)
|
||||||
.claim("name", name)
|
.claim("name", name)
|
||||||
.claim("roles", roles)
|
.claim("roles", roles)
|
||||||
@ -78,7 +79,7 @@ public class JwtTokenProvider {
|
|||||||
* @param userId 사용자 ID
|
* @param userId 사용자 ID
|
||||||
* @return Refresh Token
|
* @return Refresh Token
|
||||||
*/
|
*/
|
||||||
public String createRefreshToken(Long userId) {
|
public String createRefreshToken(UUID userId) {
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
Date expiryDate = new Date(now.getTime() + refreshTokenValidityMs);
|
Date expiryDate = new Date(now.getTime() + refreshTokenValidityMs);
|
||||||
|
|
||||||
@ -97,9 +98,9 @@ public class JwtTokenProvider {
|
|||||||
* @param token JWT 토큰
|
* @param token JWT 토큰
|
||||||
* @return 사용자 ID
|
* @return 사용자 ID
|
||||||
*/
|
*/
|
||||||
public Long getUserIdFromToken(String token) {
|
public UUID getUserIdFromToken(String token) {
|
||||||
Claims claims = parseToken(token);
|
Claims claims = parseToken(token);
|
||||||
return Long.parseLong(claims.getSubject());
|
return UUID.fromString(claims.getSubject());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -111,8 +112,8 @@ public class JwtTokenProvider {
|
|||||||
public UserPrincipal getUserPrincipalFromToken(String token) {
|
public UserPrincipal getUserPrincipalFromToken(String token) {
|
||||||
Claims claims = parseToken(token);
|
Claims claims = parseToken(token);
|
||||||
|
|
||||||
Long userId = Long.parseLong(claims.getSubject());
|
UUID userId = UUID.fromString(claims.getSubject());
|
||||||
Long storeId = claims.get("storeId", Long.class);
|
UUID storeId = UUID.fromString(claims.get("storeId", String.class));
|
||||||
String email = claims.get("email", String.class);
|
String email = claims.get("email", String.class);
|
||||||
String name = claims.get("name", String.class);
|
String name = claims.get("name", String.class);
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.kt.event.common.security;
|
package com.kt.event.common.security;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
@ -8,6 +9,7 @@ import org.springframework.security.core.userdetails.UserDetails;
|
|||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -15,18 +17,19 @@ import java.util.stream.Collectors;
|
|||||||
* JWT 토큰에서 추출한 사용자 정보를 담는 객체
|
* JWT 토큰에서 추출한 사용자 정보를 담는 객체
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
|
@Builder
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class UserPrincipal implements UserDetails {
|
public class UserPrincipal implements UserDetails {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사용자 ID
|
* 사용자 ID
|
||||||
*/
|
*/
|
||||||
private final Long userId;
|
private final UUID userId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 ID
|
* 매장 ID
|
||||||
*/
|
*/
|
||||||
private final Long storeId;
|
private final UUID storeId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사용자 이메일
|
* 사용자 이메일
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.kt.event.eventservice;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
|
||||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
import org.springframework.kafka.annotation.EnableKafka;
|
import org.springframework.kafka.annotation.EnableKafka;
|
||||||
@ -18,10 +19,13 @@ import org.springframework.kafka.annotation.EnableKafka;
|
|||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
* @since 2025-10-23
|
* @since 2025-10-23
|
||||||
*/
|
*/
|
||||||
@SpringBootApplication(scanBasePackages = {
|
@SpringBootApplication(
|
||||||
|
scanBasePackages = {
|
||||||
"com.kt.event.eventservice",
|
"com.kt.event.eventservice",
|
||||||
"com.kt.event.common"
|
"com.kt.event.common"
|
||||||
})
|
},
|
||||||
|
exclude = {UserDetailsServiceAutoConfiguration.class}
|
||||||
|
)
|
||||||
@EnableJpaAuditing
|
@EnableJpaAuditing
|
||||||
@EnableKafka
|
@EnableKafka
|
||||||
@EnableFeignClients
|
@EnableFeignClients
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import com.kt.event.eventservice.domain.enums.EventStatus;
|
|||||||
import com.kt.event.eventservice.domain.repository.EventRepository;
|
import com.kt.event.eventservice.domain.repository.EventRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.hibernate.Hibernate;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -38,20 +39,20 @@ public class EventService {
|
|||||||
/**
|
/**
|
||||||
* 이벤트 생성 (Step 1: 목적 선택)
|
* 이벤트 생성 (Step 1: 목적 선택)
|
||||||
*
|
*
|
||||||
* @param userId 사용자 ID (Long)
|
* @param userId 사용자 ID (UUID)
|
||||||
* @param storeId 매장 ID (Long)
|
* @param storeId 매장 ID (UUID)
|
||||||
* @param request 목적 선택 요청
|
* @param request 목적 선택 요청
|
||||||
* @return 생성된 이벤트 응답
|
* @return 생성된 이벤트 응답
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public EventCreatedResponse createEvent(Long userId, Long storeId, SelectObjectiveRequest request) {
|
public EventCreatedResponse createEvent(UUID userId, UUID storeId, SelectObjectiveRequest request) {
|
||||||
log.info("이벤트 생성 시작 - userId: {}, storeId: {}, objective: {}",
|
log.info("이벤트 생성 시작 - userId: {}, storeId: {}, objective: {}",
|
||||||
userId, storeId, request.getObjective());
|
userId, storeId, request.getObjective());
|
||||||
|
|
||||||
// 이벤트 엔티티 생성 (Long ID를 UUID로 변환)
|
// 이벤트 엔티티 생성
|
||||||
Event event = Event.builder()
|
Event event = Event.builder()
|
||||||
.userId(UUID.nameUUIDFromBytes(("user-" + userId).getBytes()))
|
.userId(userId)
|
||||||
.storeId(UUID.nameUUIDFromBytes(("store-" + storeId).getBytes()))
|
.storeId(storeId)
|
||||||
.objective(request.getObjective())
|
.objective(request.getObjective())
|
||||||
.eventName("") // 초기에는 비어있음, AI 추천 후 설정
|
.eventName("") // 초기에는 비어있음, AI 추천 후 설정
|
||||||
.status(EventStatus.DRAFT)
|
.status(EventStatus.DRAFT)
|
||||||
@ -73,24 +74,28 @@ public class EventService {
|
|||||||
/**
|
/**
|
||||||
* 이벤트 상세 조회
|
* 이벤트 상세 조회
|
||||||
*
|
*
|
||||||
* @param userId 사용자 ID (Long)
|
* @param userId 사용자 ID (UUID)
|
||||||
* @param eventId 이벤트 ID
|
* @param eventId 이벤트 ID
|
||||||
* @return 이벤트 상세 응답
|
* @return 이벤트 상세 응답
|
||||||
*/
|
*/
|
||||||
public EventDetailResponse getEvent(Long userId, UUID eventId) {
|
public EventDetailResponse getEvent(UUID userId, UUID eventId) {
|
||||||
log.info("이벤트 조회 - userId: {}, eventId: {}", userId, eventId);
|
log.info("이벤트 조회 - userId: {}, eventId: {}", userId, eventId);
|
||||||
|
|
||||||
UUID userUuid = UUID.nameUUIDFromBytes(("user-" + userId).getBytes());
|
Event event = eventRepository.findByEventIdAndUserId(eventId, userId)
|
||||||
Event event = eventRepository.findByEventIdAndUserId(eventId, userUuid)
|
|
||||||
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_001));
|
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_001));
|
||||||
|
|
||||||
|
// Lazy 컬렉션 초기화
|
||||||
|
Hibernate.initialize(event.getChannels());
|
||||||
|
Hibernate.initialize(event.getGeneratedImages());
|
||||||
|
Hibernate.initialize(event.getAiRecommendations());
|
||||||
|
|
||||||
return mapToDetailResponse(event);
|
return mapToDetailResponse(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 이벤트 목록 조회 (페이징, 필터링)
|
* 이벤트 목록 조회 (페이징, 필터링)
|
||||||
*
|
*
|
||||||
* @param userId 사용자 ID (Long)
|
* @param userId 사용자 ID (UUID)
|
||||||
* @param status 상태 필터
|
* @param status 상태 필터
|
||||||
* @param search 검색어
|
* @param search 검색어
|
||||||
* @param objective 목적 필터
|
* @param objective 목적 필터
|
||||||
@ -98,7 +103,7 @@ public class EventService {
|
|||||||
* @return 이벤트 목록
|
* @return 이벤트 목록
|
||||||
*/
|
*/
|
||||||
public Page<EventDetailResponse> getEvents(
|
public Page<EventDetailResponse> getEvents(
|
||||||
Long userId,
|
UUID userId,
|
||||||
EventStatus status,
|
EventStatus status,
|
||||||
String search,
|
String search,
|
||||||
String objective,
|
String objective,
|
||||||
@ -107,24 +112,28 @@ public class EventService {
|
|||||||
log.info("이벤트 목록 조회 - userId: {}, status: {}, search: {}, objective: {}",
|
log.info("이벤트 목록 조회 - userId: {}, status: {}, search: {}, objective: {}",
|
||||||
userId, status, search, objective);
|
userId, status, search, objective);
|
||||||
|
|
||||||
UUID userUuid = UUID.nameUUIDFromBytes(("user-" + userId).getBytes());
|
Page<Event> events = eventRepository.findEventsByUser(userId, status, search, objective, pageable);
|
||||||
Page<Event> events = eventRepository.findEventsByUser(userUuid, status, search, objective, pageable);
|
|
||||||
|
|
||||||
return events.map(this::mapToDetailResponse);
|
return events.map(event -> {
|
||||||
|
// Lazy 컬렉션 초기화
|
||||||
|
Hibernate.initialize(event.getChannels());
|
||||||
|
Hibernate.initialize(event.getGeneratedImages());
|
||||||
|
Hibernate.initialize(event.getAiRecommendations());
|
||||||
|
return mapToDetailResponse(event);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 이벤트 삭제
|
* 이벤트 삭제
|
||||||
*
|
*
|
||||||
* @param userId 사용자 ID (Long)
|
* @param userId 사용자 ID (UUID)
|
||||||
* @param eventId 이벤트 ID
|
* @param eventId 이벤트 ID
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deleteEvent(Long userId, UUID eventId) {
|
public void deleteEvent(UUID userId, UUID eventId) {
|
||||||
log.info("이벤트 삭제 - userId: {}, eventId: {}", userId, eventId);
|
log.info("이벤트 삭제 - userId: {}, eventId: {}", userId, eventId);
|
||||||
|
|
||||||
UUID userUuid = UUID.nameUUIDFromBytes(("user-" + userId).getBytes());
|
Event event = eventRepository.findByEventIdAndUserId(eventId, userId)
|
||||||
Event event = eventRepository.findByEventIdAndUserId(eventId, userUuid)
|
|
||||||
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_001));
|
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_001));
|
||||||
|
|
||||||
if (!event.isDeletable()) {
|
if (!event.isDeletable()) {
|
||||||
@ -139,15 +148,14 @@ public class EventService {
|
|||||||
/**
|
/**
|
||||||
* 이벤트 배포
|
* 이벤트 배포
|
||||||
*
|
*
|
||||||
* @param userId 사용자 ID (Long)
|
* @param userId 사용자 ID (UUID)
|
||||||
* @param eventId 이벤트 ID
|
* @param eventId 이벤트 ID
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public void publishEvent(Long userId, UUID eventId) {
|
public void publishEvent(UUID userId, UUID eventId) {
|
||||||
log.info("이벤트 배포 - userId: {}, eventId: {}", userId, eventId);
|
log.info("이벤트 배포 - userId: {}, eventId: {}", userId, eventId);
|
||||||
|
|
||||||
UUID userUuid = UUID.nameUUIDFromBytes(("user-" + userId).getBytes());
|
Event event = eventRepository.findByEventIdAndUserId(eventId, userId)
|
||||||
Event event = eventRepository.findByEventIdAndUserId(eventId, userUuid)
|
|
||||||
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_001));
|
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_001));
|
||||||
|
|
||||||
// 배포 가능 여부 검증 및 상태 변경
|
// 배포 가능 여부 검증 및 상태 변경
|
||||||
@ -161,15 +169,14 @@ public class EventService {
|
|||||||
/**
|
/**
|
||||||
* 이벤트 종료
|
* 이벤트 종료
|
||||||
*
|
*
|
||||||
* @param userId 사용자 ID (Long)
|
* @param userId 사용자 ID (UUID)
|
||||||
* @param eventId 이벤트 ID
|
* @param eventId 이벤트 ID
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public void endEvent(Long userId, UUID eventId) {
|
public void endEvent(UUID userId, UUID eventId) {
|
||||||
log.info("이벤트 종료 - userId: {}, eventId: {}", userId, eventId);
|
log.info("이벤트 종료 - userId: {}, eventId: {}", userId, eventId);
|
||||||
|
|
||||||
UUID userUuid = UUID.nameUUIDFromBytes(("user-" + userId).getBytes());
|
Event event = eventRepository.findByEventIdAndUserId(eventId, userId)
|
||||||
Event event = eventRepository.findByEventIdAndUserId(eventId, userUuid)
|
|
||||||
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_001));
|
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_001));
|
||||||
|
|
||||||
event.end();
|
event.end();
|
||||||
|
|||||||
@ -0,0 +1,53 @@
|
|||||||
|
package com.kt.event.eventservice.config;
|
||||||
|
|
||||||
|
import com.kt.event.common.security.UserPrincipal;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개발 환경용 인증 필터
|
||||||
|
*
|
||||||
|
* User Service가 구현되지 않은 개발 환경에서 테스트를 위해
|
||||||
|
* 기본 UserPrincipal을 자동으로 생성하여 SecurityContext에 설정합니다.
|
||||||
|
*
|
||||||
|
* TODO: 프로덕션 환경에서는 이 필터를 비활성화하고 실제 JWT 인증 필터를 사용해야 합니다.
|
||||||
|
*/
|
||||||
|
public class DevAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
|
||||||
|
// 이미 인증된 경우 스킵
|
||||||
|
if (SecurityContextHolder.getContext().getAuthentication() != null) {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 개발용 기본 UserPrincipal 생성
|
||||||
|
UserPrincipal userPrincipal = new UserPrincipal(
|
||||||
|
UUID.fromString("11111111-1111-1111-1111-111111111111"), // userId
|
||||||
|
UUID.fromString("22222222-2222-2222-2222-222222222222"), // storeId
|
||||||
|
"dev@test.com", // email
|
||||||
|
"개발테스트사용자", // name
|
||||||
|
Collections.singletonList("USER") // roles
|
||||||
|
);
|
||||||
|
|
||||||
|
// Authentication 객체 생성 및 SecurityContext에 설정
|
||||||
|
UsernamePasswordAuthenticationToken authentication =
|
||||||
|
new UsernamePasswordAuthenticationToken(userPrincipal, null, userPrincipal.getAuthorities());
|
||||||
|
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
package com.kt.event.eventservice.config;
|
||||||
|
|
||||||
|
import org.apache.kafka.clients.consumer.ConsumerConfig;
|
||||||
|
import org.apache.kafka.clients.producer.ProducerConfig;
|
||||||
|
import org.apache.kafka.common.serialization.StringDeserializer;
|
||||||
|
import org.apache.kafka.common.serialization.StringSerializer;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.kafka.annotation.EnableKafka;
|
||||||
|
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
|
||||||
|
import org.springframework.kafka.core.*;
|
||||||
|
import org.springframework.kafka.listener.ContainerProperties;
|
||||||
|
import org.springframework.kafka.support.serializer.JsonDeserializer;
|
||||||
|
import org.springframework.kafka.support.serializer.JsonSerializer;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kafka 설정 클래스
|
||||||
|
*
|
||||||
|
* Producer와 Consumer 설정을 정의합니다.
|
||||||
|
* - Producer: event-created 토픽에 이벤트 발행
|
||||||
|
* - Consumer: ai-event-generation-job, image-generation-job 토픽 구독
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableKafka
|
||||||
|
public class KafkaConfig {
|
||||||
|
|
||||||
|
@Value("${spring.kafka.bootstrap-servers}")
|
||||||
|
private String bootstrapServers;
|
||||||
|
|
||||||
|
@Value("${spring.kafka.consumer.group-id}")
|
||||||
|
private String consumerGroupId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kafka Producer 설정
|
||||||
|
*
|
||||||
|
* @return ProducerFactory 인스턴스
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public ProducerFactory<String, Object> producerFactory() {
|
||||||
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
|
||||||
|
config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
|
||||||
|
config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
|
||||||
|
config.put(JsonSerializer.ADD_TYPE_INFO_HEADERS, false);
|
||||||
|
|
||||||
|
// Producer 성능 최적화 설정
|
||||||
|
config.put(ProducerConfig.ACKS_CONFIG, "all");
|
||||||
|
config.put(ProducerConfig.RETRIES_CONFIG, 3);
|
||||||
|
config.put(ProducerConfig.LINGER_MS_CONFIG, 1);
|
||||||
|
config.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "snappy");
|
||||||
|
|
||||||
|
return new DefaultKafkaProducerFactory<>(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KafkaTemplate 빈 생성
|
||||||
|
*
|
||||||
|
* @return KafkaTemplate 인스턴스
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public KafkaTemplate<String, Object> kafkaTemplate() {
|
||||||
|
return new KafkaTemplate<>(producerFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kafka Consumer 설정
|
||||||
|
*
|
||||||
|
* @return ConsumerFactory 인스턴스
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public ConsumerFactory<String, Object> consumerFactory() {
|
||||||
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
|
||||||
|
config.put(ConsumerConfig.GROUP_ID_CONFIG, consumerGroupId);
|
||||||
|
config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
|
||||||
|
config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
|
||||||
|
config.put(JsonDeserializer.TRUSTED_PACKAGES, "*");
|
||||||
|
config.put(JsonDeserializer.USE_TYPE_INFO_HEADERS, false);
|
||||||
|
config.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
|
||||||
|
config.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
|
||||||
|
|
||||||
|
// Consumer 성능 최적화 설정
|
||||||
|
config.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
|
||||||
|
config.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 300000);
|
||||||
|
|
||||||
|
return new DefaultKafkaConsumerFactory<>(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kafka Listener Container Factory 설정
|
||||||
|
*
|
||||||
|
* @return ConcurrentKafkaListenerContainerFactory 인스턴스
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public ConcurrentKafkaListenerContainerFactory<String, Object> kafkaListenerContainerFactory() {
|
||||||
|
ConcurrentKafkaListenerContainerFactory<String, Object> factory =
|
||||||
|
new ConcurrentKafkaListenerContainerFactory<>();
|
||||||
|
factory.setConsumerFactory(consumerFactory());
|
||||||
|
factory.setConcurrency(3); // 동시 처리 스레드 수
|
||||||
|
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
|
||||||
|
return factory;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
package com.kt.event.eventservice.config;
|
||||||
|
|
||||||
|
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.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Security 설정 클래스
|
||||||
|
*
|
||||||
|
* 현재 User Service가 구현되지 않았으므로 임시로 모든 API 접근을 허용합니다.
|
||||||
|
* TODO: User Service 구현 후 JWT 기반 인증/인가 활성화 필요
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Security 필터 체인 설정
|
||||||
|
* - 모든 요청에 대해 인증 없이 접근 허용
|
||||||
|
* - CSRF 보호 비활성화 (개발 환경)
|
||||||
|
*
|
||||||
|
* @param http HttpSecurity 설정 객체
|
||||||
|
* @return SecurityFilterChain 보안 필터 체인
|
||||||
|
* @throws Exception 설정 중 예외 발생 시
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
// CSRF 보호 비활성화 (개발 환경)
|
||||||
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
|
||||||
|
// CORS 설정
|
||||||
|
.cors(AbstractHttpConfigurer::disable)
|
||||||
|
|
||||||
|
// 폼 로그인 비활성화
|
||||||
|
.formLogin(AbstractHttpConfigurer::disable)
|
||||||
|
|
||||||
|
// 로그아웃 비활성화
|
||||||
|
.logout(AbstractHttpConfigurer::disable)
|
||||||
|
|
||||||
|
// HTTP Basic 인증 비활성화
|
||||||
|
.httpBasic(AbstractHttpConfigurer::disable)
|
||||||
|
|
||||||
|
// 세션 관리 - STATELESS (세션 사용 안 함)
|
||||||
|
.sessionManagement(session -> session
|
||||||
|
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 요청 인증 설정
|
||||||
|
.authorizeHttpRequests(authz -> authz
|
||||||
|
// 모든 요청 허용 (개발 환경)
|
||||||
|
.anyRequest().permitAll()
|
||||||
|
)
|
||||||
|
|
||||||
|
// 개발용 인증 필터 추가 (User Service 구현 전까지 임시 사용)
|
||||||
|
.addFilterBefore(new DevAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,12 +4,12 @@ import com.kt.event.common.entity.BaseTimeEntity;
|
|||||||
import com.kt.event.eventservice.domain.enums.EventStatus;
|
import com.kt.event.eventservice.domain.enums.EventStatus;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
import org.hibernate.annotations.Fetch;
|
||||||
|
import org.hibernate.annotations.FetchMode;
|
||||||
import org.hibernate.annotations.GenericGenerator;
|
import org.hibernate.annotations.GenericGenerator;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 이벤트 엔티티
|
* 이벤트 엔티티
|
||||||
@ -69,22 +69,23 @@ public class Event extends BaseTimeEntity {
|
|||||||
@Column(name = "selected_image_url", length = 500)
|
@Column(name = "selected_image_url", length = 500)
|
||||||
private String selectedImageUrl;
|
private String selectedImageUrl;
|
||||||
|
|
||||||
@ElementCollection
|
@ElementCollection(fetch = FetchType.LAZY)
|
||||||
@CollectionTable(
|
@CollectionTable(
|
||||||
name = "event_channels",
|
name = "event_channels",
|
||||||
joinColumns = @JoinColumn(name = "event_id")
|
joinColumns = @JoinColumn(name = "event_id")
|
||||||
)
|
)
|
||||||
@Column(name = "channel", length = 50)
|
@Column(name = "channel", length = 50)
|
||||||
|
@Fetch(FetchMode.SUBSELECT)
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private List<String> channels = new ArrayList<>();
|
private List<String> channels = new ArrayList<>();
|
||||||
|
|
||||||
@OneToMany(mappedBy = "event", cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToMany(mappedBy = "event", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private List<GeneratedImage> generatedImages = new ArrayList<>();
|
private Set<GeneratedImage> generatedImages = new HashSet<>();
|
||||||
|
|
||||||
@OneToMany(mappedBy = "event", cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToMany(mappedBy = "event", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private List<AiRecommendation> aiRecommendations = new ArrayList<>();
|
private Set<AiRecommendation> aiRecommendations = new HashSet<>();
|
||||||
|
|
||||||
// ==== 비즈니스 로직 ==== //
|
// ==== 비즈니스 로직 ==== //
|
||||||
|
|
||||||
|
|||||||
@ -25,9 +25,8 @@ public interface EventRepository extends JpaRepository<Event, UUID> {
|
|||||||
/**
|
/**
|
||||||
* 사용자 ID와 이벤트 ID로 조회
|
* 사용자 ID와 이벤트 ID로 조회
|
||||||
*/
|
*/
|
||||||
@Query("SELECT e FROM Event e " +
|
@Query("SELECT DISTINCT e FROM Event e " +
|
||||||
"LEFT JOIN FETCH e.generatedImages " +
|
"LEFT JOIN FETCH e.channels " +
|
||||||
"LEFT JOIN FETCH e.aiRecommendations " +
|
|
||||||
"WHERE e.eventId = :eventId AND e.userId = :userId")
|
"WHERE e.eventId = :eventId AND e.userId = :userId")
|
||||||
Optional<Event> findByEventIdAndUserId(
|
Optional<Event> findByEventIdAndUserId(
|
||||||
@Param("eventId") UUID eventId,
|
@Param("eventId") UUID eventId,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user