Skip to content

Commit 7894a5d

Browse files
Merge pull request #176 from prgrms-web-devcourse-final-project/feat/#172
[Artist] 아티스트 목록 요구사항 구현
2 parents 723428c + 8903a43 commit 7894a5d

9 files changed

Lines changed: 137 additions & 30 deletions

File tree

src/main/java/com/back/web7_9_codecrete_be/domain/artists/controller/ArtistsController.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.back.web7_9_codecrete_be.domain.artists.controller;
22

3+
import com.back.web7_9_codecrete_be.domain.artists.entity.ArtistSort;
34
import com.back.web7_9_codecrete_be.domain.artists.dto.request.CreateRequest;
45
import com.back.web7_9_codecrete_be.domain.artists.dto.request.SearchRequest;
56
import com.back.web7_9_codecrete_be.domain.artists.dto.request.UpdateRequest;
@@ -16,6 +17,8 @@
1617
import io.swagger.v3.oas.annotations.tags.Tag;
1718
import jakarta.validation.Valid;
1819
import lombok.RequiredArgsConstructor;
20+
import org.springframework.data.domain.Pageable;
21+
import org.springframework.data.domain.Slice;
1922
import org.springframework.web.bind.annotation.*;
2023

2124
import java.util.List;
@@ -63,18 +66,24 @@ public RsData<Void> create(
6366
return RsData.success("아티스트 생성이 완료되었습니다.", null);
6467
}
6568

