feat(youtube-tab): TrendsTab 시장 트렌드 서브탭
This commit is contained in:
@@ -1,3 +1,173 @@
|
|||||||
|
// src/pages/music/components/TrendsTab.jsx
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
getLatestTrendReport, getTrendReports,
|
||||||
|
getMarketSuggestions, triggerYoutubeResearch,
|
||||||
|
} from '../../../api';
|
||||||
|
|
||||||
|
const FLAG = { BR: '🇧🇷', US: '🇺🇸', ID: '🇮🇩', MX: '🇲🇽', KR: '🇰🇷' };
|
||||||
|
|
||||||
export default function TrendsTab() {
|
export default function TrendsTab() {
|
||||||
return null;
|
const [latestReport, setLatestReport] = useState(null);
|
||||||
|
const [reports, setReports] = useState([]);
|
||||||
|
const [suggestions, setSuggestions] = useState([]);
|
||||||
|
const [selectedReport, setSelectedReport] = useState(null);
|
||||||
|
const [researching, setResearching] = useState(false);
|
||||||
|
const [copiedIdx, setCopiedIdx] = useState(null);
|
||||||
|
|
||||||
|
const loadAll = async () => {
|
||||||
|
const [latest, rpts, sugg] = await Promise.all([
|
||||||
|
getLatestTrendReport().catch(() => null),
|
||||||
|
getTrendReports().catch(() => []),
|
||||||
|
getMarketSuggestions().catch(() => []),
|
||||||
|
]);
|
||||||
|
setLatestReport(latest);
|
||||||
|
setReports(Array.isArray(rpts) ? rpts : rpts.reports ?? []);
|
||||||
|
setSuggestions(Array.isArray(sugg) ? sugg : sugg.suggestions ?? []);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => { loadAll(); }, []);
|
||||||
|
|
||||||
|
const handleResearch = async () => {
|
||||||
|
setResearching(true);
|
||||||
|
try {
|
||||||
|
await triggerYoutubeResearch();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('triggerYoutubeResearch:', e);
|
||||||
|
} finally {
|
||||||
|
setResearching(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = (text, idx) => {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
setCopiedIdx(idx);
|
||||||
|
setTimeout(() => setCopiedIdx(null), 2000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 선택된 리포트가 있으면 그것, 없으면 최신 리포트의 장르 표시
|
||||||
|
const displayReport = selectedReport ?? latestReport;
|
||||||
|
const topGenres = displayReport?.top_genres?.slice(0, 5) ?? [];
|
||||||
|
const maxScore = topGenres.length > 0 ? Math.max(...topGenres.map(g => g.score)) : 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="yt-content">
|
||||||
|
{/* 수집 상태 바 */}
|
||||||
|
<div className="yt-status-bar">
|
||||||
|
<div className="yt-status-bar__left">
|
||||||
|
<span className="yt-status-dot" />
|
||||||
|
<span className="yt-status-bar__text">
|
||||||
|
마지막 수집: <strong>{latestReport?.report_date ?? '없음'}</strong>
|
||||||
|
{latestReport && ` · ${latestReport.top_genres?.length ?? 0}개 장르`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ms-btn ms-btn--ghost ms-btn--sm"
|
||||||
|
onClick={handleResearch}
|
||||||
|
disabled={researching}
|
||||||
|
>
|
||||||
|
{researching ? '수집 중...' : '↻ 수동 수집'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 인기 장르 Top 5 */}
|
||||||
|
<div className="yt-card">
|
||||||
|
<h3 className="yt-card__title">🔥 오늘의 인기 장르 Top 5</h3>
|
||||||
|
{topGenres.length === 0 ? (
|
||||||
|
<p className="yt-empty">
|
||||||
|
트렌드 데이터가 없습니다. 수동 수집을 실행하거나 agent-office가 내일 09:00에 자동 수집합니다.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="yt-bar-chart yt-bar-chart--genre">
|
||||||
|
{topGenres.map((g, i) => (
|
||||||
|
<div key={i} className="yt-bar-row">
|
||||||
|
<div className="yt-bar-row__rank">#{i + 1}</div>
|
||||||
|
<div className="yt-bar-row__info">
|
||||||
|
<div className="yt-bar-row__genre-header">
|
||||||
|
<span className="yt-bar-row__genre-name">{g.genre}</span>
|
||||||
|
<span className="yt-bar-row__flags">
|
||||||
|
{(g.countries ?? []).map(c => FLAG[c] ?? c).join(' ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="yt-bar-row__track">
|
||||||
|
<div
|
||||||
|
className="yt-bar-row__fill yt-bar-row__fill--genre"
|
||||||
|
style={{ width: `${(g.score / maxScore) * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="yt-bar-row__value">{g.score}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Suno 프롬프트 추천 */}
|
||||||
|
{suggestions.length > 0 && (
|
||||||
|
<div className="yt-card">
|
||||||
|
<h3 className="yt-card__title">✨ AI 추천 Suno 프롬프트</h3>
|
||||||
|
<div className="yt-prompt-list">
|
||||||
|
{suggestions.map((s, i) => (
|
||||||
|
<div key={i} className="yt-prompt-card">
|
||||||
|
<div className="yt-prompt-card__header">
|
||||||
|
<span className="yt-prompt-card__genre">{s.genre}</span>
|
||||||
|
<span className="yt-prompt-card__countries">
|
||||||
|
{(s.target_countries ?? []).map(c => FLAG[c] ?? c).join(' ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="yt-prompt-card__text"
|
||||||
|
onClick={() => handleCopy(s.suno_prompt, i)}
|
||||||
|
title="클릭해서 복사"
|
||||||
|
>
|
||||||
|
{s.suno_prompt}
|
||||||
|
</button>
|
||||||
|
{copiedIdx === i && (
|
||||||
|
<span className="yt-prompt-card__copied">✓ 복사됨</span>
|
||||||
|
)}
|
||||||
|
{s.reason && (
|
||||||
|
<div className="yt-prompt-card__reason">{s.reason}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 트렌드 리포트 이력 */}
|
||||||
|
<div className="yt-card">
|
||||||
|
<h3 className="yt-card__title">📋 트렌드 리포트 이력</h3>
|
||||||
|
{reports.length === 0 ? (
|
||||||
|
<p className="yt-empty">리포트 이력이 없습니다</p>
|
||||||
|
) : (
|
||||||
|
<div className="yt-report-list">
|
||||||
|
{reports.map(r => (
|
||||||
|
<div
|
||||||
|
key={r.id ?? r.report_date}
|
||||||
|
className={`yt-report-row ${selectedReport?.report_date === r.report_date ? 'is-selected' : ''}`}
|
||||||
|
onClick={() => setSelectedReport(
|
||||||
|
selectedReport?.report_date === r.report_date ? null : r
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="yt-report-row__date">
|
||||||
|
{r.report_date}
|
||||||
|
{r.report_date === latestReport?.report_date && (
|
||||||
|
<span className="yt-report-row__today"> ● 오늘</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="yt-report-row__meta">
|
||||||
|
{r.top_genres?.length ?? 0}개 장르 · {r.recommended_styles?.length ?? 0}개 추천
|
||||||
|
</span>
|
||||||
|
<span className="yt-report-row__action">보기 →</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user