diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/controller/ArtistsController.java b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/controller/ArtistsController.java index a753332a..b0432528 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/controller/ArtistsController.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/controller/ArtistsController.java @@ -1,5 +1,9 @@ package com.back.web7_9_codecrete_be.domain.artists.controller; +import com.back.web7_9_codecrete_be.domain.artists.dto.request.CreateRequest; +import com.back.web7_9_codecrete_be.domain.artists.dto.request.UpdateRequest; +import com.back.web7_9_codecrete_be.domain.artists.dto.response.ArtistListResponse; +import com.back.web7_9_codecrete_be.domain.artists.dto.response.ArtistDetailResponse; import com.back.web7_9_codecrete_be.domain.artists.service.ArtistService; import com.back.web7_9_codecrete_be.domain.artists.service.ArtistEnrichService; import com.back.web7_9_codecrete_be.global.rsData.RsData; @@ -8,10 +12,12 @@ import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RestController @RequestMapping("/api/v1/artists") @RequiredArgsConstructor -@Tag(name = "Artists", description = "공연에 대한 정보를 제공하는 API 입니다.") +@Tag(name = "Artists", description = "아티스트에 대한 정보를 제공하는 API 입니다.") public class ArtistsController { private final ArtistService artistService; private final ArtistEnrichService enrichService; @@ -23,7 +29,7 @@ public RsData saveArtist() { return RsData.success("아티스트 저장에 성공하였습니다.", saved); } - @Operation(summary = "아티스트 정보 보완", description = "아티스트 한국어 이름, 그룹 여부, 소속 그룹 정보 보완") + @Operation(summary = "아티스트 정보 보완", description = "아티스트 한국어 이름, 그룹 여부, 소속 그룹 정보를 보완합니다.") @PostMapping("/enrich") public RsData enrich( @RequestParam(required = false, defaultValue = "100") int limit @@ -31,4 +37,46 @@ public RsData enrich( int updated = enrichService.enrichArtist(limit); return RsData.success("enrich 성공", updated); } + + @Operation(summary = "아티스트 생성", description = "아티스트를 등록합니다.") + @PostMapping() + public RsData create( + @RequestBody CreateRequest reqBody + ) { + artistService.createArtist(reqBody.artistName(), reqBody.artistGroup(), reqBody.artistGroup(), reqBody.genreName()); + return RsData.success("아티스트 생성이 완료되었습니다.", null); + } + + @Operation(summary = "아티스트 목록 조회", description = "아티스트 전체 목록을 조회합니다.") + @GetMapping() + public RsData> list() { + return RsData.success("아티스트 전체 목록을 조회했습니다.", artistService.listArtist()); + } + + @Operation(summary = "아티스트 상세 조회", description = "아티스트의 상세 정보를 조회합니다.") + @GetMapping("/{id}") + public RsData artist( + @PathVariable Long id + ) { + return RsData.success("아티스트 상세 조회를 성공했습니다.", artistService.getArtistDetail(id)); + } + + @Operation(summary = "아티스트 정보 수정", description = "아티스트 정보를 수정합니다.") + @PatchMapping("/{id}") + public RsData update( + @PathVariable Long id, + @RequestBody UpdateRequest reqBody + ) { + artistService.updateArtist(id, reqBody); + return RsData.success("아티스트 정보 수정을 완료했습니다.", null); + } + + @Operation(summary = "아티스트 정보 삭제", description = "아티스트 정보를 삭제합니다.") + @DeleteMapping("/{id}") + public RsData delete( + @PathVariable Long id + ) { + artistService.delete(id); + return RsData.success("아티스트 정보를 삭제했습니다.", null); + } } diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/request/CreateRequest.java b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/request/CreateRequest.java index ee3035c3..4fef288d 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/request/CreateRequest.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/request/CreateRequest.java @@ -1,12 +1,9 @@ package com.back.web7_9_codecrete_be.domain.artists.dto.request; -import com.back.web7_9_codecrete_be.domain.artists.entity.Genre; - - public record CreateRequest( String artistName, String artistGroup, String artistType, - Genre genre + String genreName ) { } diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/request/UpdateRequest.java b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/request/UpdateRequest.java index eae63073..6e43712f 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/request/UpdateRequest.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/request/UpdateRequest.java @@ -1,12 +1,9 @@ package com.back.web7_9_codecrete_be.domain.artists.dto.request; -import com.back.web7_9_codecrete_be.domain.artists.entity.Genre; - - public record UpdateRequest( String artistName, String artistGroup, String artistType, - Genre genre + String genreName ) { } diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/response/AlbumResponse.java b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/response/AlbumResponse.java new file mode 100644 index 00000000..803459d3 --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/response/AlbumResponse.java @@ -0,0 +1,10 @@ +package com.back.web7_9_codecrete_be.domain.artists.dto.response; + +public record AlbumResponse( + String albumName, + String releaseDate, + String albumType, // album / single / ep + String imageUrl, + String spotifyUrl +) { +} diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/response/ArtistDetailResponse.java b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/response/ArtistDetailResponse.java new file mode 100644 index 00000000..91ed37d8 --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/response/ArtistDetailResponse.java @@ -0,0 +1,18 @@ +package com.back.web7_9_codecrete_be.domain.artists.dto.response; + +import java.util.List; + +public record ArtistDetailResponse( + String artistName, + String artistGroup, + String artistType, + String profileImageUrl, + long likeCount, + int totalAlbums, + double popularityRating, + String description, + List albums, + List topTracks, + List relatedArtists +) { +} diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/response/ArtistListResponse.java b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/response/ArtistListResponse.java new file mode 100644 index 00000000..9483abf7 --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/response/ArtistListResponse.java @@ -0,0 +1,17 @@ +package com.back.web7_9_codecrete_be.domain.artists.dto.response; + +import com.back.web7_9_codecrete_be.domain.artists.entity.Artist; + +public record ArtistListResponse( + String artistName, + String artistGroup, + String genreName +) { + public static ArtistListResponse from(Artist artist) { + return new ArtistListResponse( + artist.getArtistName(), + artist.getArtistGroup(), + artist.getGenre().getGenreName() + ); + } +} diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/response/RelatedArtistResponse.java b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/response/RelatedArtistResponse.java new file mode 100644 index 00000000..78d53a11 --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/response/RelatedArtistResponse.java @@ -0,0 +1,8 @@ +package com.back.web7_9_codecrete_be.domain.artists.dto.response; + +public record RelatedArtistResponse( + String artistName, + String imageUrl, + String spotifyArtistId +) { +} diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/response/TopTrackResponse.java b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/response/TopTrackResponse.java new file mode 100644 index 00000000..5714c91b --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/dto/response/TopTrackResponse.java @@ -0,0 +1,7 @@ +package com.back.web7_9_codecrete_be.domain.artists.dto.response; + +public record TopTrackResponse( + String trackName, + String spotifyUrl +) { +} diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/entity/Artist.java b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/entity/Artist.java index 0a5c2c41..23db1cc3 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/entity/Artist.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/entity/Artist.java @@ -44,11 +44,34 @@ public Artist(String spotifyArtistId, String artistName, String artistGroup, Str this.genre = genre; } + public Artist(String artistName, String artistGroup, String artistType, Genre genre) { + this.artistName = artistName; + this.artistGroup = artistGroup; + this.artistType = artistType; + this.genre = genre; + } + public void updateProfile(String nameKo, String artistGroup, String artistType) { this.nameKo = nameKo; this.artistGroup = artistGroup; // nullable this.artistType = artistType; // "SOLO" / "GROUP" } + public void changeName(String name) { + this.artistName = name; + } + + public void changeGroup(String group) { + this.artistGroup = group; + } + + public void changeType(String type) { + this.artistType = type; + } + + public void changeGenre(Genre genre) { + this.genre = genre; + } + } diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/repository/ArtistLikeRepository.java b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/repository/ArtistLikeRepository.java index 5d638eeb..ffc81403 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/repository/ArtistLikeRepository.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/repository/ArtistLikeRepository.java @@ -6,4 +6,5 @@ @Repository public interface ArtistLikeRepository extends JpaRepository { + long countByArtistId(Long artistId); } diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/repository/ArtistRepository.java b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/repository/ArtistRepository.java index 1113fb4c..e749f665 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/repository/ArtistRepository.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/repository/ArtistRepository.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository public interface ArtistRepository extends JpaRepository { @@ -14,4 +15,10 @@ public interface ArtistRepository extends JpaRepository { @Query("SELECT a FROM Artist a WHERE a.nameKo IS NULL ORDER BY a.id ASC") List findByNameKoIsNullOrderByIdAsc(Pageable pageable); + + boolean existsByArtistName(String artistName); + boolean existsByNameKo(String nameKo); + + List findTop5ByArtistGroupAndIdNot(String artistGroup, long excludeId); + List findTop5ByGenreIdAndIdNot(Long genreId, long excludeId); } diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/service/ArtistService.java b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/service/ArtistService.java index 6dc05896..ecda161a 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/service/ArtistService.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/service/ArtistService.java @@ -1,17 +1,111 @@ package com.back.web7_9_codecrete_be.domain.artists.service; +import com.back.web7_9_codecrete_be.domain.artists.dto.request.UpdateRequest; +import com.back.web7_9_codecrete_be.domain.artists.dto.response.ArtistListResponse; +import com.back.web7_9_codecrete_be.domain.artists.dto.response.ArtistDetailResponse; +import com.back.web7_9_codecrete_be.domain.artists.entity.Artist; +import com.back.web7_9_codecrete_be.domain.artists.entity.Genre; +import com.back.web7_9_codecrete_be.domain.artists.repository.ArtistRepository; +import com.back.web7_9_codecrete_be.domain.artists.repository.ArtistLikeRepository; +import com.back.web7_9_codecrete_be.global.error.code.ArtistErrorCode; +import com.back.web7_9_codecrete_be.global.error.exception.BusinessException; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.List; + @Service @RequiredArgsConstructor public class ArtistService { private final SpotifyService spotifyService; + private final ArtistRepository artistRepository; + private final GenreService genreService; + private final ArtistLikeRepository artistLikeRepository; @Transactional public int setArtist() { return spotifyService.seedKoreanArtists300(); } + + @Transactional + public Artist createArtist(String artistName, String artistGroup, String artistType, String genreName) { + Genre genre = genreService.findByGenreName(genreName); + if(artistRepository.existsByArtistName(artistName) || artistRepository.existsByNameKo(artistName)) { + throw new BusinessException(ArtistErrorCode.ARTIST_ALREADY_EXISTS); + } + Artist artist = new Artist(artistName, artistGroup, artistType, genre); + artistRepository.save(artist); + return artist; + } + + @Transactional(readOnly = true) + public List listArtist() { + return artistRepository.findAll().stream() + .map(ArtistListResponse::from) + .toList(); + } + + @Transactional(readOnly = true) + public ArtistDetailResponse getArtistDetail(Long artistId) { + Artist artist = artistRepository.findById(artistId) + .orElseThrow(() -> new BusinessException(ArtistErrorCode.ARTIST_NOT_FOUND)); + + if (artist.getSpotifyArtistId() == null) { + throw new BusinessException(ArtistErrorCode.SPOTIFY_NOT_FOUND); + } + + long likeCount = artistLikeRepository.countByArtistId(artistId); + + return spotifyService.getArtistDetail( + artist.getSpotifyArtistId(), + artist.getArtistGroup(), + artist.getArtistType(), + likeCount, + artist.getId(), + artist.getGenre() != null ? artist.getGenre().getId() : null + ); + } + + @Transactional + public void updateArtist(Long id, UpdateRequest req) { + Artist artist = artistRepository.findById(id) + .orElseThrow(() -> new BusinessException(ArtistErrorCode.ARTIST_NOT_FOUND)); + + boolean changed = false; + + if (req.artistName() != null && !req.artistName().isBlank()) { + artist.changeName(req.artistName().trim()); + changed = true; + } + + if (req.artistGroup() != null && !req.artistGroup().isBlank()) { + artist.changeGroup(req.artistGroup().trim()); + changed = true; + } + + if (req.artistType() != null && !req.artistType().isBlank()) { + artist.changeType(req.artistType().trim()); + changed = true; + } + + if (req.genreName() != null && !req.genreName().isBlank()) { + Genre genre = genreService.findByGenreName(req.genreName().trim()); + artist.changeGenre(genre); + changed = true; + } + + if (!changed) { + throw new BusinessException(ArtistErrorCode.INVALID_UPDATE_REQUEST); // "수정할 값이 없습니다" + } + } + + @Transactional + public void delete(Long id) { + Artist artist = artistRepository.findById(id) + .orElseThrow(() -> new BusinessException(ArtistErrorCode.ARTIST_NOT_FOUND)); + artistRepository.delete(artist); + } + } diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/service/GenreService.java b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/service/GenreService.java new file mode 100644 index 00000000..e6d600eb --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/service/GenreService.java @@ -0,0 +1,22 @@ +package com.back.web7_9_codecrete_be.domain.artists.service; + +import com.back.web7_9_codecrete_be.domain.artists.entity.Genre; +import com.back.web7_9_codecrete_be.domain.artists.repository.GenreRepository; +import com.back.web7_9_codecrete_be.global.error.code.GenreErrorCode; +import com.back.web7_9_codecrete_be.global.error.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class GenreService { + + private final GenreRepository genreRepository; + + public Genre findByGenreName(String genreName) { + String normalized = genreName.trim().toLowerCase(); + Genre genre = genreRepository.findByGenreName(normalized) + .orElseThrow(() -> new BusinessException(GenreErrorCode.GENRE_NOT_FOUND)); + return genre; + } +} diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/service/SpotifyService.java b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/service/SpotifyService.java index 259cc207..b80b455c 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/artists/service/SpotifyService.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/artists/service/SpotifyService.java @@ -1,20 +1,35 @@ package com.back.web7_9_codecrete_be.domain.artists.service; -import com.back.web7_9_codecrete_be.domain.artists.entity.Artist; +import com.back.web7_9_codecrete_be.domain.artists.dto.response.AlbumResponse; +import com.back.web7_9_codecrete_be.domain.artists.dto.response.ArtistDetailResponse; +import com.back.web7_9_codecrete_be.domain.artists.dto.response.RelatedArtistResponse; +import com.back.web7_9_codecrete_be.domain.artists.dto.response.TopTrackResponse; import com.back.web7_9_codecrete_be.domain.artists.entity.Genre; import com.back.web7_9_codecrete_be.domain.artists.repository.ArtistRepository; import com.back.web7_9_codecrete_be.domain.artists.repository.GenreRepository; import com.back.web7_9_codecrete_be.global.error.code.ArtistErrorCode; import com.back.web7_9_codecrete_be.global.error.exception.BusinessException; import com.back.web7_9_codecrete_be.global.spotify.SpotifyClient; +import com.neovisionaries.i18n.CountryCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import se.michaelthelin.spotify.SpotifyApi; +import se.michaelthelin.spotify.enums.AlbumType; +import se.michaelthelin.spotify.exceptions.detailed.NotFoundException; +import se.michaelthelin.spotify.model_objects.specification.AlbumSimplified; +import se.michaelthelin.spotify.model_objects.specification.Artist; +import se.michaelthelin.spotify.model_objects.specification.Image; import se.michaelthelin.spotify.model_objects.specification.Paging; +import se.michaelthelin.spotify.model_objects.specification.Track; +import java.util.Arrays; import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toList; @Slf4j @Service @@ -48,8 +63,7 @@ public int seedKoreanArtists300() { int offset = 0; while (totalSaved < targetCount) { - Paging paging = - api.searchArtists(q) + Paging paging = api.searchArtists(q) .limit(limit) .offset(offset) .build() @@ -73,8 +87,8 @@ public int seedKoreanArtists300() { String artistType = inferArtistType(spotifyArtist); - // ✅ seed 단계에서는 Wikidata 호출 금지 (속도/실패 리스크) - Artist artist = new Artist( + com.back.web7_9_codecrete_be.domain.artists.entity.Artist artistEntity = + new com.back.web7_9_codecrete_be.domain.artists.entity.Artist( spotifyId, name.trim(), null, // artistGroup @@ -82,7 +96,7 @@ public int seedKoreanArtists300() { genre ); - artistRepository.save(artist); + artistRepository.save(artistEntity); totalSaved++; } @@ -113,7 +127,7 @@ public int seedKoreanArtists300() { "korean indie", "korean rock" ); - private boolean isLikelyKoreanMusic(se.michaelthelin.spotify.model_objects.specification.Artist a) { + private boolean isLikelyKoreanMusic(Artist a) { String[] genres = a.getGenres(); if (genres != null) { for (String g : genres) { @@ -124,12 +138,11 @@ private boolean isLikelyKoreanMusic(se.michaelthelin.spotify.model_objects.speci } } } - // 한글 이름 보조 필터 String name = a.getName(); return name != null && name.matches(".*[가-힣].*"); } - private String pickMainGenreName(se.michaelthelin.spotify.model_objects.specification.Artist a) { + private String pickMainGenreName(Artist a) { String[] genres = a.getGenres(); if (genres == null || genres.length == 0) return "k-pop"; @@ -148,7 +161,7 @@ private String pickMainGenreName(se.michaelthelin.spotify.model_objects.specific return "k-pop"; } - private String inferArtistType(se.michaelthelin.spotify.model_objects.specification.Artist a) { + private String inferArtistType(Artist a) { String[] genres = a.getGenres(); if (genres != null) { for (String g : genres) { @@ -164,4 +177,203 @@ private Genre findOrCreateGenreByName(String genreName, String genreGroup) { return genreRepository.findByGenreName(genreName) .orElseGet(() -> genreRepository.save(new Genre(genreName, genreGroup, null))); } + + @Transactional(readOnly = true) + public ArtistDetailResponse getArtistDetail( + String spotifyArtistId, + String artistGroup, + String artistType, + long likeCount, + long artistId, + Long genreId + ) { + try { + SpotifyApi api = spotifyClient.getAuthorizedApi(); + + Artist artist = api.getArtist(spotifyArtistId).build().execute(); // 메인 정보는 실패 시 예외 발생 + + Track[] topTracks = safeGetTopTracks(api, spotifyArtistId); + Paging albums = safeGetAlbums(api, spotifyArtistId); + + List relatedResponses = safeGetRelated( + api, + artist, + artistGroup, + artistId, + genreId + ); + + return new ArtistDetailResponse( + artist.getName(), + artistGroup, + artistType, + pickImageUrl(artist.getImages()), + likeCount, + albums != null ? albums.getTotal() : 0, + artist.getPopularity(), // (너가 별점으로 바꾸고 싶으면 여기 가공) + "", // Spotify에서는 설명을 제공하지 않아 공란 처리 + toAlbumResponses(albums != null ? albums.getItems() : null, spotifyArtistId), + toTopTrackResponses(topTracks), + relatedResponses + ); + } catch (Exception e) { + log.error("Spotify 상세 조회 실패: artistId={}", spotifyArtistId, e); + throw new BusinessException(ArtistErrorCode.SPOTIFY_API_ERROR); + } + } + + private Track[] safeGetTopTracks(SpotifyApi api, String artistId) { + try { + return api.getArtistsTopTracks(artistId, CountryCode.KR) + .build() + .execute(); + } catch (NotFoundException nf) { + log.warn("Spotify top tracks not found: artistId={}", artistId); + return new Track[0]; + } catch (Exception e) { + log.warn("Spotify top tracks fetch error: artistId={}", artistId, e); + return new Track[0]; + } + } + + private Paging safeGetAlbums(SpotifyApi api, String artistId) { + try { + return api.getArtistsAlbums(artistId) + .market(CountryCode.KR) + .limit(20) + .build() + .execute(); + } catch (NotFoundException nf) { + log.warn("Spotify albums not found: artistId={}", artistId); + return null; + } catch (Exception e) { + log.warn("Spotify albums fetch error: artistId={}", artistId, e); + return null; + } + } + + /** + * ✅ related artists + * - 정상 호출 + * - related가 비거나 404면 fallback(장르 기반 검색)으로 대체 + */ + private List safeGetRelated( + SpotifyApi api, + Artist me, + String artistGroup, + long artistId, + Long genreId + ) { + String id = (me == null || me.getId() == null) ? null : me.getId().trim(); + if (id == null || id.isBlank()) { + log.warn("Spotify related artists skipped: empty artist id"); + return List.of(); + } + + try { + Artist[] related = api.getArtistsRelatedArtists(id).build().execute(); + if (related != null && related.length > 0) { + log.info("Spotify related artists fetched: size={} spotifyArtistId={}", related.length, id); + return toRelatedArtistResponses(related); + } + + log.info("Spotify related artists empty -> fallback group/genre. spotifyArtistId={}", id); + return fallbackRelatedFromDb(artistGroup, artistId, genreId); + + } catch (NotFoundException e) { + log.warn("Spotify related artists NotFound (404) -> fallback group/genre. spotifyArtistId={}", id); + return fallbackRelatedFromDb(artistGroup, artistId, genreId); + + } catch (Exception e) { + log.error("Spotify related artists fetch error: spotifyArtistId={}", id, e); + return fallbackRelatedFromDb(artistGroup, artistId, genreId); + } + } + + private List fallbackRelatedFromDb(String artistGroup, long artistId, Long genreId) { + try { + if (artistGroup != null && !artistGroup.isBlank()) { + return artistRepository.findTop5ByArtistGroupAndIdNot(artistGroup, artistId).stream() + .map(a -> new RelatedArtistResponse( + a.getArtistName(), + null, + a.getSpotifyArtistId() + )) + .toList(); + } + if (genreId != null) { + return artistRepository.findTop5ByGenreIdAndIdNot(genreId, artistId).stream() + .map(a -> new RelatedArtistResponse( + a.getArtistName(), + null, + a.getSpotifyArtistId() + )) + .toList(); + } + } catch (Exception e) { + log.error("Fallback related from DB failed", e); + } + return List.of(); + } + + private String pickImageUrl(Image[] images) { + if (images == null || images.length == 0) return null; + return Arrays.stream(images) + .filter(Objects::nonNull) + .findFirst() + .map(Image::getUrl) + .orElse(null); + } + + private List toAlbumResponses(AlbumSimplified[] items, String artistId) { + if (items == null) return List.of(); + return Stream.of(items) + .filter(Objects::nonNull) + // 앨범 타입 필터: album / single / ep + .filter(a -> a.getAlbumType() == AlbumType.ALBUM + || a.getAlbumType() == AlbumType.SINGLE + || a.getAlbumType() == AlbumType.COMPILATION) + // 해당 아티스트가 참여한 앨범만 필터링 + .filter(a -> { + if (a.getArtists() == null) return false; + return Arrays.stream(a.getArtists()) + .anyMatch(ar -> ar != null && artistId != null && artistId.equals(ar.getId())); + }) + .map(a -> new AlbumResponse( + a.getName(), + a.getReleaseDate(), + albumTypeToString(a.getAlbumType()), + pickImageUrl(a.getImages()), + a.getExternalUrls() != null ? a.getExternalUrls().get("spotify") : null + )) + .collect(toList()); + } + + private String albumTypeToString(AlbumType type) { + if (type == null) return null; + return type.getType(); + } + + private List toTopTrackResponses(Track[] tracks) { + if (tracks == null) return List.of(); + return Stream.of(tracks) + .filter(Objects::nonNull) + .map(t -> new TopTrackResponse( + t.getName(), + t.getExternalUrls() != null ? t.getExternalUrls().get("spotify") : null + )) + .collect(toList()); + } + + private List toRelatedArtistResponses(Artist[] artists) { + if (artists == null) return List.of(); + return Stream.of(artists) + .filter(Objects::nonNull) + .map(a -> new RelatedArtistResponse( + a.getName(), + pickImageUrl(a.getImages()), + a.getId() + )) + .collect(toList()); + } } diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/users/entity/User.java b/src/main/java/com/back/web7_9_codecrete_be/domain/users/entity/User.java index 1b218e85..d139003b 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/users/entity/User.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/users/entity/User.java @@ -106,5 +106,4 @@ public void restore() { this.status = UserStatus.ACTIVE; this.deletedDate = null; } -} - +} \ No newline at end of file diff --git a/src/main/java/com/back/web7_9_codecrete_be/global/error/code/ArtistErrorCode.java b/src/main/java/com/back/web7_9_codecrete_be/global/error/code/ArtistErrorCode.java index 795fbd24..d8078183 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/global/error/code/ArtistErrorCode.java +++ b/src/main/java/com/back/web7_9_codecrete_be/global/error/code/ArtistErrorCode.java @@ -11,7 +11,10 @@ public enum ArtistErrorCode implements ErrorCode { ARTIST_SEED_FAILED(HttpStatus.BAD_REQUEST, "AT-100", "아티스트 정보 저장에 실패했습니다."), SPOTIFY_API_ERROR(HttpStatus.BAD_GATEWAY, "AT-101", "Spotify API 연동 중 오류가 발생했습니다."), - ARTIST_NOT_FOUND(HttpStatus.NOT_FOUND, "AT-102", "아티스트를 찾을 수 없습니다."); + ARTIST_NOT_FOUND(HttpStatus.NOT_FOUND, "AT-102", "아티스트를 찾을 수 없습니다."), + ARTIST_ALREADY_EXISTS(HttpStatus.CONFLICT, "AT-103", "이미 존재하는 아티스트입니다."), + SPOTIFY_NOT_FOUND(HttpStatus.NOT_FOUND, "AT-104", "존재하지 않는 Spotify Artist Key 입니다."), + INVALID_UPDATE_REQUEST(HttpStatus.BAD_REQUEST, "AT-105", "수정할 내용이 없습니다."); private final HttpStatus status; private final String code; diff --git a/src/main/java/com/back/web7_9_codecrete_be/global/error/code/GenreErrorCode.java b/src/main/java/com/back/web7_9_codecrete_be/global/error/code/GenreErrorCode.java new file mode 100644 index 00000000..f864115d --- /dev/null +++ b/src/main/java/com/back/web7_9_codecrete_be/global/error/code/GenreErrorCode.java @@ -0,0 +1,16 @@ +package com.back.web7_9_codecrete_be.global.error.code; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum GenreErrorCode implements ErrorCode { + + GENRE_NOT_FOUND(HttpStatus.NOT_FOUND, "G-100", "장르를 찾을 수 없습니다."); + + private final HttpStatus status; + private final String code; + private final String message; +}