Skip to content

Commit c65791f

Browse files
Merge pull request #158 from prgrms-web-devcourse-final-project/feat/#123
[Artist] 아티스트 정보 보완
2 parents cd03fe9 + 4ba4b73 commit c65791f

7 files changed

Lines changed: 1452 additions & 145 deletions

File tree

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ public RsData<Integer> enrich(
4545
return RsData.success("enrich 성공", updated);
4646
}
4747

48+
@Operation(summary = "MusicBrainz ID 수집", description = "아티스트의 MusicBrainz ID를 수집합니다.")
49+
@PostMapping("/musicbrainz-id")
50+
public RsData<Integer> fetchMusicBrainzIds(
51+
@RequestParam(required = false, defaultValue = "100") int limit
52+
) {
53+
int updated = enrichService.fetchMusicBrainzIds(limit);
54+
return RsData.success("MusicBrainz ID 수집 성공", updated);
55+
}
56+
4857
@Operation(summary = "아티스트 생성", description = "아티스트를 등록합니다.")
4958
@PostMapping()
5059
public RsData<Void> create(

src/main/java/com/back/web7_9_codecrete_be/domain/artists/entity/Artist.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ public class Artist {
3434
@Column(name = "spotify_artist_id", unique = true)
3535
private String spotifyArtistId;
3636

37+
@Column(name = "musicbrainz_id")
38+
private String musicBrainzId;
39+
3740
@Column(name = "name_ko", length = 200)
3841
private String nameKo;
3942

@@ -89,4 +92,8 @@ public void decreaseLikeCount() {
8992
this.likeCount--;
9093
}
9194
}
95+
96+
public void setMusicBrainzId(String musicBrainzId) {
97+
this.musicBrainzId = musicBrainzId;
98+
}
9299
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ public interface ArtistRepository extends JpaRepository<Artist, Long> {
1515
@Query("SELECT a FROM Artist a WHERE a.nameKo IS NULL ORDER BY a.id ASC")
1616
List<Artist> findByNameKoIsNullOrderByIdAsc(Pageable pageable);
1717

18+
@Query("SELECT a FROM Artist a WHERE a.musicBrainzId IS NULL ORDER BY a.id ASC")
19+
List<Artist> findByMusicBrainzIdIsNullOrderByIdAsc(Pageable pageable);
20+
1821
boolean existsByArtistName(String artistName);
1922
boolean existsByNameKo(String nameKo);
2023

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

Lines changed: 850 additions & 84 deletions
Large diffs are not rendered by default.
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package com.back.web7_9_codecrete_be.global.flo;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import lombok.Getter;
6+
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.http.HttpEntity;
9+
import org.springframework.http.HttpHeaders;
10+
import org.springframework.http.HttpMethod;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.stereotype.Component;
13+
import org.springframework.web.client.HttpClientErrorException;
14+
import org.springframework.web.client.RestTemplate;
15+
16+
import java.net.URLEncoder;
17+
import java.nio.charset.StandardCharsets;
18+
import java.util.Optional;
19+
20+
@Slf4j
21+
@Component
22+
@RequiredArgsConstructor
23+
public class FloClient {
24+
25+
private static final String FLO_API_BASE = "https://www.music-flo.com/api";
26+
27+
private final RestTemplate restTemplate;
28+
private final ObjectMapper objectMapper = new ObjectMapper();
29+
30+
// FLO에서 아티스트 정보를 검색하여 한국어 이름 가져옴
31+
public Optional<ArtistInfo> searchArtist(String artistName) {
32+
try {
33+
String encodedName = URLEncoder.encode(artistName, StandardCharsets.UTF_8);
34+
String url = String.format(
35+
"%s/search/v2/search/integration?keyword=%s",
36+
FLO_API_BASE, encodedName
37+
);
38+
39+
HttpHeaders headers = new HttpHeaders();
40+
headers.set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
41+
headers.set("Referer", "https://www.music-flo.com/");
42+
headers.set("Origin", "https://www.music-flo.com");
43+
// FLO 웹에서 사용하는 헤더들
44+
headers.set("x-gm-app-name", "FLO_WEB");
45+
headers.set("Accept", "application/json");
46+
47+
HttpEntity<String> entity = new HttpEntity<>(headers);
48+
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
49+
50+
if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) {
51+
log.debug("FLO 검색 API 응답 실패: name={}, status={}", artistName, response.getStatusCode());
52+
return Optional.empty();
53+
}
54+
55+
JsonNode root = objectMapper.readTree(response.getBody());
56+
JsonNode data = root.path("data");
57+
if (data.isMissingNode()) {
58+
log.debug("FLO 응답에 data 필드 없음: name={}", artistName);
59+
return Optional.empty();
60+
}
61+
62+
JsonNode list = data.path("list");
63+
if (!list.isArray() || list.isEmpty()) {
64+
log.debug("FLO 응답에 list 배열 없음: name={}", artistName);
65+
return Optional.empty();
66+
}
67+
68+
// 일반적인 FLO 구조: data.list[] 중 type == "ARTIST" 인 섹션에서 실제 아티스트 목록을 찾는다.
69+
for (JsonNode section : list) {
70+
String type = section.path("type").asText();
71+
if (!"ARTIST".equalsIgnoreCase(type)) {
72+
continue;
73+
}
74+
75+
JsonNode artists = section.path("list");
76+
if (!artists.isArray() || artists.isEmpty()) {
77+
continue;
78+
}
79+
80+
JsonNode first = artists.get(0);
81+
82+
// 필드 이름은 FLO 변경에 따라 달라질 수 있으므로, 여러 후보를 순서대로 시도
83+
String nameKo = first.path("name").asText(null);
84+
if (nameKo == null || nameKo.isBlank()) {
85+
nameKo = first.path("artistName").asText(null);
86+
}
87+
if (nameKo == null || nameKo.isBlank()) {
88+
nameKo = first.path("title").asText(null);
89+
}
90+
91+
String groupName = first.path("teamName").asText(null);
92+
93+
if (nameKo == null || nameKo.isBlank()) {
94+
log.debug("FLO 아티스트 이름 없음: originalName={}", artistName);
95+
return Optional.empty();
96+
}
97+
98+
log.debug("FLO 아티스트 정보 파싱 성공: originalName={}, nameKo={}, group={}",
99+
artistName, nameKo, groupName);
100+
return Optional.of(new ArtistInfo(nameKo, groupName));
101+
}
102+
103+
log.debug("FLO 응답에 ARTIST 섹션 없음: name={}", artistName);
104+
return Optional.empty();
105+
106+
} catch (HttpClientErrorException.NotFound e) {
107+
// FLO는 공개 API가 아니라 404가 날 수 있음
108+
log.debug("FLO API 404(NotFound): name={}", artistName);
109+
return Optional.empty();
110+
} catch (Exception e) {
111+
log.debug("FLO 검색 실패: name={}, error={}", artistName, e.getMessage());
112+
return Optional.empty();
113+
}
114+
}
115+
116+
@Getter
117+
public static class ArtistInfo {
118+
private final String nameKo;
119+
private final String artistGroup;
120+
121+
public ArtistInfo(String nameKo, String artistGroup) {
122+
this.nameKo = nameKo;
123+
this.artistGroup = artistGroup;
124+
}
125+
}
126+
}
127+
128+
129+

0 commit comments

Comments
 (0)