Files
web-page/src/pages/music/components/TrendsTab.jsx

209 lines
9.7 KiB
JavaScript

// 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: '🇰🇷' };
function fmtDateTime(iso) {
if (!iso) return null;
const d = new Date(iso);
if (isNaN(d.getTime())) return iso.slice(0, 10);
const today = new Date().toDateString();
if (d.toDateString() === today) {
return `오늘 ${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
}
return iso.slice(0, 10); // YYYY-MM-DD
}
export default function TrendsTab() {
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 [loading, setLoading] = useState(true);
const [researchMsg, setResearchMsg] = useState('');
const loadAll = async () => {
setLoading(true);
try {
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 ?? []);
} catch (e) {
console.error('loadAll:', e);
} finally {
setLoading(false);
}
};
useEffect(() => { loadAll(); }, []);
const handleResearch = async () => {
setResearching(true);
try {
await triggerYoutubeResearch();
setResearchMsg('수집이 시작되었습니다. 잠시 후 새로고침하세요.');
setTimeout(() => setResearchMsg(''), 4000);
} 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;
// Suno 프롬프트: 선택된 리포트가 있으면 그것의 recommended_styles, 없으면 라이브 suggestions
const displaySuggestions = selectedReport
? (selectedReport.recommended_styles ?? [])
: suggestions;
if (loading) return <div className="yt-content"><p className="yt-empty">데이터 로딩 ...</p></div>;
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>{fmtDateTime(latestReport?.created_at) ?? 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>
{researchMsg && <p className="yt-empty" style={{ color: '#22c55e', marginTop: 4 }}>{researchMsg}</p>}
</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 프롬프트 추천 */}
{displaySuggestions.length > 0 && (
<div className="yt-card">
<h3 className="yt-card__title">
{selectedReport
? `${selectedReport.report_date} 추천 프롬프트`
: '✨ AI 추천 Suno 프롬프트'}
</h3>
<div className="yt-prompt-list">
{displaySuggestions.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);
setCopiedIdx(null);
}}
>
<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.insights ? r.insights.slice(0, 60) + (r.insights.length >= 60 ? '…' : '') : ''}
</span>
<span className="yt-report-row__action">보기 </span>
</div>
))}
</div>
)}
</div>
</div>
);
}