From 08d50230c47333a8a351610dcfa8856d76cdec32 Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Fri, 26 Jun 2026 11:34:43 +0900 Subject: [PATCH] =?UTF-8?q?feat(discover):=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EC=B1=84=EB=84=90=20=EC=A7=80=EC=97=AD(KR/JP/US)=20=ED=83=AD?= =?UTF-8?q?=20+=20=EB=B0=9C=EA=B2=AC=20=EC=A7=80=EC=97=AD=20=ED=83=9C?= =?UTF-8?q?=EA=B9=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - region 을 채널 국가가 아닌 '검색 발견 지역(regionCode)'으로 태깅(UNKNOWN 해소) - upsert: 같은 배율이면 갱신(지역 등 메타 최신화) - recommend.html: 전체/KR/JP/US 탭으로 필터, 등록/제외 시 목록 갱신 검증: 재발굴 후 KR14·JP7·US1 로 분포, 탭 필터 동작. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../channel/ChannelDiscoveryService.java | 10 ++- src/main/resources/templates/recommend.html | 61 ++++++++++++------- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelDiscoveryService.java b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelDiscoveryService.java index e20e521..bd79ceb 100644 --- a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelDiscoveryService.java +++ b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelDiscoveryService.java @@ -62,7 +62,11 @@ public class ChannelDiscoveryService { cond.setOrder("viewCount"); // 떡상 발굴: 최신순이 아니라 조회수순 // 광범위 검색: 키워드 없이 인기 Shorts. 결과 빈약 시 검색 API 보강 필요(스펙 §4.2). YoutubeSearchPageDto page = youtubeSearchService.searchYoutubeVideos(cond); - if (page != null && page.getItems() != null) all.addAll(page.getItems()); + if (page != null && page.getItems() != null) { + // 발견 지역(검색 regionCode)으로 태깅 — 채널 국가가 아닌 '어느 지역 검색에서 떴나' + for (YoutubeSearchResultDto it : page.getItems()) it.setChannelCountry(region); + all.addAll(page.getItems()); + } searchedRegions.add(region); } catch (Exception e) { log.error("[Discovery] 지역 {} 검색 실패 — 건너뜀", region, e); @@ -83,8 +87,8 @@ public class ChannelDiscoveryService { RecommendedChannel rc = recommendedChannelRepository.findByChannelId(c.channelId()) .orElseGet(RecommendedChannel::new); if ("EXCLUDED".equals(rc.getStatus()) || "REGISTERED".equals(rc.getStatus())) continue; - // 기존 NEW 항목이면 더 높은 배율일 때만 갱신 - if (rc.getId() != null && rc.getRatio() != null && c.ratio() <= rc.getRatio()) continue; + // 기존 NEW 항목이면 더 낮은 배율일 때만 건너뜀(같거나 높으면 갱신 — 지역 등 메타 최신화) + if (rc.getId() != null && rc.getRatio() != null && c.ratio() < rc.getRatio()) continue; rc.setChannelId(c.channelId()); rc.setChannelTitle(c.channelTitle()); rc.setThumbnailUrl(c.thumbnailUrl()); diff --git a/src/main/resources/templates/recommend.html b/src/main/resources/templates/recommend.html index 7811819..ee199a0 100644 --- a/src/main/resources/templates/recommend.html +++ b/src/main/resources/templates/recommend.html @@ -15,6 +15,15 @@ + + +
+ + + + +
+
@@ -43,29 +52,39 @@ function esc(s){ return (s==null?'':String(s)).replace(/&/g,'&').replace(//g,'>'); } function fmt(n){ return (n==null)?'-':Number(n).toLocaleString(); } + let ALL_RECS = []; + let CUR_REGION = ''; // ''=전체 + async function load(){ - const grid=document.getElementById('grid'); - try { - const list=await api(API); - if(!list.length){ grid.innerHTML='
아직 발굴된 추천 채널이 없습니다. ‘지금 발굴’을 눌러보세요.
'; return; } - grid.innerHTML=list.map(c=> - '
' - + '' - + '
' - + '
'+esc(c.channelTitle)+'
' - + '
👤 '+fmt(c.subscriberCount)+'명 · '+esc(c.region||'')+'
' - + '
🔥 배율 '+(c.ratio?Number(c.ratio).toFixed(1):'-')+'x 👁 '+fmt(c.topVideoViewCount)+'
' - + '
'+esc(c.topVideoTitle||'')+'
' - + '
' - + '' - + '' - + '
' - + '
').join(''); - if(window.lucide) lucide.createIcons(); - } catch(e){ grid.innerHTML='
불러오기 실패: '+esc(e.message)+'
'; } + try { ALL_RECS = await api(API); renderGrid(); } + catch(e){ document.getElementById('grid').innerHTML='
불러오기 실패: '+esc(e.message)+'
'; } } - async function register(id){ try{ await api(API+'/'+id+'/register',{method:'POST'}); document.getElementById('rc-'+id)?.remove(); }catch(e){ alert('등록 실패: '+e.message); } } - async function exclude(id){ try{ await api(API+'/'+id+'/exclude',{method:'POST'}); document.getElementById('rc-'+id)?.remove(); }catch(e){ alert('제외 실패: '+e.message); } } + function setTab(btn){ + CUR_REGION = btn.dataset.region || ''; + document.querySelectorAll('.rc-tab').forEach(b=>{ const on=(b===btn); b.classList.toggle('btn-primary',on); b.classList.toggle('btn-secondary',!on); }); + renderGrid(); + } + function renderGrid(){ + const grid=document.getElementById('grid'); + const list = CUR_REGION ? ALL_RECS.filter(c=>c.region===CUR_REGION) : ALL_RECS; + if(!list.length){ grid.innerHTML='
'+(CUR_REGION ? (CUR_REGION+' 지역 추천 채널이 없습니다.') : '아직 발굴된 추천 채널이 없습니다. ‘지금 발굴’을 눌러보세요.')+'
'; return; } + grid.innerHTML=list.map(c=> + '
' + + '' + + '
' + + '
'+esc(c.channelTitle)+'
' + + '
👤 '+fmt(c.subscriberCount)+'명 · '+esc(c.region||'')+'
' + + '
🔥 배율 '+(c.ratio?Number(c.ratio).toFixed(1):'-')+'x 👁 '+fmt(c.topVideoViewCount)+'
' + + '
'+esc(c.topVideoTitle||'')+'
' + + '
' + + '' + + '' + + '
' + + '
').join(''); + if(window.lucide) lucide.createIcons(); + } + async function register(id){ try{ await api(API+'/'+id+'/register',{method:'POST'}); ALL_RECS=ALL_RECS.filter(c=>c.id!==id); renderGrid(); }catch(e){ alert('등록 실패: '+e.message); } } + async function exclude(id){ try{ await api(API+'/'+id+'/exclude',{method:'POST'}); ALL_RECS=ALL_RECS.filter(c=>c.id!==id); renderGrid(); }catch(e){ alert('제외 실패: '+e.message); } } function openVideoModal(el){ const vid = el.dataset.vid || ''; document.getElementById('modalTitle').textContent = el.dataset.title || '';