diff --git a/src/main/java/com/midas/shootpointer/domain/highlight/business/HighlightManager.java b/src/main/java/com/midas/shootpointer/domain/highlight/business/HighlightManager.java index ce61a103..bc2cfdac 100644 --- a/src/main/java/com/midas/shootpointer/domain/highlight/business/HighlightManager.java +++ b/src/main/java/com/midas/shootpointer/domain/highlight/business/HighlightManager.java @@ -122,7 +122,7 @@ public HighlightCalendarResponse fetchCalendar(int year, int month,UUID memberId return new HighlightCalendarResponse(year,month,daysResponses); } - public List fetchLatestCreatedHighlights(UUID jobId,UUID memberId){ + public List fetchLatestCreatedHighlights(String jobId,UUID memberId){ List highlightList=highlightHelper.fetchLastestCreatedHighlights(jobId,memberId); return highlightList.stream() diff --git a/src/main/java/com/midas/shootpointer/domain/highlight/controller/HighlightQueryController.java b/src/main/java/com/midas/shootpointer/domain/highlight/controller/HighlightQueryController.java index c352ade2..6b726343 100644 --- a/src/main/java/com/midas/shootpointer/domain/highlight/controller/HighlightQueryController.java +++ b/src/main/java/com/midas/shootpointer/domain/highlight/controller/HighlightQueryController.java @@ -57,7 +57,7 @@ public ResponseEntity> fetchCalendar( @GetMapping("/latest") public ResponseEntity>> latestCreatedHighlights( - @RequestParam(value = "jobId") UUID jobId + @RequestParam(value = "jobId") String jobId ){ UUID memberId=SecurityUtils.getCurrentMemberId(); return ResponseEntity.ok(ApiResponse.ok(manager.fetchLatestCreatedHighlights(jobId,memberId))); diff --git a/src/main/java/com/midas/shootpointer/domain/highlight/dto/HighlightRequest.java b/src/main/java/com/midas/shootpointer/domain/highlight/dto/HighlightRequest.java index 327ceb03..87823a27 100644 --- a/src/main/java/com/midas/shootpointer/domain/highlight/dto/HighlightRequest.java +++ b/src/main/java/com/midas/shootpointer/domain/highlight/dto/HighlightRequest.java @@ -29,5 +29,5 @@ public class HighlightRequest { private LocalDateTime createdAt; @NotNull(message = "하이라이트 JobId는 필수입니다.") - private UUID jobId; + private String jobId; } diff --git a/src/main/java/com/midas/shootpointer/domain/highlight/entity/HighlightEntity.java b/src/main/java/com/midas/shootpointer/domain/highlight/entity/HighlightEntity.java index 6557b6b3..a8bdd3c8 100644 --- a/src/main/java/com/midas/shootpointer/domain/highlight/entity/HighlightEntity.java +++ b/src/main/java/com/midas/shootpointer/domain/highlight/entity/HighlightEntity.java @@ -50,8 +50,8 @@ public class HighlightEntity extends BaseEntity { @Column(name = "video_created_at") private LocalDateTime videoCreatedAt; - @Column(name = "job_id",columnDefinition = "uuid") - private UUID jobId; + @Column(name = "job_id") + private String jobId; /* =========== [ 도메인-행위 ] ============== diff --git a/src/main/java/com/midas/shootpointer/domain/highlight/helper/HighlightHelperImpl.java b/src/main/java/com/midas/shootpointer/domain/highlight/helper/HighlightHelperImpl.java index 8922933c..691d4f1a 100644 --- a/src/main/java/com/midas/shootpointer/domain/highlight/helper/HighlightHelperImpl.java +++ b/src/main/java/com/midas/shootpointer/domain/highlight/helper/HighlightHelperImpl.java @@ -68,7 +68,7 @@ public DateTimeRange getMonthDateTimeRange(int year, int month) { } @Override - public List fetchLastestCreatedHighlights(UUID jobId, UUID memberId) { + public List fetchLastestCreatedHighlights(String jobId, UUID memberId) { return highlightUtil.fetchLastestCreatedHighlights(jobId,memberId); } diff --git a/src/main/java/com/midas/shootpointer/domain/highlight/helper/HighlightUtil.java b/src/main/java/com/midas/shootpointer/domain/highlight/helper/HighlightUtil.java index 2fa9e856..ceef6992 100644 --- a/src/main/java/com/midas/shootpointer/domain/highlight/helper/HighlightUtil.java +++ b/src/main/java/com/midas/shootpointer/domain/highlight/helper/HighlightUtil.java @@ -24,5 +24,5 @@ public interface HighlightUtil { TreeMap> groupingHighlights(List flatHighlightList); List fetchFlatHighlightList(int year,int month,UUID memberId); DateTimeRange getMonthDateTimeRange(int year, int month); - List fetchLastestCreatedHighlights(UUID jobId,UUID memberId); + List fetchLastestCreatedHighlights(String jobId,UUID memberId); } diff --git a/src/main/java/com/midas/shootpointer/domain/highlight/helper/HighlightUtilImpl.java b/src/main/java/com/midas/shootpointer/domain/highlight/helper/HighlightUtilImpl.java index efe4862f..6d540d77 100644 --- a/src/main/java/com/midas/shootpointer/domain/highlight/helper/HighlightUtilImpl.java +++ b/src/main/java/com/midas/shootpointer/domain/highlight/helper/HighlightUtilImpl.java @@ -164,7 +164,7 @@ public DateTimeRange getMonthDateTimeRange(int year, int month) { } @Override - public List fetchLastestCreatedHighlights(UUID jobId, UUID memberId) { + public List fetchLastestCreatedHighlights(String jobId, UUID memberId) { return highlightQueryRepository.fetchHighlightsByJobId(jobId,memberId); } diff --git a/src/main/java/com/midas/shootpointer/domain/highlight/mapper/HighlightFactory.java b/src/main/java/com/midas/shootpointer/domain/highlight/mapper/HighlightFactory.java index ad8ea67e..dcfdc13e 100644 --- a/src/main/java/com/midas/shootpointer/domain/highlight/mapper/HighlightFactory.java +++ b/src/main/java/com/midas/shootpointer/domain/highlight/mapper/HighlightFactory.java @@ -22,7 +22,7 @@ public List createHighlightEntities(List highlig Member member, BackNumberEntity backNumber, LocalDateTime createAt, - UUID jobId + String jobId ){ return highlightInfos.stream() .map(info -> HighlightEntity.builder() diff --git a/src/main/java/com/midas/shootpointer/domain/highlight/repository/HighlightQueryRepository.java b/src/main/java/com/midas/shootpointer/domain/highlight/repository/HighlightQueryRepository.java index 62496f1a..1ed8d140 100644 --- a/src/main/java/com/midas/shootpointer/domain/highlight/repository/HighlightQueryRepository.java +++ b/src/main/java/com/midas/shootpointer/domain/highlight/repository/HighlightQueryRepository.java @@ -107,15 +107,15 @@ SELECT EXISTS( @Query(value = """ - SELECT * + SELECT h.* FROM highlight AS h INNER JOIN member AS m ON h.member_id = :memberId WHERE - h.job_id = :jobId + h.job_id = :jobId AND m.member_id =:memberId """,nativeQuery = true) - List fetchHighlightsByJobId(UUID jobId,UUID memberId); + List fetchHighlightsByJobId(String jobId,UUID memberId); /** * =========================== *

diff --git a/src/main/java/com/midas/shootpointer/domain/like/helper/LikeUtilImpl.java b/src/main/java/com/midas/shootpointer/domain/like/helper/LikeUtilImpl.java index 56f42897..5384bedb 100644 --- a/src/main/java/com/midas/shootpointer/domain/like/helper/LikeUtilImpl.java +++ b/src/main/java/com/midas/shootpointer/domain/like/helper/LikeUtilImpl.java @@ -5,17 +5,11 @@ import com.midas.shootpointer.domain.like.repository.LikeQueryRepository; import com.midas.shootpointer.domain.member.entity.Member; import com.midas.shootpointer.domain.post.entity.PostEntity; -import com.midas.shootpointer.domain.post.repository.PostCommandRepository; -import com.midas.shootpointer.domain.post.repository.PostQueryRepository; -import com.midas.shootpointer.global.annotation.DistributedLock; import com.midas.shootpointer.global.common.ErrorCode; import com.midas.shootpointer.global.exception.CustomException; -import jakarta.persistence.OptimisticLockException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; import java.util.UUID; diff --git a/src/main/java/com/midas/shootpointer/domain/like/repository/LikeQueryRepository.java b/src/main/java/com/midas/shootpointer/domain/like/repository/LikeQueryRepository.java index d9777eed..f24ad41e 100644 --- a/src/main/java/com/midas/shootpointer/domain/like/repository/LikeQueryRepository.java +++ b/src/main/java/com/midas/shootpointer/domain/like/repository/LikeQueryRepository.java @@ -18,4 +18,12 @@ public interface LikeQueryRepository extends JpaRepository { nativeQuery = true ) Optional findByPostIdAndMemberId(@Param("postId") Long postId,@Param("memberId") UUID memberId); + + + @Query(value = """ + SELECT 1 FROM like_table AS l + WHERE l.member_id = :memberId + AND l.post_id = :postId + """,nativeQuery = true) + Boolean isLiked(UUID memberId,Long postId); } diff --git a/src/main/java/com/midas/shootpointer/domain/post/dto/response/PostResponse.java b/src/main/java/com/midas/shootpointer/domain/post/dto/response/PostResponse.java index 7e49d0ae..7ed729e8 100644 --- a/src/main/java/com/midas/shootpointer/domain/post/dto/response/PostResponse.java +++ b/src/main/java/com/midas/shootpointer/domain/post/dto/response/PostResponse.java @@ -2,10 +2,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import java.time.LocalDateTime; @@ -33,6 +30,10 @@ public class PostResponse { //좋아요 개수 private Long likeCnt; + //좋아요 여부 + @Setter + private boolean isLiked=false; + //게시 시간 @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Seoul") private LocalDateTime createdAt; diff --git a/src/main/java/com/midas/shootpointer/domain/progress/controller/ProgressController.java b/src/main/java/com/midas/shootpointer/domain/progress/controller/ProgressController.java index c98c88c2..699ccba7 100644 --- a/src/main/java/com/midas/shootpointer/domain/progress/controller/ProgressController.java +++ b/src/main/java/com/midas/shootpointer/domain/progress/controller/ProgressController.java @@ -3,9 +3,11 @@ import com.midas.shootpointer.domain.progress.service.ProgressSseEmitter; import com.midas.shootpointer.global.security.SecurityUtils; import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import java.util.Map; import java.util.UUID; @RestController @@ -14,7 +16,7 @@ public class ProgressController { private final ProgressSseEmitter progressSseEmitter; - @GetMapping("/subscribe") + @GetMapping(value = "/subscribe",produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter subscribe( @RequestHeader(value = "Last-Event-ID", required = false, defaultValue = "") final String lastEventId, @RequestParam String jobId @@ -22,4 +24,14 @@ public SseEmitter subscribe( UUID memberId = SecurityUtils.getCurrentMemberId(); return progressSseEmitter.createEmitter(memberId.toString(),lastEventId,jobId); } + + @GetMapping + public Object progress(@RequestParam String jobId) { + Object data=progressSseEmitter.getLatestProgress(jobId); + + if (data==null){ + return Map.of("type","NONE","progress",0); + } + return data; + } } diff --git a/src/main/java/com/midas/shootpointer/domain/progress/dto/SseEvent.java b/src/main/java/com/midas/shootpointer/domain/progress/dto/SseEvent.java index 93e1005c..54dc86e5 100644 --- a/src/main/java/com/midas/shootpointer/domain/progress/dto/SseEvent.java +++ b/src/main/java/com/midas/shootpointer/domain/progress/dto/SseEvent.java @@ -4,7 +4,6 @@ public record SseEvent( @NotNull long eventId, - @NotNull String name, @NotNull Object data ) { } diff --git a/src/main/java/com/midas/shootpointer/domain/progress/service/ProgressSseEmitter.java b/src/main/java/com/midas/shootpointer/domain/progress/service/ProgressSseEmitter.java index 1aaf784c..56da096d 100644 --- a/src/main/java/com/midas/shootpointer/domain/progress/service/ProgressSseEmitter.java +++ b/src/main/java/com/midas/shootpointer/domain/progress/service/ProgressSseEmitter.java @@ -12,9 +12,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.time.Instant; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; @Component @@ -24,16 +22,13 @@ public class ProgressSseEmitter { @Value("${sse.ttl}") private long ttlMillis; - @Value("${sse.event-name}") - private String name; - //유저당 보관 가능 최대 이벤트 수 : 360(5초당 이벤트 발행 -> 30분 : 360개) @Value("${sse.cache-max-size}") private int cacheMaxSize; @PostConstruct private void init(){ - log.info("ProgressSseEmitter start : {} {} {}",ttlMillis,name,cacheMaxSize); + log.info("ProgressSseEmitter start : {} {}",ttlMillis,cacheMaxSize); } //memberId -> emitter private static Map emitters=new ConcurrentHashMap<>(); @@ -41,6 +36,7 @@ private void init(){ //memberId -> 최근 이벤트 (오름차순 : 맨 뒤가 최신) private static Map> eventCache=new ConcurrentHashMap<>(); + private static final Map latestProgressMap=new ConcurrentHashMap<>(); /** * 구독 생성 * @param memberId : 멤버 Id @@ -49,8 +45,22 @@ private void init(){ public SseEmitter createEmitter(String memberId, String lastEventId, String jobId){ SseEmitter emitter=new SseEmitter(ttlMillis); String sseKey=buildKey(memberId,jobId); + log.info("[SSE-createEmitter] key : {} / memberId = {} / jobId = {} ",sseKey,memberId,jobId); emitters.put(sseKey,emitter); + // 연결 확인용 초기 이벤트 전송 + try { + emitter.send(SseEmitter.event() + .data(Map.of( + "type", "CONNECTED", + "jobId", jobId, + "timestamp", Instant.now().toEpochMilli() + ))); + log.info("SSE initial event sent: {}", sseKey); + } catch (Exception e) { + log.warn("Failed to send initial event: {}", e.getMessage()); + } + emitter.onCompletion(()-> { emitters.remove(sseKey); log.debug("SSE completed and removed {}",sseKey); @@ -90,9 +100,11 @@ public SseEmitter createEmitter(String memberId, String lastEventId, String jobI public void sendToClient(String jobId,String memberId,Object data){ //Event Id는 TimeMillis() 사용 String sseKey=buildKey(memberId,jobId); + log.info("[SSE-sendToClient] key : {} / memberId = {} / jobId = {} ",sseKey,memberId,jobId); long eventId= Instant.now().toEpochMilli(); - SseEvent event=new SseEvent(eventId,name,data); + SseEvent event=new SseEvent(eventId,data); + latestProgressMap.put(jobId,data);//REST API 통신을 위한 임시 메서드 /** * 1. 캐시에 저장. */ @@ -121,28 +133,37 @@ public void sendToClient(String jobId,String memberId,Object data){ * cache 정리 */ @Scheduled(fixedRateString = "${sse.clean-up-interval}") - public void cleanUp(){ - long expireBefore=Instant.now().toEpochMilli()-ttlMillis; - eventCache.forEach((memberId,deque)->{ - synchronized (deque){ - while (!deque.isEmpty() && deque.peekFirst().eventId() < expireBefore){ - deque.removeFirst(); - } - if (deque.isEmpty() && !emitters.containsKey(memberId)){ - eventCache.remove(memberId); - } + public void cleanUp() { + long expireBefore = Instant.now().toEpochMilli() - ttlMillis; + + // 삭제할 키 임시 보관 + List keysToRemove = new ArrayList<>(); + + eventCache.forEach((key, deque) -> { + synchronized (deque) { + // 오래된 이벤트 정리 + while (!deque.isEmpty() && deque.peekFirst().eventId() < expireBefore) { + deque.removeFirst(); } - }); - } + + // emitter도 없고 deque도 비었으면 지우기 대상 + if (deque.isEmpty() && !emitters.containsKey(key)) { + keysToRemove.add(key); + } + } + }); + + // forEach 종료 후 삭제 + keysToRemove.forEach(eventCache::remove); +} private void sendToEvent(SseEmitter emitter,SseEvent event){ try { emitter.send(SseEmitter.event() .id(String.valueOf(event.eventId())) - .name(event.name()) .data(event.data())); } catch (Exception e){ - log.warn("Failed to send SSE event id = {} name = {} message = {}",event.eventId(),event.name(),e.getMessage()); + log.warn("Failed to send SSE event id = {} message = {}",event.eventId(),e.getMessage()); emitter.complete(); } } @@ -151,4 +172,7 @@ private String buildKey(String memberId,String jobId){ return String.format("%s:%s",memberId,jobId); } + public Object getLatestProgress(String jobId) { + return latestProgressMap.get(jobId); + } } \ No newline at end of file diff --git a/src/main/java/com/midas/shootpointer/infrastructure/redis/config/RedisOpenCVConfig.java b/src/main/java/com/midas/shootpointer/infrastructure/redis/config/RedisOpenCVConfig.java index dc8d78d9..d5891cd2 100644 --- a/src/main/java/com/midas/shootpointer/infrastructure/redis/config/RedisOpenCVConfig.java +++ b/src/main/java/com/midas/shootpointer/infrastructure/redis/config/RedisOpenCVConfig.java @@ -5,64 +5,131 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.midas.shootpointer.infrastructure.redis.subscriber.ProgressSubscriber; +import io.lettuce.core.ClientOptions; +import io.lettuce.core.SocketOptions; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.PatternTopic; import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.data.redis.connection.RedisPassword; +import java.time.Duration; + +@Slf4j @Configuration @RequiredArgsConstructor -@Profile("prod") //opencv 환경에서만 실행 +@Profile("prod") public class RedisOpenCVConfig { - @Value("${spring.data.redis.opencv.host}") + + @Value("${spring.data.redis.opencv.host:localhost}") private String host; - @Value("${spring.data.redis.opencv.port}") + @Value("${spring.data.redis.opencv.port:6379}") private int port; - @Value("${spring.data.redis.opencv.channels.highlight}") + @Value("${spring.data.redis.opencv.password}") + private String password; + + @Value("${spring.data.redis.opencv.channels.highlight:opencv-progress-highlight}") private String highlightChannel; - @Value("${spring.data.redis.opencv.channels.upload}") + @Value("${spring.data.redis.opencv.channels.upload:opencv-progress-upload}") private String uploadChannel; - /** - * 채널 빈 생성 - */ - @Bean - public ChannelTopic uploadChannelTopic(){ - return new ChannelTopic(uploadChannel); - } - - @Bean - public ChannelTopic highlightChannelTopic(){ - return new ChannelTopic(highlightChannel); + @PostConstruct + public void init() { + log.info("╔════════════════════════════════════════════════════════╗"); + log.info("║ RedisOpenCVConfig Initialization ║"); + log.info("╠════════════════════════════════════════════════════════╣"); + log.info("║ Host: {}:{}", host, port); + log.info("║ Upload Channel: {}:*", uploadChannel); + log.info("║ Highlight Channel: {}:*", highlightChannel); + log.info("╚════════════════════════════════════════════════════════╝"); } @Bean(name = "opencvRedisConnectionFactory") - public RedisConnectionFactory opencvRedisConnectionFactory(){ - RedisStandaloneConfiguration configuration=new RedisStandaloneConfiguration(); - configuration.setHostName(host); - configuration.setPort(port); - - return new LettuceConnectionFactory(configuration); + public RedisConnectionFactory opencvRedisConnectionFactory() { + log.info("[Redis OpenCV] Creating connection factory"); + + try { + // Redis 서버 설정 + RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(); + configuration.setHostName(host); + configuration.setPort(port); + configuration.setPassword(RedisPassword.of(password)); + + // Lettuce 클라이언트 설정 (타임아웃, 재연결 등) + SocketOptions socketOptions = SocketOptions.builder() + .connectTimeout(Duration.ofSeconds(10)) + .keepAlive(true) + .build(); + + ClientOptions clientOptions = ClientOptions.builder() + .socketOptions(socketOptions) + .autoReconnect(true) + .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS) + .build(); + + LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() + .clientOptions(clientOptions) + .commandTimeout(Duration.ofSeconds(10)) + .build(); + + LettuceConnectionFactory factory = new LettuceConnectionFactory(configuration, clientConfig); + + // CRITICAL: afterPropertiesSet() 호출 필수! + factory.afterPropertiesSet(); + + log.info("[Redis OpenCV] Connection factory created"); + + // 연결 테스트 - 명시적으로 수행 + log.info("[Redis OpenCV] Testing connection to {}:{}...", host, port); + + try (RedisConnection connection = factory.getConnection()) { + String pingResult = connection.ping(); + log.info("[Redis OpenCV] ✓ Connection test SUCCESS: {}", pingResult); + log.info("[Redis OpenCV] ✓ Redis server is reachable and responding"); + } catch (Exception e) { + log.error("[Redis OpenCV] ✗ Connection test FAILED", e); + log.error("[Redis OpenCV] Error details: {}", e.getMessage()); + log.error("[Redis OpenCV] Please check:"); + log.error("[Redis OpenCV] 1. Redis server is running on {}:{}", host, port); + log.error("[Redis OpenCV] 2. Network connectivity (nc -zv {} {})", host, port); + log.error("[Redis OpenCV] 3. Redis bind configuration (should be 0.0.0.0)"); + log.error("[Redis OpenCV] 4. Firewall allows port {}", port); + throw new RuntimeException("Failed to connect to OpenCV Redis server", e); + } + + return factory; + + } catch (Exception e) { + log.error("[Redis OpenCV] Failed to create connection factory", e); + throw new RuntimeException("Failed to initialize OpenCV Redis connection", e); + } } @Bean(name = "opencvRedisTemplate") - public RedisTemplate opencvRedisTemplate( + public RedisTemplate opencvRedisTemplate( @Qualifier("opencvRedisConnectionFactory") RedisConnectionFactory connectionFactory - ){ - RedisTemplate template=new RedisTemplate<>(); + ) { + log.info("[Redis OpenCV] Creating RedisTemplate..."); + + RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); ObjectMapper objectMapper = new ObjectMapper() @@ -73,34 +140,54 @@ public RedisTemplate opencvRedisTemplate( template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)); + + template.afterPropertiesSet(); + log.info("[Redis OpenCV] ✓ RedisTemplate created successfully"); return template; } - //Redis 메시지 수진하고 리스너에 전달 컨테이너 @Bean - public RedisMessageListenerContainer redisMessage( + public RedisMessageListenerContainer redisMessageListenerContainer( @Qualifier("opencvRedisConnectionFactory") RedisConnectionFactory connectionFactory, ProgressSubscriber progressSubscriber - ){ - /** - * 1. Redis 연결 컨테이너 생성 - */ - RedisMessageListenerContainer container=new RedisMessageListenerContainer(); - - /** - * 2. 연결 설정 주입 - */ - container.setConnectionFactory(connectionFactory); - - /** - * 3. 채널로 구독자 등록 - */ - //원본 영상 업로드 채널 구독 - container.addMessageListener(progressSubscriber,uploadChannelTopic()); - //하이라이트 영상 채널 구독 - container.addMessageListener(progressSubscriber,highlightChannelTopic()); - - return container; + ) { + log.info("[Redis OpenCV] Creating message listener container..."); + + try { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + + // 리스너 어댑터 생성 + MessageListenerAdapter listenerAdapter = new MessageListenerAdapter(progressSubscriber); + listenerAdapter.afterPropertiesSet(); + + // Upload 채널 패턴 구독 + String uploadPattern = uploadChannel + ":*"; + PatternTopic uploadTopic = new PatternTopic(uploadPattern); + container.addMessageListener(listenerAdapter, uploadTopic); + log.info("[Redis OpenCV] ✓ Registered listener for pattern: {}", uploadPattern); + + // Highlight 채널 패턴 구독 + String highlightPattern = highlightChannel + ":*"; + PatternTopic highlightTopic = new PatternTopic(highlightPattern); + container.addMessageListener(listenerAdapter, highlightTopic); + + log.info("[Redis OpenCV] ✓ Registered listener for pattern: {}", highlightPattern); + log.info("[Redis OpenCV] ✓ Message listener container started successfully"); + log.info("[Redis OpenCV] ✓ Now listening for messages from OpenCV server..."); + log.info("[Redis OpenCV] ✓ Subscriptions active:"); + log.info("[Redis OpenCV] - {}", uploadPattern); + log.info("[Redis OpenCV] - {}", highlightPattern); + + return container; + + } catch (Exception e) { + log.error("[Redis OpenCV] ✗ Failed to create message listener container", e); + log.error("[Redis OpenCV] Error details: {}", e.getMessage()); + throw new RuntimeException("Failed to initialize Redis message listener", e); + } } } diff --git a/src/main/java/com/midas/shootpointer/infrastructure/redis/subscriber/ProgressSubscriber.java b/src/main/java/com/midas/shootpointer/infrastructure/redis/subscriber/ProgressSubscriber.java index f9a572cc..9e4128eb 100644 --- a/src/main/java/com/midas/shootpointer/infrastructure/redis/subscriber/ProgressSubscriber.java +++ b/src/main/java/com/midas/shootpointer/infrastructure/redis/subscriber/ProgressSubscriber.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.midas.shootpointer.domain.progress.dto.ProgressRedisResponse; +import com.midas.shootpointer.domain.progress.dto.ProgressResponse; import com.midas.shootpointer.domain.progress.service.ProgressSseEmitter; import com.midas.shootpointer.infrastructure.redis.helper.ProgressValidator; import lombok.RequiredArgsConstructor; @@ -33,7 +34,7 @@ public void onMessage(Message message, byte[] pattern) { log.info("[Redis SUB] Received message : {}",body); String[] tokens = channel.split(":"); - String jobIdFromChannel = tokens.length >= 3 ? tokens[2] : null; // 2: "opencv-progress-upload", 3: jobId + String jobIdFromChannel = tokens[1]; log.info("[Redis SUB] channel={} jobId={} body={}", channel, jobIdFromChannel, body); @@ -64,11 +65,18 @@ public void onMessage(Message message, byte[] pattern) { //SUB로 받은 값 null 검증 validator.validate(progress); + ProgressResponse response=new ProgressResponse( + progress.status(), + progress.success(), + progress.progress(), + progress.type() + ); + //SSE로 client에 전달 emitter.sendToClient( - progress.memberId(), jobIdFromChannel, //redis에서 구독한 jobId로 SSE 발행 - progress + progress.memberId(), + response ); log.info("[Redis SUB] progress info : jobId = {}",progress.jobId()); diff --git a/src/main/java/com/midas/shootpointer/test/SetRealPostDataLoader.java b/src/main/java/com/midas/shootpointer/test/SetRealPostDataLoader.java index ca85deee..8b18bfb8 100644 --- a/src/main/java/com/midas/shootpointer/test/SetRealPostDataLoader.java +++ b/src/main/java/com/midas/shootpointer/test/SetRealPostDataLoader.java @@ -2,6 +2,7 @@ import com.midas.shootpointer.domain.backnumber.entity.BackNumber; import com.midas.shootpointer.domain.backnumber.entity.BackNumberEntity; +import com.midas.shootpointer.domain.backnumber.helper.BackNumberHelper; import com.midas.shootpointer.domain.backnumber.repository.BackNumberRepository; import com.midas.shootpointer.domain.highlight.entity.HighlightEntity; import com.midas.shootpointer.domain.highlight.repository.HighlightCommandRepository; @@ -12,7 +13,10 @@ import com.midas.shootpointer.domain.memberbacknumber.entity.MemberBackNumberEntity; import com.midas.shootpointer.domain.memberbacknumber.repository.MemberBackNumberRepository; import com.midas.shootpointer.domain.post.entity.HashTag; +import com.midas.shootpointer.domain.post.entity.PostDocument; import com.midas.shootpointer.domain.post.entity.PostEntity; +import com.midas.shootpointer.domain.post.mapper.PostElasticSearchMapper; +import com.midas.shootpointer.domain.post.repository.PostElasticSearchRepository; import com.midas.shootpointer.domain.post.repository.PostQueryRepository; import com.midas.shootpointer.test.BasketballPostDataGenerator.PostData; import lombok.RequiredArgsConstructor; @@ -46,9 +50,10 @@ public class SetRealPostDataLoader implements CommandLineRunner { private final MemberCommandRepository memberRepository; private final HighlightCommandRepository highlightCommandRepository; private final PostQueryRepository postQueryRepository; - //private final PostElasticSearchMapper mapper; - //private final PostElasticSearchRepository postElasticSearchRepository; + private final PostElasticSearchMapper mapper; + private final PostElasticSearchRepository postElasticSearchRepository; private final BackNumberRepository backNumberRepository; + private final BackNumberHelper backNumberHelper; private final MemberBackNumberRepository memberBackNumberRepository; private final LikeCommandRepository likeCommandRepository; //30개 @@ -113,11 +118,12 @@ public void run(String... args) throws Exception { Map memberBackNumberMap = new HashMap<>(); for (Member m : memberList) { - BackNumberEntity bn = backNumberRepository.save( - BackNumberEntity.builder() - .backNumber(BackNumber.of(random.nextInt(1, 99))) - .build() - ); + Integer backNumberValue=random.nextInt(1,99); + BackNumber backNumber=BackNumber.of(backNumberValue); + + //중복 방지 + BackNumberEntity bn = backNumberHelper.findOrElseGetBackNumber(backNumber); + memberBackNumberRepository.save(MemberBackNumberEntity.of(m, bn)); @@ -221,12 +227,12 @@ public void run(String... args) throws Exception { List repositoryAll = postQueryRepository.findAllWithMemberAndHighlight(); // PostEntity → PostDocument 변환 - /*List docs = repositoryAll.stream() + List docs = repositoryAll.stream() .map(mapper::entityToDoc) .toList(); postElasticSearchRepository.saveAll(docs); - System.out.println("ES - 삽입 완료");*/ + System.out.println("ES - 삽입 완료"); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c1f80b4d..124f7e50 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -83,6 +83,7 @@ spring: opencv: host: ${OPENCV_REDIS_HOST:localhost} port: ${OPENCV_REDIS_PORT:6379} + password: ${OPENCV_REDIS_PASSWORD:} channels: upload: opencv-progress-upload:* highlight: opencv-progress-highlight:* diff --git a/src/test/java/com/midas/shootpointer/domain/highlight/mapper/HighlightFactoryTest.java b/src/test/java/com/midas/shootpointer/domain/highlight/mapper/HighlightFactoryTest.java index 701a4a94..0378be7f 100644 --- a/src/test/java/com/midas/shootpointer/domain/highlight/mapper/HighlightFactoryTest.java +++ b/src/test/java/com/midas/shootpointer/domain/highlight/mapper/HighlightFactoryTest.java @@ -49,7 +49,7 @@ void createHighlightEntities(){ .backNumber(BackNumber.of(10)) .build(); LocalDateTime now=LocalDateTime.now(); - UUID jobId=UUID.randomUUID(); + String jobId=UUID.randomUUID().toString(); //when List result=factory.createHighlightEntities(highlightInfos,highlightKey,member,backNumber,now,jobId);