Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
70cf980
Merge pull request #226 from ShootPointer/dev
tkv00 Nov 17, 2025
f28293d
Merge pull request #234 from ShootPointer/dev
tkv00 Nov 18, 2025
475ad3e
Merge pull request #243 from ShootPointer/dev
tkv00 Nov 19, 2025
09426c7
refact : Progress ์‘๋‹ต response ๋ณ€๊ฒฝ
tkv00 Nov 19, 2025
881b592
Merge pull request #244 from ShootPointer/REQ_009
tkv00 Nov 19, 2025
1fed660
refact : jobId UUID -> String ๋ณ€๊ฒฝ
tkv00 Nov 19, 2025
27d7f64
refact : jobId UUID -> String ์˜คํƒˆ์ž ์ˆ˜์ •
tkv00 Nov 19, 2025
bf8c810
Merge pull request #245 from ShootPointer/REQ_009
tkv00 Nov 19, 2025
9553a54
refact : SSE ์ดˆ๊ธฐ ์—ฐ๊ฒฐ ํ™•์ธ ์ „์†ก ์ถ”๊ฐ€
tkv00 Nov 20, 2025
16bf09f
refact : Redis connection log ์ถ”๊ฐ€
tkv00 Nov 20, 2025
aafae2a
feat : RedisConnection ์ˆ˜์ •
tkv00 Nov 20, 2025
2b1f407
refact : Container ๋นˆ ๋“ฑ๋ก ๋ช…์‹œ์  -> ๋ฌต์‹œ์ 
tkv00 Nov 20, 2025
0536f82
refact : Redis SUB์—์„œ JobId ์ถ”์ถœ ์ธ๋ฑ์Šค ์ •์ •
tkv00 Nov 20, 2025
995867f
test : ์‹ค์ œ ๊ฒŒ์‹œ๋ฌผ ๋”๋ฏธ ๋ฐ์ดํ„ฐ ํ…Œ์ŠคํŠธ ์ˆ˜์ •
tkv00 Nov 20, 2025
99f4713
refact : SSE ์บ์‹œ ์ •๋ฆฌ ๋ฉ”์„œ๋“œ ์ˆ˜์ •
tkv00 Nov 20, 2025
05b2aff
fix : Redis channel jobId ์ถ”์ถœ ์ธ๋ฑ์Šค ์˜ค๋ฅ˜ ์ˆ˜์ •
tkv00 Nov 20, 2025
42abc03
fix : openCV redis password ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ถ”๊ฐ€ #248
tkv00 Nov 20, 2025
7c357d1
Merge pull request #249 from ShootPointer/FIX_003
tkv00 Nov 20, 2025
dce2c87
Update Redis password configuration method
tkv00 Nov 20, 2025
2a42b30
Update RedisOpenCVConfig.java
tkv00 Nov 20, 2025
631214d
fix : SSE name ์‚ญ์ œ
tkv00 Nov 20, 2025
d1fc2d0
feat : SSE ๋ฐฉ์‹์—์„œ REST ๋ฐฉ์‹ ์ถ”๊ฐ€
tkv00 Nov 20, 2025
bc300e8
fix : progress URL ์ˆ˜์ •
tkv00 Nov 20, 2025
26541b4
fix : ์‚ฌ์šฉ์ž์˜ ์ตœ๊ทผ ํ•˜์ด๋ผ์ดํŠธ ๋ชฉ๋ก ์กฐํšŒ ์ฟผ๋ฆฌ ์ˆ˜์ •
tkv00 Nov 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public HighlightCalendarResponse fetchCalendar(int year, int month,UUID memberId
return new HighlightCalendarResponse(year,month,daysResponses);
}

