Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -59,13 +59,15 @@ public ResponseEntity<SuccessResponse<BandResponse>> createBand(
}

// 밴드 추천 목록 조회 API
// withDummy=false면 더미 밴드(운영 DB 시드 id 1~474)를 추천 후보에서 제외, 생략·true면 전체 (기존 호환)
@GetMapping("/recommendations")
public ResponseEntity<SuccessResponse<BandRecommendResponse>> 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<BandRecommendResponse> successResponse = SuccessResponse.of(
response,
BandSuccessCode.BAND_RECOMMEND_LIST_GET_SUCCESS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -67,7 +70,9 @@ public BandRecommendResponse getRecommendedBands(Long userId, Long cursor, Integ
.map(UserRegions::getRegion)
.collect(Collectors.toSet());

List<Band> candidates = bandRepository.findAllByStatus(BandStatus.ACCEPTED);
List<Band> candidates = bandRepository.findAllByStatus(BandStatus.ACCEPTED).stream()
.filter(band -> withDummy || band.getId() > DUMMY_BAND_MAX_ID)
.toList();
List<Long> candidateIds = candidates.stream().map(Band::getId).toList();

LocalDateTime postSince = LocalDateTime.now().minusDays(RECENT_ACTIVITY_DAYS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<Long> candidateIds = new ArrayList<>(candidateBands.keySet());

// 노출 피로도 감점 : 후보에서 빼지 않고 점수만 깎는다 (FLOOR 덕에 유일한 매치는 그대로 남음).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Long> followedBandIds = List.of(100L, 200L, 300L);
Expand Down
Loading