209 lines
9.7 KiB
JavaScript
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>
|
|
);
|
|
}
|