diff --git a/src/main/java/com/hlab/yanalyst/domain/channel/RecommendedChannelController.java b/src/main/java/com/hlab/yanalyst/domain/channel/RecommendedChannelController.java index b05cad7..2f5cd0e 100644 --- a/src/main/java/com/hlab/yanalyst/domain/channel/RecommendedChannelController.java +++ b/src/main/java/com/hlab/yanalyst/domain/channel/RecommendedChannelController.java @@ -21,9 +21,9 @@ public class RecommendedChannelController { private final ChannelService channelService; @GetMapping - @Operation(summary = "추천 채널 목록", description = "status=NEW 를 배율 내림차순으로 반환") - public ApiResponse> list() { - return ApiResponse.ok(repository.findByStatusOrderByRatioDesc("NEW")); + @Operation(summary = "추천 채널 목록", description = "status(NEW|EXCLUDED|REGISTERED, 기본 NEW)를 배율 내림차순으로 반환") + public ApiResponse> list(@RequestParam(defaultValue = "NEW") String status) { + return ApiResponse.ok(repository.findByStatusOrderByRatioDesc(status)); } @PostMapping("/{id}/register") @@ -49,6 +49,17 @@ public class RecommendedChannelController { return ApiResponse.ok(null); } + @PostMapping("/{id}/restore") + @Operation(summary = "제외 복원", description = "EXCLUDED 를 다시 NEW(추천 목록)로 되돌린다") + @Transactional + public ApiResponse restore(@PathVariable Long id) { + RecommendedChannel rc = repository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("추천 채널을 찾을 수 없습니다: " + id)); + rc.setStatus("NEW"); + repository.save(rc); + return ApiResponse.ok(null); + } + @PostMapping("/run") @Operation(summary = "수동 발굴 실행", description = "스케줄과 동일한 발굴을 즉시 1회 실행") public ApiResponse> run() { diff --git a/src/main/resources/templates/recommend.html b/src/main/resources/templates/recommend.html index 4644827..98e36c2 100644 --- a/src/main/resources/templates/recommend.html +++ b/src/main/resources/templates/recommend.html @@ -10,6 +10,7 @@

지역(KR·JP·US) 인기 Shorts에서 자동 발굴한 떡상 채널 — 구독자는 적은데 조회수가 터진 채널

+
@@ -54,12 +55,21 @@ function fmt(n){ return (n==null)?'-':Number(n).toLocaleString(); } let ALL_RECS = []; - let CUR_REGION = ''; // ''=전체 + let CUR_REGION = ''; // ''=전체 + let VIEW_STATUS = 'NEW'; // 'NEW'(추천) | 'EXCLUDED'(제외 목록) async function load(){ - try { ALL_RECS = await api(API); renderGrid(); } + try { ALL_RECS = await api(API + '?status=' + VIEW_STATUS); renderGrid(); } catch(e){ document.getElementById('grid').innerHTML='
불러오기 실패: '+esc(e.message)+'
'; } } + function toggleView(){ + VIEW_STATUS = (VIEW_STATUS === 'NEW') ? 'EXCLUDED' : 'NEW'; + const btn = document.getElementById('viewToggle'); + btn.innerHTML = (VIEW_STATUS === 'EXCLUDED') + ? ' 추천 목록' + : ' 제외 목록'; + load(); + } 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); }); @@ -67,26 +77,36 @@ } function renderGrid(){ const grid=document.getElementById('grid'); + const excludedView = (VIEW_STATUS === 'EXCLUDED'); 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(!list.length){ + grid.innerHTML='
' + + (excludedView ? '제외한 채널이 없습니다.' + : (CUR_REGION ? (CUR_REGION+' 지역 추천 채널이 없습니다.') : '아직 발굴된 추천 채널이 없습니다. ‘지금 발굴’을 눌러보세요.')) + + '
'; + return; + } + grid.innerHTML=list.map(c=>{ + const moveBtn = ' 채널 이동'; + const actions = excludedView + ? '' + moveBtn + : '' + moveBtn + + ''; + return '
' + + '' + + '
' + + '
'+esc(c.channelTitle)+'
' + + '
👤 '+fmt(c.subscriberCount)+'명 · '+esc(c.region||'')+'
' + + '
🔥 배율 '+(c.ratio?Number(c.ratio).toFixed(1):'-')+'x 👁 '+fmt(c.topVideoViewCount)+'
' + + '
'+esc(c.topVideoTitle||'')+'
' + + '
'+actions+'
' + + '
'; + }).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); } } + async function restore(id){ try{ await api(API+'/'+id+'/restore',{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 || '';