From c2c5779329830e506eff700568d1be90ad11c3ec Mon Sep 17 00:00:00 2001 From: LeeJuYoung Date: Sat, 29 Aug 2026 20:11:42 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20withDummy=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../band/controller/BandController.java | 6 ++- .../service/BandRecommendationService.java | 7 +++- .../impl/BandRecommendationServiceV1Impl.java | 9 ++++- .../impl/BandRecommendationServiceV2Impl.java | 10 ++++- .../BandRecommendationServiceV1ImplTest.java | 25 ++++++++++++ .../BandRecommendationServiceV2ImplTest.java | 38 +++++++++++++++++++ 6 files changed, 89 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/umc/bscene/domain/band/controller/BandController.java b/src/main/java/com/umc/bscene/domain/band/controller/BandController.java index 1693ac81..d561698f 100644 --- a/src/main/java/com/umc/bscene/domain/band/controller/BandController.java +++ b/src/main/java/com/umc/bscene/domain/band/controller/BandController.java @@ -59,13 +59,15 @@ public ResponseEntity> createBand( } // 밴드 추천 목록 조회 API + // withDummy=false면 더미 밴드(운영 DB 시드 id 1~474)를 추천 후보에서 제외, 생략·true면 전체 (기존 호환) @GetMapping("/recommendations") public ResponseEntity> getRecommendedBands( @AuthenticationPrincipal AuthMember authMember, @RequestParam(required = false) Long cursor, - @RequestParam(required = false) Integer size + @RequestParam(required = false) Integer size, + @RequestParam(defaultValue = "true") boolean withDummy ) { - BandRecommendResponse response = bandRecommendationService.getRecommendedBands(authMember.getUser().getId(), cursor, size); + BandRecommendResponse response = bandRecommendationService.getRecommendedBands(authMember.getUser().getId(), cursor, size, withDummy); SuccessResponse successResponse = SuccessResponse.of( response, BandSuccessCode.BAND_RECOMMEND_LIST_GET_SUCCESS diff --git a/src/main/java/com/umc/bscene/domain/band/service/BandRecommendationService.java b/src/main/java/com/umc/bscene/domain/band/service/BandRecommendationService.java index a0aee67d..95c30d01 100644 --- a/src/main/java/com/umc/bscene/domain/band/service/BandRecommendationService.java +++ b/src/main/java/com/umc/bscene/domain/band/service/BandRecommendationService.java @@ -4,5 +4,10 @@ public interface BandRecommendationService { - BandRecommendResponse getRecommendedBands(Long userId, Long cursor, Integer size); + BandRecommendResponse getRecommendedBands(Long userId, Long cursor, Integer size, boolean withDummy); + + // withDummy 생략 호출은 더미 밴드를 포함한 기존 동작을 유지한다 (팬홈 등 기존 호출부 호환) + default BandRecommendResponse getRecommendedBands(Long userId, Long cursor, Integer size) { + return getRecommendedBands(userId, cursor, size, true); + } } diff --git a/src/main/java/com/umc/bscene/domain/band/service/impl/BandRecommendationServiceV1Impl.java b/src/main/java/com/umc/bscene/domain/band/service/impl/BandRecommendationServiceV1Impl.java index 3e22d879..0c765ee8 100644 --- a/src/main/java/com/umc/bscene/domain/band/service/impl/BandRecommendationServiceV1Impl.java +++ b/src/main/java/com/umc/bscene/domain/band/service/impl/BandRecommendationServiceV1Impl.java @@ -44,6 +44,9 @@ public class BandRecommendationServiceV1Impl implements BandRecommendationServic private static final int RECENT_POST_SCORE = 2; private static final int RECENT_PERFORMANCE_SCORE = 1; + // 더미 밴드 경계 : 운영 DB 시드 기준으로 이 값 이하 band id는 전부 더미 밴드 (withDummy=false 필터의 기준) + private static final long DUMMY_BAND_MAX_ID = 474; + private final BandRepository bandRepository; private final UserRepository userRepository; private final UserGenresRepository userGenresRepository; @@ -53,7 +56,7 @@ public class BandRecommendationServiceV1Impl implements BandRecommendationServic // 밴드 추천 목록 조회 (장르/지역/활동 내역 기반 스코어링) @Override - public BandRecommendResponse getRecommendedBands(Long userId, Long cursor, Integer size) { + public BandRecommendResponse getRecommendedBands(Long userId, Long cursor, Integer size, boolean withDummy) { int pageSize = (size != null) ? size : DEFAULT_RECOMMEND_PAGE_SIZE; long offset = (cursor != null) ? cursor : 0; @@ -67,7 +70,9 @@ public BandRecommendResponse getRecommendedBands(Long userId, Long cursor, Integ .map(UserRegions::getRegion) .collect(Collectors.toSet()); - List candidates = bandRepository.findAllByStatus(BandStatus.ACCEPTED); + List candidates = bandRepository.findAllByStatus(BandStatus.ACCEPTED).stream() + .filter(band -> withDummy || band.getId() > DUMMY_BAND_MAX_ID) + .toList(); List candidateIds = candidates.stream().map(Band::getId).toList(); LocalDateTime postSince = LocalDateTime.now().minusDays(RECENT_ACTIVITY_DAYS); diff --git a/src/main/java/com/umc/bscene/domain/band/service/impl/BandRecommendationServiceV2Impl.java b/src/main/java/com/umc/bscene/domain/band/service/impl/BandRecommendationServiceV2Impl.java index 1f84d938..e917e7f3 100644 --- a/src/main/java/com/umc/bscene/domain/band/service/impl/BandRecommendationServiceV2Impl.java +++ b/src/main/java/com/umc/bscene/domain/band/service/impl/BandRecommendationServiceV2Impl.java @@ -57,6 +57,9 @@ public class BandRecommendationServiceV2Impl implements BandRecommendationServic private static final int RECENT_ACTIVITY_DAYS = 30; private static final int TOP_CLICKED_BANDS_LIMIT = 10; + // 더미 밴드 경계 : 운영 DB 시드 기준으로 이 값 이하 band id는 전부 더미 밴드 (withDummy=false 필터의 기준) + private static final long DUMMY_BAND_MAX_ID = 474; + // explicit(장르/지역/활동) 내부 가중치. 셋 다 만족 시 explicit 원점수는 GENRE+REGION+ACTIVITY(=6)로 정규화된다. private static final double GENRE_MATCH_SCORE = 3; private static final double REGION_MATCH_SCORE = 2; @@ -129,7 +132,7 @@ public class BandRecommendationServiceV2Impl implements BandRecommendationServic private final BandRecommendationLogRepository bandRecommendationLogRepository; @Override - public BandRecommendResponse getRecommendedBands(Long userId, Long cursor, Integer size) { + public BandRecommendResponse getRecommendedBands(Long userId, Long cursor, Integer size, boolean withDummy) { int limit = (size != null) ? Math.min(size, RECOMMEND_LIMIT) : RECOMMEND_LIMIT; User user = userRepository.getReferenceById(userId); @@ -184,6 +187,11 @@ public BandRecommendResponse getRecommendedBands(Long userId, Long cursor, Integ : ColdStartFallback.EMPTY; coldStartFallback.bands().forEach(band -> candidateBands.put(band.getId(), band)); + // withDummy=false : 더미 밴드(경계 이하 id) 제외 + if (!withDummy) { + candidateBands.keySet().removeIf(id -> id <= DUMMY_BAND_MAX_ID); + } + List candidateIds = new ArrayList<>(candidateBands.keySet()); // 노출 피로도 감점 : 후보에서 빼지 않고 점수만 깎는다 (FLOOR 덕에 유일한 매치는 그대로 남음). diff --git a/src/test/java/com/umc/bscene/domain/band/service/BandRecommendationServiceV1ImplTest.java b/src/test/java/com/umc/bscene/domain/band/service/BandRecommendationServiceV1ImplTest.java index 1dfb6649..dfc491c6 100644 --- a/src/test/java/com/umc/bscene/domain/band/service/BandRecommendationServiceV1ImplTest.java +++ b/src/test/java/com/umc/bscene/domain/band/service/BandRecommendationServiceV1ImplTest.java @@ -174,4 +174,29 @@ private void preferredGenreAndRegion(Genre genre, Region region) { assertTrue(response.bands().isEmpty()); } + + @Test + void withDummy가_false면_더미_밴드_id는_후보에서_제외된다() { + preferredGenreAndRegion(Genre.HARD_ROCK, Region.SEOUL); + Band dummyBand = band(474L, Genre.HARD_ROCK, Region.SEOUL); // 더미 경계(<=474) + Band realBand = band(475L, Genre.HARD_ROCK, Region.SEOUL); + when(bandRepository.findAllByStatus(BandStatus.ACCEPTED)).thenReturn(List.of(dummyBand, realBand)); + + BandRecommendResponse response = service.getRecommendedBands(USER_ID, null, null, false); + + assertEquals(1, response.bands().size()); + assertEquals(475L, response.bands().get(0).bandId()); + } + + @Test + void withDummy_생략시_기본값true로_더미_밴드도_포함된다() { + preferredGenreAndRegion(Genre.HARD_ROCK, Region.SEOUL); + Band dummyBand = band(474L, Genre.HARD_ROCK, Region.SEOUL); + Band realBand = band(475L, Genre.HARD_ROCK, Region.SEOUL); + when(bandRepository.findAllByStatus(BandStatus.ACCEPTED)).thenReturn(List.of(dummyBand, realBand)); + + BandRecommendResponse response = service.getRecommendedBands(USER_ID, null, null); + + assertEquals(2, response.bands().size()); + } } diff --git a/src/test/java/com/umc/bscene/domain/band/service/impl/BandRecommendationServiceV2ImplTest.java b/src/test/java/com/umc/bscene/domain/band/service/impl/BandRecommendationServiceV2ImplTest.java index 8025909a..61ec35d2 100644 --- a/src/test/java/com/umc/bscene/domain/band/service/impl/BandRecommendationServiceV2ImplTest.java +++ b/src/test/java/com/umc/bscene/domain/band/service/impl/BandRecommendationServiceV2ImplTest.java @@ -123,6 +123,44 @@ void similarityBonusIsZeroWhenUserFollowsNoBand() { verify(bandSimilarityRepository, never()).findByBandIdIn(anyList()); } + @Test + void withDummyDefaultsToTrueAndIncludesDummyBands() { + when(followRepository.findBandIdsByUserId(USER_ID)).thenReturn(List.of()); + when(userGenresRepository.findAllByUser(any(User.class))) + .thenReturn(List.of(UserGenres.builder().genre(Genre.HARD_ROCK).build())); + when(userRegionsRepository.findAllByUser(any(User.class))).thenReturn(List.of()); + + Band dummyBand = band(474L, Genre.HARD_ROCK, Region.SEOUL); + Band realBand = band(475L, Genre.HARD_ROCK, Region.SEOUL); + when(bandRepository.findByGenreIn(any())).thenReturn(List.of(dummyBand, realBand)); + when(postRepository.findBandIdsWithRecentPost(anyList(), any())).thenReturn(List.of()); + when(postRepository.findLatestActivityAtByBandIds(anyList())).thenReturn(List.of()); + + // withDummy 생략 -> 기본값 true, 더미 밴드도 그대로 포함 + BandRecommendResponse response = service.getRecommendedBands(USER_ID, null, null); + + assertEquals(2, response.bands().size()); + } + + @Test + void withDummyFalseExcludesDummyBands() { + when(followRepository.findBandIdsByUserId(USER_ID)).thenReturn(List.of()); + when(userGenresRepository.findAllByUser(any(User.class))) + .thenReturn(List.of(UserGenres.builder().genre(Genre.HARD_ROCK).build())); + when(userRegionsRepository.findAllByUser(any(User.class))).thenReturn(List.of()); + + Band dummyBand = band(474L, Genre.HARD_ROCK, Region.SEOUL); + Band realBand = band(475L, Genre.HARD_ROCK, Region.SEOUL); + when(bandRepository.findByGenreIn(any())).thenReturn(List.of(dummyBand, realBand)); + when(postRepository.findBandIdsWithRecentPost(anyList(), any())).thenReturn(List.of()); + when(postRepository.findLatestActivityAtByBandIds(anyList())).thenReturn(List.of()); + + BandRecommendResponse response = service.getRecommendedBands(USER_ID, null, null, false); + + assertEquals(1, response.bands().size()); + assertEquals(475L, response.bands().get(0).bandId()); + } + @Test void similarityAverageIsCappedBeforeNormalizing() { List followedBandIds = List.of(100L, 200L, 300L);