feat: 소재 피드에서 숏폼 큐로 바로 보내기
발굴한 롱폼을 숏폼 큐로 넘기는 동선이 없어 URL을 손으로 옮겨야 했다. 채널 등록·롱폼 수집·정보 표시는 이미 소재 피드에 있으므로 페이지를 새로 만들지 않고 카드에 버튼만 붙였다. - 소재 원본/인물 탭 카드의 주 버튼을 '숏폼 큐로'로 바꿨다. 경쟁 쇼츠는 이미 잘린 결과물이라 큐 대상이 아니라 원본 보기만 남긴다. - 피드를 열 때 큐 목록을 함께 받아 이미 담긴 영상은 '큐에 있음'으로 표시하고 숏폼 큐로 링크한다. 등록 API가 videoId 기준 멱등이라 중복 클릭도 안전하다. - 기존 재가공 스튜디오 진입은 아이콘 버튼으로 남겼다. - 큐 목록 조회가 실패해도 피드 자체는 정상 동작한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9a960be663
commit
e9d3f9a9f4
@ -308,9 +308,12 @@
|
|||||||
/*<![CDATA[*/
|
/*<![CDATA[*/
|
||||||
const API = '/api/feed';
|
const API = '/api/feed';
|
||||||
const CV_API = '/api/v1/channel-videos';
|
const CV_API = '/api/v1/channel-videos';
|
||||||
|
const SF_API = '/api/shortform';
|
||||||
|
|
||||||
let currentTab = 'SOURCE';
|
let currentTab = 'SOURCE';
|
||||||
let items = [];
|
let items = [];
|
||||||
|
/** 숏폼 큐에 이미 담긴 videoId — 중복 등록을 막고 카드에 표시한다. */
|
||||||
|
let queued = new Set();
|
||||||
|
|
||||||
// ---------- 공통 ----------
|
// ---------- 공통 ----------
|
||||||
function esc(s){ return (s==null?'':String(s)).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
function esc(s){ return (s==null?'':String(s)).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||||
@ -526,13 +529,18 @@
|
|||||||
const vph = (currentTab === 'RIVAL' && it.viewsPerHour != null)
|
const vph = (currentTab === 'RIVAL' && it.viewsPerHour != null)
|
||||||
? `<span>·</span><span class="num">${fmtNum(Math.round(it.viewsPerHour))}/h</span>` : '';
|
? `<span>·</span><span class="num">${fmtNum(Math.round(it.viewsPerHour))}/h</span>` : '';
|
||||||
|
|
||||||
// 롱폼 소재(소스·인물)는 재가공 스튜디오로, 경쟁 쇼츠는 원본 보기로
|
// 롱폼 소재(소스·인물)는 숏폼 큐로 넘기는 게 주 동선이다.
|
||||||
// (남의 쇼츠는 재가공 대상이 아님)
|
// 경쟁 쇼츠는 이미 잘린 결과물이라 큐 대상이 아니고 원본 보기만 준다.
|
||||||
|
const inQueue = queued.has(it.videoId);
|
||||||
const primary = currentTab === 'RIVAL'
|
const primary = currentTab === 'RIVAL'
|
||||||
? `<a class="btn btn-secondary grow" href="${url}" target="_blank" rel="noopener">
|
? `<a class="btn btn-secondary grow" href="${url}" target="_blank" rel="noopener">
|
||||||
<i data-lucide="external-link" style="width:14px;"></i> 원본 보기</a>`
|
<i data-lucide="external-link" style="width:14px;"></i> 원본 보기</a>`
|
||||||
: `<a class="btn btn-primary grow" href="/rework/${it.id}">
|
: (inQueue
|
||||||
<i data-lucide="wand-2" style="width:14px;"></i> 작업 시작</a>`;
|
? `<a class="btn btn-secondary grow" href="/shortform">
|
||||||
|
<i data-lucide="check" style="width:14px;"></i> 큐에 있음</a>`
|
||||||
|
: `<button class="btn btn-primary grow" id="sfBtn-${it.id}"
|
||||||
|
onclick="sendToQueue(${it.id})">
|
||||||
|
<i data-lucide="clapperboard" style="width:14px;"></i> 숏폼 큐로</button>`);
|
||||||
|
|
||||||
return `<article class="fcard${it.worked ? ' is-worked' : ''}" data-id="${it.id}">
|
return `<article class="fcard${it.worked ? ' is-worked' : ''}" data-id="${it.id}">
|
||||||
<a class="fthumb" href="${url}" target="_blank" rel="noopener"
|
<a class="fthumb" href="${url}" target="_blank" rel="noopener"
|
||||||
@ -555,6 +563,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="factions">
|
<div class="factions">
|
||||||
${primary}
|
${primary}
|
||||||
|
${currentTab === 'RIVAL' ? '' : `
|
||||||
|
<a class="icon-btn" href="/rework/${it.id}" title="재가공 스튜디오"
|
||||||
|
aria-label="재가공 스튜디오에서 열기">
|
||||||
|
<i data-lucide="wand-2" style="width:16px;"></i>
|
||||||
|
</a>`}
|
||||||
<button class="icon-btn${it.bookmarked ? ' on' : ''}" title="북마크"
|
<button class="icon-btn${it.bookmarked ? ' on' : ''}" title="북마크"
|
||||||
aria-label="북마크" aria-pressed="${it.bookmarked}"
|
aria-label="북마크" aria-pressed="${it.bookmarked}"
|
||||||
onclick="toggleBookmark(${it.id}, ${!!it.bookmarked})">
|
onclick="toggleBookmark(${it.id}, ${!!it.bookmarked})">
|
||||||
@ -593,6 +606,38 @@
|
|||||||
if(window.lucide) lucide.createIcons();
|
if(window.lucide) lucide.createIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 숏폼 큐 ----------
|
||||||
|
/** 큐에 이미 담긴 videoId 를 받아둔다. 실패해도 피드 자체는 보여야 하므로 조용히 넘긴다. */
|
||||||
|
async function loadQueued(){
|
||||||
|
try {
|
||||||
|
const jobs = await api(SF_API + '/jobs') || [];
|
||||||
|
queued = new Set(jobs.map(j => j.videoId).filter(Boolean));
|
||||||
|
} catch(e){ /* 큐를 못 읽어도 피드는 정상 동작 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendToQueue(id){
|
||||||
|
const it = items.find(v => v.id === id);
|
||||||
|
if(!it) return;
|
||||||
|
const btn = document.getElementById('sfBtn-' + id);
|
||||||
|
if(btn){
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<i data-lucide="loader-2" style="width:14px;" class="animate-spin"></i> 보내는 중...';
|
||||||
|
if(window.lucide) lucide.createIcons();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await api(SF_API + '/jobs', {
|
||||||
|
method:'POST', headers:{'Content-Type':'application/json'},
|
||||||
|
body: JSON.stringify({ youtubeUrl: 'https://www.youtube.com/watch?v=' + it.videoId })
|
||||||
|
});
|
||||||
|
queued.add(it.videoId);
|
||||||
|
render();
|
||||||
|
toast('숏폼 큐에 담았습니다 — 숏폼 큐에서 Opal 실행하세요');
|
||||||
|
} catch(e){
|
||||||
|
toast('숏폼 큐 등록 실패: ' + e.message, true);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 카드 액션 ----------
|
// ---------- 카드 액션 ----------
|
||||||
async function toggleBookmark(id, current){
|
async function toggleBookmark(id, current){
|
||||||
try {
|
try {
|
||||||
@ -868,7 +913,7 @@
|
|||||||
// ---------- init ----------
|
// ---------- init ----------
|
||||||
(async () => {
|
(async () => {
|
||||||
const preselect = restoreFilters();
|
const preselect = restoreFilters();
|
||||||
await loadFilterOptions(preselect);
|
await Promise.all([loadFilterOptions(preselect), loadQueued()]);
|
||||||
await loadFeed();
|
await loadFeed();
|
||||||
})();
|
})();
|
||||||
/*]]>*/
|
/*]]>*/
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user