public List<HighlightInfoResponse> fetchLatestCreatedHighlights(UUID jobId,UUID memberId){
public List<HighlightInfoResponse> fetchLatestCreatedHighlights(String jobId,UUID memberId){
List<HighlightEntity> highlightList=highlightHelper.fetchLastestCreatedHighlights(jobId,memberId);

return highlightList.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public ResponseEntity<ApiResponse<HighlightCalendarResponse>> fetchCalendar(

@GetMapping("/latest")
public ResponseEntity<ApiResponse<List<HighlightInfoResponse>>> latestCreatedHighlights(
@RequestParam(value = "jobId") UUID jobId
@RequestParam(value = "jobId") String jobId
){
UUID memberId=SecurityUtils.getCurrentMemberId();
return ResponseEntity.ok(ApiResponse.ok(manager.fetchLatestCreatedHighlights(jobId,memberId)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ public class HighlightRequest {
private LocalDateTime createdAt;

@NotNull(message = "ํ•˜์ด๋ผ์ดํŠธ JobId๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.")
private UUID jobId;
private String jobId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/*
=========== [ ๋„๋ฉ”์ธ-ํ–‰์œ„ ] ==============
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public DateTimeRange getMonthDateTimeRange(int year, int month) {
}

@Override
public List<HighlightEntity> fetchLastestCreatedHighlights(UUID jobId, UUID memberId) {
public List<HighlightEntity> fetchLastestCreatedHighlights(String jobId, UUID memberId) {
return highlightUtil.fetchLastestCreatedHighlights(jobId,memberId);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ public interface HighlightUtil {
TreeMap<LocalDate,List<HighlightInfoResponse>> groupingHighlights(List<HighlightInfoResponse> flatHighlightList);
List<HighlightInfoResponse> fetchFlatHighlightList(int year,int month,UUID memberId);
DateTimeRange getMonthDateTimeRange(int year, int month);
List<HighlightEntity> fetchLastestCreatedHighlights(UUID jobId,UUID memberId);
List<HighlightEntity> fetchLastestCreatedHighlights(String jobId,UUID memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ public DateTimeRange getMonthDateTimeRange(int year, int month) {
}

@Override
public List<HighlightEntity> fetchLastestCreatedHighlights(UUID jobId, UUID memberId) {
public List<HighlightEntity> fetchLastestCreatedHighlights(String jobId, UUID memberId) {
return highlightQueryRepository.fetchHighlightsByJobId(jobId,memberId);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public List<HighlightEntity> createHighlightEntities(List<HighlightInfo> highlig
Member member,
BackNumberEntity backNumber,
LocalDateTime createAt,
UUID jobId
String jobId
){
return highlightInfos.stream()
.map(info -> HighlightEntity.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HighlightEntity> fetchHighlightsByJobId(UUID jobId,UUID memberId);
List<HighlightEntity> fetchHighlightsByJobId(String jobId,UUID memberId);
/**
* ===========================
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,12 @@ public interface LikeQueryRepository extends JpaRepository<LikeEntity,Long> {
nativeQuery = true
)
Optional<LikeEntity> 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,12 +16,22 @@
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
) {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

public record SseEvent(
@NotNull long eventId,
@NotNull String name,
@NotNull Object data
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,23 +22,21 @@ 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<String, SseEmitter> emitters=new ConcurrentHashMap<>();

//memberId -> ์ตœ๊ทผ ์ด๋ฒคํŠธ (์˜ค๋ฆ„์ฐจ์ˆœ : ๋งจ ๋’ค๊ฐ€ ์ตœ์‹ )
private static Map<String, Deque<SseEvent>> eventCache=new ConcurrentHashMap<>();

private static final Map<String,Object> latestProgressMap=new ConcurrentHashMap<>();
/**
* ๊ตฌ๋… ์ƒ์„ฑ
* @param memberId : ๋ฉค๋ฒ„ Id
Expand All @@ -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);
Expand Down Expand Up @@ -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. ์บ์‹œ์— ์ €์žฅ.
*/
Expand Down Expand Up @@ -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<String> 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();
}
}
Expand All @@ -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);
}
}
Loading
Loading