From 09426c7d9a411bb6fd1381a3ab35eb4e11e9f6cf Mon Sep 17 00:00:00 2001 From: tkv00 Date: Wed, 19 Nov 2025 22:02:57 +0900 Subject: [PATCH 01/18] =?UTF-8?q?refact=20:=20Progress=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20response=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../redis/subscriber/ProgressSubscriber.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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..5c4fe50a 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; @@ -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 + response ); log.info("[Redis SUB] progress info : jobId = {}",progress.jobId()); From 1fed66000a938ad937b3d9ec50d403a86c901360 Mon Sep 17 00:00:00 2001 From: tkv00 Date: Wed, 19 Nov 2025 23:51:08 +0900 Subject: [PATCH 02/18] =?UTF-8?q?refact=20:=20jobId=20UUID=20->=20String?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/highlight/business/HighlightManager.java | 2 +- .../domain/highlight/controller/HighlightQueryController.java | 2 +- .../shootpointer/domain/highlight/dto/HighlightRequest.java | 2 +- .../shootpointer/domain/highlight/entity/HighlightEntity.java | 4 ++-- .../domain/highlight/helper/HighlightHelperImpl.java | 2 +- .../shootpointer/domain/highlight/helper/HighlightUtil.java | 2 +- .../domain/highlight/mapper/HighlightFactory.java | 2 +- .../domain/highlight/repository/HighlightQueryRepository.java | 2 +- .../domain/highlight/mapper/HighlightFactoryTest.java | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) 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/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..9993322f 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 @@ -115,7 +115,7 @@ SELECT EXISTS( WHERE h.job_id = :jobId """,nativeQuery = true) - List fetchHighlightsByJobId(UUID jobId,UUID memberId); + List fetchHighlightsByJobId(String jobId,UUID memberId); /** * =========================== *

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); From 27d7f64b3995546b97322ce83f9730ae9f39e4e3 Mon Sep 17 00:00:00 2001 From: tkv00 Date: Wed, 19 Nov 2025 23:59:50 +0900 Subject: [PATCH 03/18] =?UTF-8?q?refact=20:=20jobId=20UUID=20->=20String?= =?UTF-8?q?=20=EC=98=A4=ED=83=88=EC=9E=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shootpointer/domain/highlight/helper/HighlightUtilImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } From 9553a54b7189c9ab3aa47ea8d15af6456dffacfe Mon Sep 17 00:00:00 2001 From: tkv00 Date: Thu, 20 Nov 2025 13:07:19 +0900 Subject: [PATCH 04/18] =?UTF-8?q?refact=20:=20SSE=20=EC=B4=88=EA=B8=B0=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20=ED=99=95=EC=9D=B8=20=EC=A0=84=EC=86=A1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../progress/controller/ProgressController.java | 3 ++- .../progress/service/ProgressSseEmitter.java | 14 ++++++++++++++ .../redis/subscriber/ProgressSubscriber.java | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) 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..39014970 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,6 +3,7 @@ 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; @@ -14,7 +15,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 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..99af6ce8 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 @@ -51,6 +51,20 @@ public SseEmitter createEmitter(String memberId, String lastEventId, String jobI String sseKey=buildKey(memberId,jobId); emitters.put(sseKey,emitter); + // 연결 확인용 초기 이벤트 전송 + try { + emitter.send(SseEmitter.event() + .name("connected") + .data(Map.of( + "message", "SSE connection established", + "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); 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 5c4fe50a..9ef16cb4 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 @@ -74,8 +74,8 @@ public void onMessage(Message message, byte[] pattern) { //SSE로 client에 전달 emitter.sendToClient( - progress.memberId(), jobIdFromChannel, //redis에서 구독한 jobId로 SSE 발행 + progress.memberId(), response ); From 16bf09ff361e7da1fe76cdd77d28505f216389c1 Mon Sep 17 00:00:00 2001 From: tkv00 Date: Thu, 20 Nov 2025 14:27:11 +0900 Subject: [PATCH 05/18] =?UTF-8?q?refact=20:=20Redis=20connection=20log=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../redis/config/RedisOpenCVConfig.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) 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..eba0a5ed 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 @@ -6,6 +6,7 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.midas.shootpointer.infrastructure.redis.subscriber.ProgressSubscriber; 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; @@ -17,12 +18,14 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.listener.ChannelTopic; 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; @Configuration @RequiredArgsConstructor @Profile("prod") //opencv 환경에서만 실행 +@Slf4j public class RedisOpenCVConfig { @Value("${spring.data.redis.opencv.host}") private String host; @@ -41,16 +44,17 @@ public class RedisOpenCVConfig { */ @Bean public ChannelTopic uploadChannelTopic(){ - return new ChannelTopic(uploadChannel); + return new ChannelTopic(uploadChannel+":*"); } @Bean public ChannelTopic highlightChannelTopic(){ - return new ChannelTopic(highlightChannel); + return new ChannelTopic(highlightChannel+":*"); } @Bean(name = "opencvRedisConnectionFactory") public RedisConnectionFactory opencvRedisConnectionFactory(){ + log.info("[Redis OpenCV] connected redis information host : {} port : {}",host,port); RedisStandaloneConfiguration configuration=new RedisStandaloneConfiguration(); configuration.setHostName(host); configuration.setPort(port); @@ -88,6 +92,7 @@ public RedisMessageListenerContainer redisMessage( */ RedisMessageListenerContainer container=new RedisMessageListenerContainer(); + MessageListenerAdapter listenerAdapter=new MessageListenerAdapter(progressSubscriber); /** * 2. 연결 설정 주입 */ @@ -97,10 +102,11 @@ public RedisMessageListenerContainer redisMessage( * 3. 채널로 구독자 등록 */ //원본 영상 업로드 채널 구독 - container.addMessageListener(progressSubscriber,uploadChannelTopic()); + container.addMessageListener(listenerAdapter,uploadChannelTopic()); + log.info("[Redis OpenCV] Subscribed upload pattern : {}",uploadChannel+":*"); //하이라이트 영상 채널 구독 - container.addMessageListener(progressSubscriber,highlightChannelTopic()); - + container.addMessageListener(listenerAdapter,highlightChannelTopic()); + log.info("[Redis OpenCV] Subscribed highlight pattern : {}",highlightChannel+":*"); return container; } } From aafae2a85098f876cdec1269c7180bcd1e720de5 Mon Sep 17 00:00:00 2001 From: tkv00 Date: Thu, 20 Nov 2025 15:20:10 +0900 Subject: [PATCH 06/18] =?UTF-8?q?feat=20:=20RedisConnection=20=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../redis/config/RedisOpenCVConfig.java | 188 +++++++++++++----- 1 file changed, 135 insertions(+), 53 deletions(-) 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 eba0a5ed..fc978d11 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,6 +5,9 @@ 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; @@ -12,61 +15,116 @@ 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 java.time.Duration; + +@Slf4j @Configuration @RequiredArgsConstructor -@Profile("prod") //opencv 환경에서만 실행 -@Slf4j +@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.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(){ - log.info("[Redis OpenCV] connected redis information host : {} port : {}",host,port); - 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); + + // 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() @@ -77,36 +135,60 @@ 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(); - - MessageListenerAdapter listenerAdapter=new MessageListenerAdapter(progressSubscriber); - /** - * 2. 연결 설정 주입 - */ - container.setConnectionFactory(connectionFactory); - - /** - * 3. 채널로 구독자 등록 - */ - //원본 영상 업로드 채널 구독 - container.addMessageListener(listenerAdapter,uploadChannelTopic()); - log.info("[Redis OpenCV] Subscribed upload pattern : {}",uploadChannel+":*"); - //하이라이트 영상 채널 구독 - container.addMessageListener(listenerAdapter,highlightChannelTopic()); - log.info("[Redis OpenCV] Subscribed highlight pattern : {}",highlightChannel+":*"); - 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); + + // 컨테이너 초기화 + container.afterPropertiesSet(); + + // 컨테이너 시작 (명시적) + container.start(); + + 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); + } } -} +} \ No newline at end of file From 2b1f4078b780f3af8ae64a111fb21e67d8bac875 Mon Sep 17 00:00:00 2001 From: tkv00 Date: Thu, 20 Nov 2025 15:26:38 +0900 Subject: [PATCH 07/18] =?UTF-8?q?refact=20:=20Container=20=EB=B9=88=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EB=AA=85=EC=8B=9C=EC=A0=81=20->=20?= =?UTF-8?q?=EB=AC=B5=EC=8B=9C=EC=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/redis/config/RedisOpenCVConfig.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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 fc978d11..b88109f1 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 @@ -169,14 +169,8 @@ public RedisMessageListenerContainer redisMessageListenerContainer( String highlightPattern = highlightChannel + ":*"; PatternTopic highlightTopic = new PatternTopic(highlightPattern); container.addMessageListener(listenerAdapter, highlightTopic); - log.info("[Redis OpenCV] ✓ Registered listener for pattern: {}", highlightPattern); - - // 컨테이너 초기화 - container.afterPropertiesSet(); - - // 컨테이너 시작 (명시적) - container.start(); + 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:"); From 0536f82db8c7465d0284e667a600432f1bb634be Mon Sep 17 00:00:00 2001 From: tkv00 Date: Thu, 20 Nov 2025 15:36:43 +0900 Subject: [PATCH 08/18] =?UTF-8?q?refact=20:=20Redis=20SUB=EC=97=90?= =?UTF-8?q?=EC=84=9C=20JobId=20=EC=B6=94=EC=B6=9C=20=EC=9D=B8=EB=8D=B1?= =?UTF-8?q?=EC=8A=A4=20=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/redis/subscriber/ProgressSubscriber.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9ef16cb4..a5d7bc39 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 @@ -34,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.length >= 3 ? tokens[1] : null; // 2: "opencv-progress-upload", 3: jobId log.info("[Redis SUB] channel={} jobId={} body={}", channel, jobIdFromChannel, body); From 995867f9452712502bed2998f7e950e8a5872321 Mon Sep 17 00:00:00 2001 From: tkv00 Date: Thu, 20 Nov 2025 15:51:36 +0900 Subject: [PATCH 09/18] =?UTF-8?q?test=20:=20=EC=8B=A4=EC=A0=9C=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EB=AC=BC=20=EB=8D=94=EB=AF=B8=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/SetRealPostDataLoader.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) 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 - 삽입 완료"); } From 99f4713023e5d4af044e2e8b1e5f0a8c517e7095 Mon Sep 17 00:00:00 2001 From: tkv00 Date: Thu, 20 Nov 2025 16:40:13 +0900 Subject: [PATCH 10/18] =?UTF-8?q?refact=20:=20SSE=20=EC=BA=90=EC=8B=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../progress/service/ProgressSseEmitter.java | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) 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 99af6ce8..5c876f24 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 @@ -49,6 +47,7 @@ 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); // 연결 확인용 초기 이벤트 전송 @@ -104,6 +103,7 @@ 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); @@ -135,19 +135,29 @@ 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 { From 05b2aff3b3376404fd035eae6e8cbbdadb7dad02 Mon Sep 17 00:00:00 2001 From: tkv00 Date: Thu, 20 Nov 2025 17:24:35 +0900 Subject: [PATCH 11/18] =?UTF-8?q?fix=20:=20Redis=20channel=20jobId=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/redis/subscriber/ProgressSubscriber.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a5d7bc39..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 @@ -34,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[1] : null; // 2: "opencv-progress-upload", 3: jobId + String jobIdFromChannel = tokens[1]; log.info("[Redis SUB] channel={} jobId={} body={}", channel, jobIdFromChannel, body); From 42abc03c6cb07374060e4678bca4eca4c6209b66 Mon Sep 17 00:00:00 2001 From: tkv00 Date: Fri, 21 Nov 2025 01:48:30 +0900 Subject: [PATCH 12/18] =?UTF-8?q?fix=20:=20openCV=20redis=20password=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=B6=94=EA=B0=80=20#24?= =?UTF-8?q?8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/redis/config/RedisOpenCVConfig.java | 4 ++++ src/main/resources/application.yml | 1 + 2 files changed, 5 insertions(+) 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 b88109f1..5b8a1b8d 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 @@ -41,6 +41,9 @@ public class RedisOpenCVConfig { @Value("${spring.data.redis.opencv.port:6379}") private int port; + @Value("${spring.data.redis.opencv.password}") + private String password; + @Value("${spring.data.redis.opencv.channels.highlight:opencv-progress-highlight}") private String highlightChannel; @@ -67,6 +70,7 @@ public RedisConnectionFactory opencvRedisConnectionFactory() { RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(); configuration.setHostName(host); configuration.setPort(port); + configuration.setPassword(password); // Lettuce 클라이언트 설정 (타임아웃, 재연결 등) SocketOptions socketOptions = SocketOptions.builder() 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:* From dce2c87797f4878abbd34fc5b668043f8c1373bf Mon Sep 17 00:00:00 2001 From: Kim Do Yeon <144890194+tkv00@users.noreply.github.com> Date: Fri, 21 Nov 2025 02:49:20 +0900 Subject: [PATCH 13/18] Update Redis password configuration method --- .../infrastructure/redis/config/RedisOpenCVConfig.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 5b8a1b8d..2d8ba055 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 @@ -70,7 +70,7 @@ public RedisConnectionFactory opencvRedisConnectionFactory() { RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(); configuration.setHostName(host); configuration.setPort(port); - configuration.setPassword(password); + configuration.setPassword(RedisPassword.of(password)); // Lettuce 클라이언트 설정 (타임아웃, 재연결 등) SocketOptions socketOptions = SocketOptions.builder() @@ -189,4 +189,4 @@ public RedisMessageListenerContainer redisMessageListenerContainer( throw new RuntimeException("Failed to initialize Redis message listener", e); } } -} \ No newline at end of file +} From 2a42b30896f731f9b894c14fb2ec52222e6e5bd4 Mon Sep 17 00:00:00 2001 From: Kim Do Yeon <144890194+tkv00@users.noreply.github.com> Date: Fri, 21 Nov 2025 02:51:59 +0900 Subject: [PATCH 14/18] Update RedisOpenCVConfig.java --- .../infrastructure/redis/config/RedisOpenCVConfig.java | 1 + 1 file changed, 1 insertion(+) 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 2d8ba055..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 @@ -26,6 +26,7 @@ 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; From 631214d083ea94ad11ee3b1b7e33c4050d598087 Mon Sep 17 00:00:00 2001 From: tkv00 Date: Fri, 21 Nov 2025 04:59:34 +0900 Subject: [PATCH 15/18] =?UTF-8?q?fix=20:=20SSE=20name=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/like/helper/LikeUtilImpl.java | 6 ------ .../domain/like/repository/LikeQueryRepository.java | 8 ++++++++ .../domain/post/dto/response/PostResponse.java | 9 +++++---- .../shootpointer/domain/progress/dto/SseEvent.java | 1 - .../domain/progress/service/ProgressSseEmitter.java | 13 ++++--------- 5 files changed, 17 insertions(+), 20 deletions(-) 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/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 5c876f24..6a54e390 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 @@ -22,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<>(); @@ -53,9 +50,8 @@ public SseEmitter createEmitter(String memberId, String lastEventId, String jobI // 연결 확인용 초기 이벤트 전송 try { emitter.send(SseEmitter.event() - .name("connected") .data(Map.of( - "message", "SSE connection established", + "type", "CONNECTED", "jobId", jobId, "timestamp", Instant.now().toEpochMilli() ))); @@ -105,7 +101,7 @@ public void sendToClient(String jobId,String memberId,Object data){ 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); /** * 1. 캐시에 저장. @@ -163,10 +159,9 @@ 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(); } } From d1fc2d01fd70323e8945822e117fa34955f24ed6 Mon Sep 17 00:00:00 2001 From: tkv00 Date: Fri, 21 Nov 2025 06:38:24 +0900 Subject: [PATCH 16/18] =?UTF-8?q?feat=20:=20SSE=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=97=90=EC=84=9C=20REST=20=EB=B0=A9=EC=8B=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../progress/controller/ProgressController.java | 11 +++++++++++ .../domain/progress/service/ProgressSseEmitter.java | 5 +++++ 2 files changed, 16 insertions(+) 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 39014970..0ba50ba9 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 @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import java.util.Map; import java.util.UUID; @RestController @@ -23,4 +24,14 @@ public SseEmitter subscribe( UUID memberId = SecurityUtils.getCurrentMemberId(); return progressSseEmitter.createEmitter(memberId.toString(),lastEventId,jobId); } + + @GetMapping(value = "/progress") + 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/service/ProgressSseEmitter.java b/src/main/java/com/midas/shootpointer/domain/progress/service/ProgressSseEmitter.java index 6a54e390..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 @@ -36,6 +36,7 @@ private void init(){ //memberId -> 최근 이벤트 (오름차순 : 맨 뒤가 최신) private static Map> eventCache=new ConcurrentHashMap<>(); + private static final Map latestProgressMap=new ConcurrentHashMap<>(); /** * 구독 생성 * @param memberId : 멤버 Id @@ -103,6 +104,7 @@ public void sendToClient(String jobId,String memberId,Object data){ long eventId= Instant.now().toEpochMilli(); SseEvent event=new SseEvent(eventId,data); + latestProgressMap.put(jobId,data);//REST API 통신을 위한 임시 메서드 /** * 1. 캐시에 저장. */ @@ -170,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 From bc300e81455e260c0a0600983dd0a3932085f006 Mon Sep 17 00:00:00 2001 From: tkv00 Date: Fri, 21 Nov 2025 06:52:52 +0900 Subject: [PATCH 17/18] =?UTF-8?q?fix=20:=20progress=20URL=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/progress/controller/ProgressController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0ba50ba9..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 @@ -25,7 +25,7 @@ public SseEmitter subscribe( return progressSseEmitter.createEmitter(memberId.toString(),lastEventId,jobId); } - @GetMapping(value = "/progress") + @GetMapping public Object progress(@RequestParam String jobId) { Object data=progressSseEmitter.getLatestProgress(jobId); From 26541b415a5169558600ce84857ea131aeb15d22 Mon Sep 17 00:00:00 2001 From: tkv00 Date: Fri, 21 Nov 2025 07:39:38 +0900 Subject: [PATCH 18/18] =?UTF-8?q?fix=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20=EC=B5=9C=EA=B7=BC=20=ED=95=98=EC=9D=B4=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/highlight/repository/HighlightQueryRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 9993322f..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,13 +107,13 @@ 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(String jobId,UUID memberId); /**