feat(youtube-tab): TrendsTab 시장 트렌드 서브탭

This commit is contained in:
2026-05-01 14:51:10 +09:00
parent 3e54b2c98d
commit 3f2fdb095c

View File

@@ -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>
);
}