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/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..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,23 +73,23 @@ public List searchBoothsByKeyword1(String keyword) { } public List getBoothRanking() { - List allBooths = boothRepository.findByIsActiveTrue(); - if (allBooths.isEmpty()) { + Set> rawRanking = boothLikeService.getRanking(); + if (rawRanking == null || rawRanking.isEmpty()) { return List.of(); } - Set> ranking = boothLikeService.getRanking(); - Map likeCountMap = (ranking == null) ? Map.of() : 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(); + } - return allBooths.stream() + 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)) @@ -125,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..48efce8 --- /dev/null +++ b/src/main/java/kr/co/knuserver/application/booth/BoothRanking.java @@ -0,0 +1,44 @@ +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; + + public BoothRanking(Set> tuples) { + this.likeCountMap = tuples.stream() + .filter(t -> t.getValue() != null) + .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() { + return likeCountMap.isEmpty(); + } + + public Set boothIds() { + return likeCountMap.keySet(); + } + + public long getLikeCount(Long boothId) { + return likeCountMap.getOrDefault(boothId, 0L); + } +} 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); 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", "존재하지 않는 테이블입니다."),