From 97ecb25a5cf636690bee162a165737fbb29db874 Mon Sep 17 00:00:00 2001 From: wlgns12370 Date: Mon, 16 Mar 2026 00:05:24 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20EXTERNAL=5FSUPPORT=20=EB=B6=80?= =?UTF-8?q?=EC=8A=A4=20=EC=A2=8B=EC=95=84=EC=9A=94=20=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../knuserver/application/booth/BoothLikeService.java | 10 ++++++++++ .../knuserver/global/exception/BusinessErrorCode.java | 1 + 2 files changed, 11 insertions(+) diff --git a/src/main/java/kr/co/knuserver/application/booth/BoothLikeService.java b/src/main/java/kr/co/knuserver/application/booth/BoothLikeService.java index 94ad2c3..c01634c 100644 --- a/src/main/java/kr/co/knuserver/application/booth/BoothLikeService.java +++ b/src/main/java/kr/co/knuserver/application/booth/BoothLikeService.java @@ -3,6 +3,9 @@ import java.time.Duration; import java.util.Collections; import java.util.Set; +import kr.co.knuserver.domain.booth.entity.Booth; +import kr.co.knuserver.domain.booth.entity.BoothDivision; +import kr.co.knuserver.domain.booth.repository.BoothRepository; import kr.co.knuserver.global.exception.BusinessErrorCode; import kr.co.knuserver.global.exception.BusinessException; import lombok.RequiredArgsConstructor; @@ -25,8 +28,15 @@ public class BoothLikeService { private long rateLimitTtlSeconds; private final StringRedisTemplate redisTemplate; + private final BoothRepository boothRepository; public long like(Long boothId, String deviceId, String clientIp) { + Booth booth = boothRepository.findById(boothId) + .orElseThrow(() -> new BusinessException(BusinessErrorCode.BOOTH_NOT_FOUND)); + if (booth.getDivision() == BoothDivision.EXTERNAL_SUPPORT) { + throw new BusinessException(BusinessErrorCode.LIKE_NOT_ALLOWED); + } + try { checkRateLimit(clientIp, deviceId, boothId); Double score = redisTemplate.opsForZSet().incrementScore(RANKING_KEY, String.valueOf(boothId), 1); diff --git a/src/main/java/kr/co/knuserver/global/exception/BusinessErrorCode.java b/src/main/java/kr/co/knuserver/global/exception/BusinessErrorCode.java index b689961..37e6c45 100644 --- a/src/main/java/kr/co/knuserver/global/exception/BusinessErrorCode.java +++ b/src/main/java/kr/co/knuserver/global/exception/BusinessErrorCode.java @@ -43,6 +43,7 @@ public enum BusinessErrorCode implements ErrorCode { */ EVENT_NOT_FOUND(HttpStatus.NOT_FOUND, "C102", "이벤트가 존재하지 않습니다."), BOOTH_NOT_FOUND(HttpStatus.NOT_FOUND, "C101", "존재하지 않는 부스입니다."), + LIKE_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "C103", "해당 부스는 좋아요를 등록할 수 없습니다."), NOTICE_NOT_FOUND(HttpStatus.NOT_FOUND, "C403", "해당 공지사항을 찾을 수 없습니다."), MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "C404", "해당 사용자를 찾을 수 없습니다."), PUB_TABLE_NOT_FOUND(HttpStatus.NOT_FOUND, "C201", "존재하지 않는 테이블입니다."), From 4266547f717b4219c9cc88506756108ceebaf1b9 Mon Sep 17 00:00:00 2001 From: wlgns12370 Date: Mon, 16 Mar 2026 00:05:40 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EB=9E=AD=ED=82=B9=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20Redis=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20EXTERNAL=5FSUPPORT=20=EB=B6=80?= =?UTF-8?q?=EC=8A=A4=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/booth/BoothLikeWarmupRunner.java | 3 ++- .../application/booth/BoothQueryService.java | 12 +++++++----- .../domain/booth/repository/BoothRepository.java | 8 +++++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/java/kr/co/knuserver/application/booth/BoothLikeWarmupRunner.java b/src/main/java/kr/co/knuserver/application/booth/BoothLikeWarmupRunner.java index 77d4f9a..49e241c 100644 --- a/src/main/java/kr/co/knuserver/application/booth/BoothLikeWarmupRunner.java +++ b/src/main/java/kr/co/knuserver/application/booth/BoothLikeWarmupRunner.java @@ -1,6 +1,7 @@ package kr.co.knuserver.application.booth; import jakarta.annotation.PostConstruct; +import kr.co.knuserver.domain.booth.entity.BoothDivision; import kr.co.knuserver.domain.booth.repository.BoothRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -22,7 +23,7 @@ public void warmup() { try { log.debug("[LikeWarmup] 부스 좋아요 캐시 초기화 시작"); - boothRepository.findByIsActiveTrue() + boothRepository.findByIsActiveTrueAndDivisionNot(BoothDivision.EXTERNAL_SUPPORT) .forEach(booth -> redisTemplate.opsForZSet() .addIfAbsent(RANKING_KEY, String.valueOf(booth.getId()), 0)); diff --git a/src/main/java/kr/co/knuserver/application/booth/BoothQueryService.java b/src/main/java/kr/co/knuserver/application/booth/BoothQueryService.java index 81a3d8e..96ce6bb 100644 --- a/src/main/java/kr/co/knuserver/application/booth/BoothQueryService.java +++ b/src/main/java/kr/co/knuserver/application/booth/BoothQueryService.java @@ -73,19 +73,21 @@ public List searchBoothsByKeyword1(String keyword) { } public List getBoothRanking() { - List allBooths = boothRepository.findByIsActiveTrue(); - if (allBooths.isEmpty()) { + Set> ranking = boothLikeService.getRanking(); + if (ranking == null || ranking.isEmpty()) { return List.of(); } - Set> ranking = boothLikeService.getRanking(); - Map likeCountMap = (ranking == null) ? Map.of() : ranking.stream() + Map likeCountMap = ranking.stream() .collect(Collectors.toMap( t -> Long.parseLong(t.getValue()), t -> t.getScore() == null ? 0L : t.getScore().longValue() )); - return allBooths.stream() + List boothIds = List.copyOf(likeCountMap.keySet()); + List booths = boothRepository.findAllById(boothIds); + + return booths.stream() .map(booth -> new BoothRankingResponseDto( booth.getId(), booth.getName(), diff --git a/src/main/java/kr/co/knuserver/domain/booth/repository/BoothRepository.java b/src/main/java/kr/co/knuserver/domain/booth/repository/BoothRepository.java index 98401b3..f4801e3 100644 --- a/src/main/java/kr/co/knuserver/domain/booth/repository/BoothRepository.java +++ b/src/main/java/kr/co/knuserver/domain/booth/repository/BoothRepository.java @@ -2,6 +2,7 @@ import java.util.List; import kr.co.knuserver.domain.booth.entity.Booth; +import kr.co.knuserver.domain.booth.entity.BoothDivision; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -16,15 +17,16 @@ public interface BoothRepository extends JpaRepository { List findByIsActiveTrue(); - // 주어진 키워드가 부스명 or 부스 설명에 들어간 부스들을 조회(페이지네이션 X) + List findByIsActiveTrueAndDivisionNot(BoothDivision division); + @Query("SELECT b FROM Booth b " + "WHERE b.isActive = true " + "AND (b.name LIKE CONCAT('%', :keyword, '%') " + " OR CONCAT(',', b.keywords, ',') LIKE CONCAT('%,', :keyword, ',%')) " + "ORDER BY " + " CASE " + - " WHEN b.name LIKE CONCAT('%', :keyword, '%') THEN 1 " + // 1순위: 이름에 키워드가 포함 (부분 일치) - " ELSE 2 " + // 2순위: keywords에 해당 키워드가 포함 (정확히 일치) + " WHEN b.name LIKE CONCAT('%', :keyword, '%') THEN 1 " + + " ELSE 2 " + " END ASC, " + " b.boothNumber ASC") List searchByKeyword(@Param("keyword") String keyword); From 2cd8ce9e159770b64e96a45474047a9b42945635 Mon Sep 17 00:00:00 2001 From: wlgns12370 Date: Mon, 16 Mar 2026 00:09:04 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EB=9E=AD=ED=82=B9=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20Redis=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20EXTERNAL=5FSUPPORT=20=EB=B6=80?= =?UTF-8?q?=EC=8A=A4=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/booth/BoothQueryService.java | 22 ++++++------- .../application/booth/BoothRanking.java | 32 +++++++++++++++++++ 2 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 src/main/java/kr/co/knuserver/application/booth/BoothRanking.java diff --git a/src/main/java/kr/co/knuserver/application/booth/BoothQueryService.java b/src/main/java/kr/co/knuserver/application/booth/BoothQueryService.java index 96ce6bb..b7b145c 100644 --- a/src/main/java/kr/co/knuserver/application/booth/BoothQueryService.java +++ b/src/main/java/kr/co/knuserver/application/booth/BoothQueryService.java @@ -73,25 +73,23 @@ public List searchBoothsByKeyword1(String keyword) { } public List getBoothRanking() { - Set> ranking = boothLikeService.getRanking(); - if (ranking == null || ranking.isEmpty()) { + Set> rawRanking = boothLikeService.getRanking(); + if (rawRanking == null || rawRanking.isEmpty()) { return List.of(); } - Map likeCountMap = ranking.stream() - .collect(Collectors.toMap( - t -> Long.parseLong(t.getValue()), - t -> t.getScore() == null ? 0L : t.getScore().longValue() - )); + BoothRanking boothRanking = new BoothRanking(rawRanking); + if (boothRanking.isEmpty()) { + return List.of(); + } - List boothIds = List.copyOf(likeCountMap.keySet()); - List booths = boothRepository.findAllById(boothIds); + List booths = boothRepository.findAllById(boothRanking.boothIds()); return booths.stream() .map(booth -> new BoothRankingResponseDto( booth.getId(), booth.getName(), - likeCountMap.getOrDefault(booth.getId(), 0L) + boothRanking.getLikeCount(booth.getId()) )) .sorted(Comparator.comparingLong(BoothRankingResponseDto::likeCount).reversed() .thenComparingLong(BoothRankingResponseDto::boothId)) @@ -127,12 +125,12 @@ public CursorPaginationResponse searchBoothsByKeyword2(Str List items = pagedBooths.stream() .map(booth -> { List urls = imageUrlsMap.getOrDefault(booth.getId(), Collections.emptyList()); - String firstImageUrl = urls.isEmpty() ? null : urls.get(0); + String firstImageUrl = urls.isEmpty() ? null : urls.getFirst(); return BoothListResponseDto.fromEntity(booth, firstImageUrl); }) .toList(); - Long nextCursor = items.isEmpty() ? null : items.get(items.size() - 1).id(); + Long nextCursor = items.isEmpty() ? null : items.getLast().id(); return CursorPaginationResponse.of(items, nextCursor, hasNext); } diff --git a/src/main/java/kr/co/knuserver/application/booth/BoothRanking.java b/src/main/java/kr/co/knuserver/application/booth/BoothRanking.java new file mode 100644 index 0000000..f0990de --- /dev/null +++ b/src/main/java/kr/co/knuserver/application/booth/BoothRanking.java @@ -0,0 +1,32 @@ +package kr.co.knuserver.application.booth; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.data.redis.core.ZSetOperations; + +public class BoothRanking { + + private final Map likeCountMap; + + public BoothRanking(Set> tuples) { + this.likeCountMap = tuples.stream() + .filter(t -> t.getValue() != null) + .collect(Collectors.toMap( + t -> Long.parseLong(t.getValue()), + t -> t.getScore() == null ? 0L : t.getScore().longValue() + )); + } + + public boolean isEmpty() { + return likeCountMap.isEmpty(); + } + + public Set boothIds() { + return likeCountMap.keySet(); + } + + public long getLikeCount(Long boothId) { + return likeCountMap.getOrDefault(boothId, 0L); + } +} From 7b7a47422aff97e2f55fb71d17daf0c2e46c9fea Mon Sep 17 00:00:00 2001 From: wlgns12370 Date: Mon, 16 Mar 2026 00:55:33 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=ED=8C=8C=EC=8B=B1=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=EB=A1=9C=20=EB=9E=AD=ED=82=B9=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EA=B0=80=20500=20=EC=8B=A4=ED=8C=A8=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/booth/BoothRanking.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/kr/co/knuserver/application/booth/BoothRanking.java b/src/main/java/kr/co/knuserver/application/booth/BoothRanking.java index f0990de..48efce8 100644 --- a/src/main/java/kr/co/knuserver/application/booth/BoothRanking.java +++ b/src/main/java/kr/co/knuserver/application/booth/BoothRanking.java @@ -1,10 +1,13 @@ package kr.co.knuserver.application.booth; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.ZSetOperations; +@Slf4j public class BoothRanking { private final Map likeCountMap; @@ -12,10 +15,19 @@ public class BoothRanking { public BoothRanking(Set> tuples) { this.likeCountMap = tuples.stream() .filter(t -> t.getValue() != null) - .collect(Collectors.toMap( - t -> Long.parseLong(t.getValue()), - t -> t.getScore() == null ? 0L : t.getScore().longValue() - )); + .flatMap(t -> parseBoothId(t.getValue()) + .map(id -> Map.entry(id, t.getScore() == null ? 0L : t.getScore().longValue())) + .stream()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private Optional parseBoothId(String value) { + try { + return Optional.of(Long.parseLong(value)); + } catch (NumberFormatException e) { + log.error("[BoothRanking] Redis member 파싱 실패 value={}", value, e); + return Optional.empty(); + } } public boolean isEmpty() {