feat: 소재 피드 롱폼만 수집 — 경쟁 채널 쇼츠 수집 제거
피드 수집이 역할(SOURCE/RIVAL)과 무관하게 롱폼만 저장하도록 변경. 쇼츠는 이미 잘린 결과물이라 자를 원본(소재)이 아니므로 수집 대상에서 제외. UI 문구(경쟁 쇼츠 → 경쟁 채널)와 길이 필터도 함께 정리. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
aba3c5badc
commit
f96e4c583d
@ -8,7 +8,7 @@ package com.hlab.yanalyst.domain.channel;
|
||||
* <li>{@link #OWN} — 내가 운영하는 채널. 이미 발행한 결과물이라 소재가 아니므로
|
||||
* 수집함·칸반·발굴·피드 어디에도 섞이지 않고 내 채널 분석 화면에서만 쓰인다.</li>
|
||||
* <li>{@link #SOURCE} — 소재 원본 채널(웹예능 공식채널). 롱폼만 피드에 수집한다.</li>
|
||||
* <li>{@link #RIVAL} — 경쟁 쇼츠 채널(같은 소재를 다루는 클립 채널). 쇼츠만 피드에 수집한다.</li>
|
||||
* <li>{@link #RIVAL} — 경쟁 채널(같은 소재를 다루는 클립 채널). 역시 롱폼만 피드에 수집한다.</li>
|
||||
* </ul>
|
||||
*
|
||||
* ddl-auto:update 로 컬럼이 추가되므로 기존 행은 role 이 null 이다. null 은 항상 {@link #MY} 로 취급한다.
|
||||
@ -45,9 +45,4 @@ public final class ChannelRole {
|
||||
String r = normalize(role);
|
||||
return SOURCE.equals(r) || RIVAL.equals(r);
|
||||
}
|
||||
|
||||
/** 해당 역할이 수집할 영상 포맷 — SOURCE 는 롱폼만, RIVAL 은 쇼츠만. */
|
||||
public static boolean acceptsShorts(String role) {
|
||||
return RIVAL.equals(normalize(role));
|
||||
}
|
||||
}
|
||||
|
||||
@ -390,7 +390,7 @@ public class ChannelService {
|
||||
|
||||
/**
|
||||
* 피드 시드 채널의 최근 업로드를 1페이지(최대 50건)만 수집한다.
|
||||
* 소스 채널은 롱폼만, 경쟁 채널은 쇼츠만 저장한다.
|
||||
* 역할과 무관하게 롱폼만 저장한다 — 쇼츠는 이미 잘린 결과물이라 소재가 아니다.
|
||||
*
|
||||
* @param channel SOURCE 또는 RIVAL 역할의 채널
|
||||
* @param publishedAfter 이 시각 이후 업로드만 수집
|
||||
@ -422,10 +422,9 @@ public class ChannelService {
|
||||
}
|
||||
if (videoIds.isEmpty()) return 0;
|
||||
|
||||
// SOURCE = 롱폼만(공식계정 쇼츠는 이미 잘린 결과물), RIVAL = 쇼츠만
|
||||
Boolean shortsOnly = ChannelRole.acceptsShorts(role);
|
||||
// 피드는 롱폼만 수집한다 (쇼츠는 이미 잘린 결과물 — 자를 원본이 아니다)
|
||||
String source = ChannelRole.RIVAL.equals(role) ? ChannelRole.RIVAL : ChannelRole.SOURCE;
|
||||
return upsertVideos(channel, videoIds, source, shortsOnly, publishedAfter);
|
||||
return upsertVideos(channel, videoIds, source, Boolean.FALSE, publishedAfter);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ import java.util.Map;
|
||||
/**
|
||||
* 소재 발굴 피드 수집기.
|
||||
*
|
||||
* <p>SOURCE(웹예능 공식채널)의 신규 롱폼과 RIVAL(경쟁 예능짤 채널)의 신규 쇼츠를 주기적으로 받아온다.
|
||||
* <p>SOURCE(웹예능 공식채널)와 RIVAL(경쟁 클립 채널)의 신규 롱폼을 주기적으로 받아온다(쇼츠는 수집하지 않는다).
|
||||
* uploads 플레이리스트 1페이지만 조회하므로 채널당 약 2 units — 검색(search.list 100 units)보다 훨씬 싸다.
|
||||
*/
|
||||
@Slf4j
|
||||
|
||||
@ -100,7 +100,7 @@ hlab:
|
||||
# 발굴 대상 포맷 기본값: LONG_FORM(롱폼) | SHORTS. UI '지금 발굴'에서 건별 선택 가능.
|
||||
format: ${DISCOVERY_FORMAT:LONG_FORM}
|
||||
|
||||
# 소재 발굴 피드: 소스(웹예능 공식채널) 신규 롱폼 + 경쟁(예능짤) 신규 쇼츠를 최신순으로 수집
|
||||
# 소재 발굴 피드: 소스(웹예능 공식채널)·경쟁(클립 채널)의 신규 롱폼만 최신순으로 수집 (쇼츠 제외)
|
||||
feed:
|
||||
enabled: ${FEED_ENABLED:true}
|
||||
cron: ${FEED_CRON:0 15 */3 * * *} # 3시간마다 (소재 선점은 업로드 후 몇 시간이 승부)
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>소재 피드</h1>
|
||||
<p class="sub">소재 원본(웹예능 롱폼)과 경쟁 쇼츠의 신규 업로드를 최신순으로 봅니다.</p>
|
||||
<p class="sub">소재 원본(웹예능)과 경쟁 채널의 신규 롱폼 업로드를 최신순으로 봅니다.</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-secondary" onclick="openPersons()">
|
||||
@ -37,7 +37,7 @@
|
||||
<button class="feed-tab" role="tab" id="tab-RIVAL" data-tab="RIVAL"
|
||||
aria-selected="false" aria-controls="feedGrid" tabindex="-1" onclick="switchTab('RIVAL')">
|
||||
<i data-lucide="swords" style="width:15px;"></i>
|
||||
<span>경쟁 쇼츠</span>
|
||||
<span>경쟁 채널</span>
|
||||
<span class="feed-tab-hint">예능짤 채널</span>
|
||||
</button>
|
||||
<button class="feed-tab" role="tab" id="tab-PERSON" data-tab="PERSON"
|
||||
@ -101,8 +101,8 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-sm text-muted mb-3" style="line-height:1.6;">
|
||||
<b>소재 원본</b>은 유퀴즈·핑계고 같은 웹예능 공식채널(롱폼만 수집),
|
||||
<b>경쟁 쇼츠</b>는 같은 소재를 다루는 예능짤 채널(쇼츠만 수집)입니다.
|
||||
<b>소재 원본</b>은 유퀴즈·핑계고 같은 웹예능 공식채널,
|
||||
<b>경쟁 채널</b>은 같은 소재를 다루는 예능짤 채널입니다. 두 역할 모두 롱폼만 수집합니다.
|
||||
</p>
|
||||
|
||||
<div class="seed-add">
|
||||
@ -111,7 +111,7 @@
|
||||
<label class="sr-only" for="seedRole">역할</label>
|
||||
<select id="seedRole">
|
||||
<option value="SOURCE">소재 원본</option>
|
||||
<option value="RIVAL">경쟁 쇼츠</option>
|
||||
<option value="RIVAL">경쟁 채널</option>
|
||||
</select>
|
||||
<button class="btn btn-primary" id="seedAddBtn" onclick="addSeed()">추가</button>
|
||||
</div>
|
||||
@ -428,7 +428,7 @@
|
||||
// ---------- 탭 ----------
|
||||
const TABS = ['SOURCE', 'RIVAL', 'PERSON'];
|
||||
|
||||
// 탭별로 의미 있는 필터만 남긴다 (경쟁은 전부 쇼츠, 인물은 채널이 아니라 사람 기준)
|
||||
// 탭별로 의미 있는 필터만 남긴다 (소스·경쟁은 롱폼 채널 기준, 인물은 채널이 아니라 사람 기준)
|
||||
function applyTabChrome(tab){
|
||||
document.querySelectorAll('.feed-tab').forEach(btn => {
|
||||
const on = btn.dataset.tab === tab;
|
||||
@ -436,10 +436,10 @@
|
||||
btn.tabIndex = on ? 0 : -1;
|
||||
});
|
||||
document.getElementById('feedGrid').setAttribute('aria-labelledby', 'tab-' + tab);
|
||||
document.getElementById('lengthField').style.display = (tab === 'SOURCE') ? '' : 'none';
|
||||
document.getElementById('lengthField').style.display = (tab === 'PERSON') ? 'none' : '';
|
||||
document.getElementById('programField').style.display = (tab === 'PERSON') ? 'none' : '';
|
||||
document.getElementById('personField').style.display = (tab === 'PERSON') ? '' : 'none';
|
||||
if(tab !== 'SOURCE') document.getElementById('fLength').value = '';
|
||||
if(tab === 'PERSON') document.getElementById('fLength').value = '';
|
||||
}
|
||||
|
||||
function switchTab(tab){
|
||||
@ -572,7 +572,7 @@
|
||||
? `<span>·</span><span class="num">${fmtNum(Math.round(it.viewsPerHour))}/h</span>` : '';
|
||||
|
||||
// 롱폼 소재(소스·인물)는 숏폼 큐로 넘기는 게 주 동선이다.
|
||||
// 경쟁 쇼츠는 이미 잘린 결과물이라 큐 대상이 아니고 원본 보기만 준다.
|
||||
// 경쟁 채널 영상은 경쟁자의 결과물(벤치마크)이라 큐 대상이 아니고 원본 보기만 준다.
|
||||
const inQueue = queued.has(it.videoId);
|
||||
const primary = currentTab === 'RIVAL'
|
||||
? `<a class="btn btn-secondary grow" href="${url}" target="_blank" rel="noopener">
|
||||
@ -645,7 +645,7 @@
|
||||
<button class="btn btn-secondary" onclick="runCollect()">지금 수집</button>`);
|
||||
} else {
|
||||
showState('inbox', '아직 소재가 없습니다',
|
||||
'경쟁 예능짤 쇼츠 채널을 시드로 등록한 뒤 수집하세요.',
|
||||
'경쟁 예능짤 채널을 시드로 등록한 뒤 수집하세요. (롱폼만 수집됩니다)',
|
||||
`<button class="btn btn-primary" onclick="openSeeds()">시드 등록하기</button>
|
||||
<button class="btn btn-secondary" onclick="runCollect()">지금 수집</button>`);
|
||||
}
|
||||
@ -810,7 +810,7 @@
|
||||
return;
|
||||
}
|
||||
box.innerHTML = list.map(s => {
|
||||
const roleLabel = s.role === 'SOURCE' ? '소재 원본' : '경쟁 쇼츠';
|
||||
const roleLabel = s.role === 'SOURCE' ? '소재 원본' : '경쟁 채널';
|
||||
const roleCls = s.role === 'SOURCE' ? 'badge-primary' : 'badge-muted';
|
||||
const warn = s.disabled
|
||||
? `<button class="warn" title="수집 실패 ${s.failCount}회로 중단됨 — 다시 활성화"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user