66-
@Operation(summary = "아티스트 목록 조회", description = "아티스트 전체 목록을 조회합니다.")
69+
@Operation(summary = "아티스트 목록 조회",
70+
description = "아티스트 전체 목록을 조회합니다(NAME: 이름순 / LIKE: 인기순 (좋아요 많은 순)")
6771
@GetMapping()
68-
public RsData<List<ArtistListResponse>> list() {
69-
return RsData.success("아티스트 전체 목록을 조회했습니다.", artistService.listArtist());
72+
public RsData<Slice<ArtistListResponse>> list(
73+
Pageable pageable,
74+
@RequestParam(required = false) ArtistSort sort
75+
) {
76+
User user = rq.getUserOrNull(); // 로그인하지 않은 경우 null
77+
return RsData.success("아티스트 전체 목록을 조회했습니다.", artistService.listArtist(pageable, user, sort));
7078
}
7179

7280
@Operation(summary = "아티스트 상세 조회", description = "아티스트의 상세 정보를 조회합니다.")
7381
@GetMapping("/{id}")
7482
public RsData<ArtistDetailResponse> artist(
7583
@PathVariable Long id
7684
) {
77-
return RsData.success("아티스트 상세 조회를 성공했습니다.", artistService.getArtistDetail(id));
85+
User user = rq.getUserOrNull(); // 로그인하지 않은 경우 null
86+
return RsData.success("아티스트 상세 조회를 성공했습니다.", artistService.getArtistDetail(id, user));
7887
}
7988

8089
@Operation(summary = "아티스트 정보 수정", description = "아티스트 정보를 수정합니다.")

src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/response/ArtistDetailResponse.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ public record ArtistDetailResponse(
3737
List<TopTrackResponse> topTracks,
3838

3939
@Schema(description = "아티스트와 관련 있는 다른 아티스트 목록입니다.")
40-
List<RelatedArtistResponse> relatedArtists
40+
List<RelatedArtistResponse> relatedArtists,
41+
42+
@Schema(description = "로그인한 유저의 좋아요 여부입니다. 비회원인 경우 false입니다.")
43+
Boolean isLiked
4144
) {
4245
}

src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/response/ArtistListResponse.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ public record ArtistListResponse(
2020
int likeCount,
2121

2222
@Schema(description = "아티스트 프로필 사진 URL 입니다.")
23-
String imageUrl
23+
String imageUrl,
24+
25+
@Schema(description = "로그인한 유저의 좋아요 여부입니다. 비회원인 경우 false입니다.")
26+
Boolean isLiked
2427
) {
2528
public static ArtistListResponse from(Artist artist) {
2629
return new ArtistListResponse(
@@ -29,7 +32,20 @@ public static ArtistListResponse from(Artist artist) {
2932
artist.getArtistGroup(),
3033
artist.getGenre().getGenreName(),
3134
artist.getLikeCount(),
32-
artist.getImageUrl()
35+
artist.getImageUrl(),
36+
false // 기본값은 false
37+
);
38+
}
39+
40+
public static ArtistListResponse from(Artist artist, boolean isLiked) {
41+
return new ArtistListResponse(
42+
artist.getId(),
43+
artist.getArtistName(),
44+
artist.getArtistGroup(),
45+
artist.getGenre().getGenreName(),
46+
artist.getLikeCount(),
47+
artist.getImageUrl(),
48+
isLiked
3349
);
3450
}
3551
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.back.web7_9_codecrete_be.domain.artists.entity;
2+
3+
public enum ArtistSort {
4+
NAME, // 이름순
5+
LIKE // 인기순 (좋아요 많은 순)
6+
}

src/main/java/com/back/web7_9_codecrete_be/domain/artists/repository/ArtistRepository.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.back.web7_9_codecrete_be.domain.artists.entity.Artist;
44
import org.springframework.data.domain.Pageable;
5+
import org.springframework.data.domain.Slice;
56
import org.springframework.data.jpa.repository.JpaRepository;
67
import org.springframework.data.jpa.repository.Query;
78
import org.springframework.stereotype.Repository;
@@ -25,4 +26,21 @@ public interface ArtistRepository extends JpaRepository<Artist, Long> {
2526
List<Artist> findTop5ByGenreIdAndIdNot(Long genreId, long excludeId);
2627

2728
List<Artist> findAllByArtistNameContainingIgnoreCaseOrNameKoContainingIgnoreCase(String artistName1, String artistName2);
29+
30+
Slice<Artist> findAllBy(Pageable pageable);
31+
32+
// 이름순 정렬 (nameKo 우선, 없으면 artistName) - 가나다순
33+
@Query("""
34+
SELECT a FROM Artist a
35+
ORDER BY
36+
CASE
37+
WHEN a.nameKo IS NOT NULL AND a.nameKo != '' THEN a.nameKo
38+
ELSE a.artistName
39+
END ASC
40+
""")
41+
Slice<Artist> findAllOrderByName(Pageable pageable);
42+
43+
// 인기순 정렬 (좋아요 많은 순)
44+
@Query("SELECT a FROM Artist a ORDER BY a.likeCount DESC")
45+
Slice<Artist> findAllOrderByLikeCountDesc(Pageable pageable);
2846
}

src/main/java/com/back/web7_9_codecrete_be/domain/artists/service/ArtistEnrichService.java

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ protected void enrichSingleArtist(Artist artist) {
189189
// 기존 artistType이 있으면 유지, 없으면 가져온 값 사용
190190
String artistTypeStr = result.artistType != null ? result.artistType :
191191
(artist.getArtistType() != null ? artist.getArtistType().name() : null);
192-
192+
193193
// String을 ArtistType enum으로 변환
194194
ArtistType artistType;
195195
if (artistTypeStr != null) {
@@ -329,8 +329,8 @@ private EnrichResult enrichArtist(Artist artist) {
329329
artist.getMusicBrainzId(), artistGroup);
330330
}
331331
}
332-
}
333-
332+
}
333+
334334
if (artistType != null || artistGroup != null) {
335335
log.info("✅ -1단계 성공 (MBID 직접 검색): artistId={}, mbid={}, qid={}, type={}, group={}",
336336
artist.getId(), artist.getMusicBrainzId(), mbidWikidataQid, artistType, artistGroup);
@@ -459,13 +459,13 @@ private EnrichResult enrichArtist(Artist artist) {
459459
artist.getSpotifyArtistId(), artistGroup, groups);
460460
} else {
461461
// SPARQL로 못 찾으면 기존 방식 시도
462-
artistGroup = resolveGroupNameFromWikidata(entity);
463-
if (artistGroup != null) {
464-
source += "Wikidata ";
462+
artistGroup = resolveGroupNameFromWikidata(entity);
463+
if (artistGroup != null) {
464+
source += "Wikidata ";
465465
log.debug("소속 그룹 추출 성공 (Wikidata): spotifyId={}, group={}",
466466
artist.getSpotifyArtistId(), artistGroup);
467-
}
468-
}
467+
}
468+
}
469469
}
470470
// Wikidata에서 못 찾으면 MusicBrainz에서 시도
471471
if (artistGroup == null && mbInfo.getArtistGroup() != null &&
@@ -499,9 +499,9 @@ private EnrichResult enrichArtist(Artist artist) {
499499

500500
// MBID 상세 조회
501501
Optional<MusicBrainzClient.ArtistInfo> mbInfoOpt = musicBrainzClient.getArtistByMbid(mbid);
502-
if (mbInfoOpt.isPresent()) {
503-
MusicBrainzClient.ArtistInfo mbInfo = mbInfoOpt.get();
504-
502+
if (mbInfoOpt.isPresent()) {
503+
MusicBrainzClient.ArtistInfo mbInfo = mbInfoOpt.get();
504+
505505
// Type 덮어쓰기 정책: 합의(consensus) 방식
506506
if (mbInfo.getArtistType() != null && !mbInfo.getArtistType().isBlank() &&
507507
"spotify-url".equals(mbidSource)) {
@@ -517,7 +517,7 @@ private EnrichResult enrichArtist(Artist artist) {
517517
} else if (wdType == null) {
518518
// Wikidata에서 type을 못 찾았으면 MusicBrainz 사용
519519
artistType = mbType;
520-
source += "MusicBrainz ";
520+
source += "MusicBrainz ";
521521
log.debug("artistType 추출 성공 (MusicBrainz, Wikidata type 없음): spotifyId={}, mbid={}, type={}",
522522
artist.getSpotifyArtistId(), mbid, mbType);
523523
} else {
@@ -530,8 +530,8 @@ private EnrichResult enrichArtist(Artist artist) {
530530
// SOLO일 때만 group 추출 (MusicBrainz만 사용, Wikidata는 이미 시도했거나 없음)
531531
if ("SOLO".equals(artistType) && artistGroup == null) {
532532
if (mbInfo.getArtistGroup() != null && !mbInfo.getArtistGroup().isBlank()) {
533-
artistGroup = mbInfo.getArtistGroup();
534-
source += "MusicBrainz ";
533+
artistGroup = mbInfo.getArtistGroup();
534+
source += "MusicBrainz ";
535535
log.debug("소속 그룹 추출 성공 (MusicBrainz): spotifyId={}, mbid={}, group={}",
536536
artist.getSpotifyArtistId(), mbid, artistGroup);
537537
}
@@ -619,7 +619,7 @@ private EnrichResult enrichArtist(Artist artist) {
619619
}
620620

621621
return new EnrichResult(nameKo, artistGroup, artistType, source.trim());
622-
}
622+
}
623623

624624
// Wikidata 엔티티에서 정보 추출 (한국이름-활동명, 소속그룹, 솔로/그룹)
625625
private EnrichResult extractInfoFromWikidata(JsonNode entity) {

src/main/java/com/back/web7_9_codecrete_be/domain/artists/service/ArtistService.java

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.back.web7_9_codecrete_be.domain.artists.service;
22

3+
import com.back.web7_9_codecrete_be.domain.artists.entity.ArtistSort;
34
import com.back.web7_9_codecrete_be.domain.artists.dto.request.UpdateRequest;
45
import com.back.web7_9_codecrete_be.domain.artists.dto.response.ArtistListResponse;
56
import com.back.web7_9_codecrete_be.domain.artists.dto.response.ArtistDetailResponse;
@@ -16,11 +17,16 @@
1617
import com.back.web7_9_codecrete_be.global.error.code.ArtistErrorCode;
1718
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
1819
import lombok.AccessLevel;
20+
import org.springframework.data.domain.PageRequest;
21+
import org.springframework.data.domain.Pageable;
22+
import org.springframework.data.domain.Slice;
1923
import org.springframework.transaction.annotation.Transactional;
2024
import lombok.RequiredArgsConstructor;
2125
import org.springframework.stereotype.Service;
2226

27+
import java.util.HashSet;
2328
import java.util.List;
29+
import java.util.Set;
2430

2531
@Service
2632
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
@@ -57,14 +63,42 @@ public Artist createArtist(String artistName, String artistGroup, ArtistType art
5763
}
5864

5965
@Transactional(readOnly = true)
60-
public List<ArtistListResponse> listArtist() {
61-
return artistRepository.findAll().stream()
62-
.map(ArtistListResponse::from)
63-
.toList();
66+
public Slice<ArtistListResponse> listArtist(Pageable pageable, User user, ArtistSort sort) {
67+
// Pageable의 sort를 제거 (우리가 정의한 sort 파라미터만 사용)
68+
Pageable pageableWithoutSort = PageRequest.of(
69+
pageable.getPageNumber(),
70+
pageable.getPageSize()
71+
);
72+
73+
// 로그인한 유저가 좋아요한 아티스트 ID 목록 조회
74+
Set<Long> likedArtistIds = new HashSet<>();
75+
if (user != null) {
76+
List<Long> artistIds = artistLikeRepository.findArtistIdsByUserId(user.getId());
77+
likedArtistIds.addAll(artistIds);
78+
}
79+
80+
final Set<Long> finalLikedArtistIds = likedArtistIds;
81+
82+
// 정렬에 따라 다른 쿼리 사용
83+
Slice<Artist> artistSlice;
84+
if (sort == null) {
85+
// sort가 없으면 기본 정렬 (id 순)
86+
artistSlice = artistRepository.findAllBy(pageableWithoutSort);
87+
} else {
88+
artistSlice = switch (sort) {
89+
case NAME -> artistRepository.findAllOrderByName(pageableWithoutSort);
90+
case LIKE -> artistRepository.findAllOrderByLikeCountDesc(pageableWithoutSort);
91+
};
92+
}
93+
94+
return artistSlice.map(artist -> {
95+
boolean isLiked = finalLikedArtistIds.contains(artist.getId());
96+
return ArtistListResponse.from(artist, isLiked);
97+
});
6498
}
6599

66100
@Transactional(readOnly = true)
67-
public ArtistDetailResponse getArtistDetail(Long artistId) {
101+
public ArtistDetailResponse getArtistDetail(Long artistId, User user) {
68102
Artist artist = artistRepository.findById(artistId)
69103
.orElseThrow(() -> new BusinessException(ArtistErrorCode.ARTIST_NOT_FOUND));
70104

@@ -74,13 +108,20 @@ public ArtistDetailResponse getArtistDetail(Long artistId) {
74108

75109
long likeCount = artistLikeRepository.countByArtistId(artistId);
76110

111+
// 로그인한 유저의 좋아요 여부 확인
112+
boolean isLiked = false;
113+
if (user != null) {
114+
isLiked = artistLikeRepository.existsByArtistAndUser(artist, user);
115+
}
116+
77117
return spotifyService.getArtistDetail(
78118
artist.getSpotifyArtistId(),
79119
artist.getArtistGroup(),
80120
artist.getArtistType(),
81121
likeCount,
82122
artist.getId(),
83-
artist.getGenre() != null ? artist.getGenre().getId() : null
123+
artist.getGenre() != null ? artist.getGenre().getId() : null,
124+
isLiked
84125
);
85126
}
86127

src/main/java/com/back/web7_9_codecrete_be/domain/artists/service/SpotifyService.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ public ArtistDetailResponse getArtistDetail(
190190
ArtistType artistType,
191191
long likeCount,
192192
long artistId,
193-
Long genreId
193+
Long genreId,
194+
boolean isLiked
194195
) {
195196
try {
196197
SpotifyApi api = spotifyClient.getAuthorizedApi();
@@ -219,7 +220,8 @@ public ArtistDetailResponse getArtistDetail(
219220
"", // 설명
220221
toAlbumResponses(albums != null ? albums.getItems() : null, spotifyArtistId),
221222
toTopTrackResponses(topTracks),
222-
relatedResponses
223+
relatedResponses,
224+
isLiked
223225
);
224226
} catch (Exception e) {
225227
log.error("Spotify 상세 조회 실패: artistId={}", spotifyArtistId, e);

src/main/java/com/back/web7_9_codecrete_be/global/rq/Rq.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ public User getUser() {
4545
return userDetails.getUser();
4646
}
4747

48+
// 현재 인증된 사용자 정보 가져오기 (로그인하지 않은 경우 null 반환)
49+
public User getUserOrNull() {
50+
try {
51+
return getUser();
52+
} catch (BusinessException e) {
53+
if (e.getErrorCode() == AuthErrorCode.UNAUTHORIZED_USER) {
54+
return null;
55+
}
56+
throw e;
57+
}
58+
}
59+
4860
// 쿠키 설정
4961
public void setCookie(String name, String value, long maxAge) {
5062
String safeValue = value != null ? value : "";

0 commit comments

Comments
 (0)