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() {
|
||||
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