결제 페이지 폐기하고 Spotify · YouTube · Groq 로 음악 ↔ 팝업 매칭 만들기 — POP-SPOT v1.3
결제 페이지 폐기. Spotify 5 단계 한국어 폴백 + YouTube IFrame 영구 캐시 + Groq 40 화이트리스트 무드 분석으로 음악 ↔ 팝업 매칭. 자동수집 V4 와 04:00 cron 까지.
POP-PASS 결제 페이지를 폐기하고, 음악 ↔ 팝업 매칭을 새로운 코어 가치로 갈아엎었음. 같은 버전에 자동수집 V4 (Naver/Kakao 검색 API + Groq) 와 등급 시스템도 같이 들어갔다.
이 글에서 다루는 것
모르는 단어 한 줄로
| 용어 | 한 줄 설명 |
|---|---|
| YouTube IFrame | YouTube 플레이어를 내 사이트 안에 박는 표준 방식. 광고/약관 준수까지 자동 처리됨 |
| quota (YouTube API) | YouTube Data API 의 일일 호출 한도. 보통 일일 10,000 units |
| polling / lazy fetch | "필요할 때만 외부 API 호출" — 페이지 안 보면 호출 안 함 |
| cron | 리눅스의 정기 작업 스케줄러. "매일 04:00 KST 에 이거 실행" 같은 표현 |
| confidence | 여기서는 자동수집한 팝업 정보가 "진짜일 확률" 을 0~1 사이 숫자로 표시한 값 |
| Geocoding | 주소 문자열을 위경도 좌표 (lat, lng) 로 변환하는 일 |
무엇이 바뀌었나
| 영역 | v1.2 | v1.3 |
|---|---|---|
| 메인 가치 | 팝업 정보 + 결제 (POP-PASS · 확성기) | 팝업 정보 + 음악 ↔ 팝업 매칭 |
| 음악 검색 | 없음 | Spotify Web API + Groq 영문 변환 (5 단계 폴백) |
| 음악 재생 | 없음 | YouTube IFrame · 영구 캐시 (lazy fetch) |
| 매칭 알고리즘 | 없음 | Groq 40 화이트리스트 + 키워드 30 점 + 카테고리 보너스 |
| 자동수집 | 없음 | V4 — Naver/Kakao 검색 API + Groq, confidence ≥ 0.8 자동 게시 |
| 지도 | 주소만 표시 | Kakao Local API 로 lat/lng 자동 채움 |
| 스케줄러 | 없음 | 04:00 자동수집 · 05:00 만료 처리 (KST) |
| 등급 | 없음 | BEGINNER (스탬프 3) / HUNTER (6) / MASTER (12) |
왜 이렇게 했음
결제 폐기 — POP-PASS 와 확성기는 운영 책임 (환불, CS) 만 늘리고 사용자에게 주는 가치는 약했다. 같은 시간을 음악 매칭에 쓰는 게 "검색하다가 자연스럽게 음악으로 들어간다" 는 행동 흐름과 잘 맞았음.
Spotify 5 단계 폴백 — Spotify 의 한국 카탈로그는 좋지만 검색은 한글에 약하다. 예) "아이유 좋은날" 을 그대로 던지면 첫 결과가 cover 일 때가 많음. 그래서 (1) 원문 → (2) Groq 정규화 → (3) 영문 표기 → (4) YouTube Suggest 보강 → (5) 글로벌 검색, 이 순서로 폴백을 깐다.
YouTube 영구 캐시 — Data API 의 일일 한도가 10,000 units 인데 검색 한 번이 100 unit 이다. 100 번 검색하면 끝. 그래서 한 번 찾은 (artist, title) → video_id 매핑은 DB 에 영구 보관해서 같은 곡을 다시 찾을 때 API 호출을 안 하게 한다.
Groq 40 화이트리스트 — 음악 → 팝업 매칭에서 Groq 가 자유롭게 무드 태그를 만들면 "우울한, 슬픈, 멜랑콜리한" 같은 비슷한 단어가 무한히 나온다. DB 매칭이 불가능해짐. 그래서 미리 40 개 (예: dreamy, energetic, retro, melancholy …) 만 후보로 주고 "이 중에서만 골라" 라고 강제. 결과: 외부 호출 0 회로 매칭 가능 + 결과가 결정적.
04:00 자동수집 — 검색 API 응답은 시간대마다 다를 수 있고, 새벽이 외부 API 응답이 가장 안정적이다. 같은 시각에 "오늘 만료된 팝업" 도 같이 정리하는 게 데이터 일관성에 좋음.
코드로는 어떻게 (필요한 부분만)
Spotify 5 단계 폴백 — 한 단계 실패하면 다음 단계.
파일: popspot-backend/src/main/java/com/popspot/service/music/SpotifySearchService.java
// v1.3 — SpotifySearchService. 5단계 폴백 파이프라인
public Optional<Track> search(String query) {
// ^^^^^^^^ ^^^^^ 사용자가 입력한 원문 질의 (예: "아이유 좋은날")
// +-- null 가능성 명시. 이 5단계 모두 부족하면 empty 리턴
return tryRaw(query)
// ^^^^^^^^^^^^^ 1단계: 원문 그대로 Spotify 에 던짐
// (영문 곡이면 이게 바로 맞음. 불필요한 외부호출 없애는 일차 시도)
.or(() -> tryGroqNormalized(query))
// ^^ Optional.or — 앞이 empty 일 때만 다음 람다 실행
// ^^^^^^^^^^^^^^^^^^^^^^^^^ 2단계: Groq 에 "이 한글 곡명·가수명을 영문 표기로" 요청
// 예: "아이유 좋은날" → "IU Good Day"
.or(() -> tryEnglishOnly(query))
// ^^^^^^^^^^^^^^^^^^^^^^ 3단계: 한글 제거·영문 토큰만 남겼 는지 재시도
// (본명+영어 제목 혼합 케이스 대응)
.or(() -> tryYoutubeSuggest(query))
// ^^^^^^^^^^^^^^^^^^^^^^^^^ 4단계: YouTube 검색 suggestion 이 더 잘 잡아주는 경우가 있음
// 추천된 결과 제목을 다시 Spotify 에 넘김
.or(() -> tryGlobal(query));
// ^^^^^^^^^^^^^^^^^ 5단계: market=KR 없이 글로벌 카탈로그 검색
// (일본·중화권 곡·기타 외국 곡 대응)
}YouTube 영구 캐시 — DB 에 (artist, title, video_id) 를 적재.
파일: popspot-backend/src/main/java/com/popspot/service/music/MusicTrackService.java (v1.2 양식)
// v1.2 까지 — 조회 매번 YouTube Data API 호출
String videoId = youtubeApi.search(artist + " " + title);
// ^^^^^^ search 한 회당 100 quota units 소모
// 일일 한도 10,000 / 100 = 100회면 소진
// 많은 사용자가 같은 곡을 찾아도 구분 없이 재호출파일: 같은 MusicTrackService.java (v1.3 교체 후)
// v1.3 — (artist, title, video_id) DB 영구 캠시. 없을 때만 API 추가
String videoId = musicTrackRepository
// ^^^^^^^^^^^^^^^^^^^^ Spring Data JPA 레포지터·테이블명: music_track
.findByArtistAndTitle(artist, title)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 메서드명 규칙으로 자동 생성된 쿼리:
// SELECT * FROM music_track WHERE artist=? AND title=?
.map(MusicTrack::getYoutubeVideoId)
// ^^^ Optional<MusicTrack> 에서 MusicTrack 존재 시만 내부 값 추출
// 결과: Optional<String> (레코드가 없으면 여전히 empty)
.orElseGet(() -> {
// ^^^^^^^^^ 캐시 미적중 시에만 람다 호출 — 지연 평가
String fetched = youtubeApi.search(artist + " " + title);
// ^^^^^^ 그제서야 YouTube Data API 호출 (100 units 소모)
musicTrackRepository.save(
MusicTrack.of(artist, title, fetched));
// ^^^^^^^^^^^^^ 트랙 레코드 생성 후 적재 — 다음 동일 호출부터는 캐시 적중
return fetched;
});
// ^ 결과: 캐시 적중 시 호출 한 줄로 곳바로 video_id 반환 — 외부 API 호출 0Groq 화이트리스트 강제 — 프롬프트에 "이 중에서만 골라" 를 명시.
파일: popspot-backend/src/main/java/com/popspot/service/music/MusicMoodAnalysisService.java
// v1.3 — Groq 에 넘길 프롬프트. 자유 생성이 아닌 "택1" 구조
String prompt = """
// ^^^ Text Block (Java 13+). \\n 에스케이프 없이 여러 줄 문자열
아래 40 개 무드 중에서만 골라서 3 개 반환. 다른 단어 절대 금지.
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Groq 에게 "창의적으로 만들어" 가 아닌 "이 목록에서만 고르라" 명시.
// 이게 없으면 "우울한·쓸쓸한·멜랑콜리한" 등 비슷한 단어가 무한 생성돼 DB 매칭 실패
[dreamy, energetic, retro, melancholy, ...] (40 개)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// 이 40개는 popup 카테고리에도 동일하게 부여되어 있음
// → Groq 답변이 "dreamy, retro, energetic" 이면 popup.mood_tags 와 바로 교집합 계산 가능
곡 제목: %s, 가수: %s
// ^^ ^^ 아래서 title, artist 으로 치환됨
""".formatted(title, artist);
// ^^^^^^^^^^ Java 15+: String::formatted. printf 처럼 %s 치환자동수집 cron — Spring Scheduling.
파일: popspot-backend/src/main/java/com/popspot/service/crawler/PopupCrawlOrchestrator.java
// v1.3 — PopupCrawlOrchestrator. 매일 04:00 KST 자동 수집
@Scheduled(cron = "0 0 4 * * *", zone = "Asia/Seoul")
// ^^^^ ^ ^ ^ ^ ^ ^ ^^^^^^^^^^^^^^^^^^^
// | | | | | | | |
// | | | | | | | +-- 종일제 적용 타임존. 안 적으면 서버·JVM 기본으로 실행돼
// | | | | | | +------- 요일 (* = 전체)
// | | | | | +--------- 월 (*)
// | | | | +----------- 일 (*)
// | | | +------------- 시 = 4 (04시)
// | | +--------------- 분 = 0
// | +----------------- 초 = 0 → 매일 04:00:00
// +-------------------------- Spring 의 cron 은 6개 필드 (초까지). 리눅스 cron(5자리)과 다름
public void runOnce() {
keywords.forEach(this::collectAndNormalize);
// ^^^^^^^^ List<String> — 최상위 키워드 (v2.13 에서 95개로 확장됨)
// ^^^^^^^ 경량 순차 처리 — 외부 API 속도제한도 있으니 병렬 안 함
// ^^^^^^^^^^^^^^^^^^^^^^^ 메서드 레퍼런스. 각 키워드마다 6단계 실행:
// 1) Naver 검색 API 2) Kakao 검색 API
// 3) Groq 정규화 4) Kakao Geocoding
// 5) confidence 계산 6) ≥0.8 이면 자동 게시
}핵심 파일:
직접 보는 법
popspot.co.kr 에서 메인 진입 → "음악" 탭. 곡 카드 클릭하면 위에 만든 5 단계 폴백 → 영구 캐시 → YouTube IFrame 재생이 모두 한 번에 일어난다. 같은 곡을 두 번째 누르면 외부 호출이 일어나지 않는다 (캐시 적중).
비하인드 · 사고
매칭 정확도를 올리려고 Spotify 검색에서 첫 결과를 그대로 믿었다가 cover/라이브 영상이 자주 걸렸음. 결국 제목 검증 5 단계 (원곡 키워드 포함 / cover/live/karaoke 미포함 등) 를 한 번 더 거는데, 이 부분이 v2.14 의 출발점이 된다.
교훈 한 줄. 외부 API 의 첫 결과는 "가장 정확한 답" 이 아니라 "가장 관련성 높은 후보" 다. 검증 단계를 따로 둬야 한다.