diff --git a/src/pages/music/components/RevenueTab.jsx b/src/pages/music/components/RevenueTab.jsx index 623301e..2fa48b5 100644 --- a/src/pages/music/components/RevenueTab.jsx +++ b/src/pages/music/components/RevenueTab.jsx @@ -1,3 +1,276 @@ +// src/pages/music/components/RevenueTab.jsx +import { useState, useEffect } from 'react'; +import { + getRevenueDashboard, getRevenueRecords, + addRevenueRecord, updateRevenueRecord, deleteRevenueRecord, +} from '../../../api'; + +const COUNTRIES = ['BR', 'US', 'ID', 'MX', 'KR']; +const currentMonth = () => new Date().toISOString().slice(0, 7); + export default function RevenueTab() { - return null; + const [dashboard, setDashboard] = useState(null); + const [records, setRecords] = useState([]); + const [form, setForm] = useState({ + yt_video_id: '', record_month: currentMonth(), + revenue_usd: '', views: '', country: 'BR', + }); + const [saving, setSaving] = useState(false); + const [editingId, setEditingId] = useState(null); + const [editForm, setEditForm] = useState({}); + + const loadAll = async () => { + const [dash, recs] = await Promise.all([ + getRevenueDashboard().catch(() => null), + getRevenueRecords().catch(() => []), + ]); + setDashboard(dash); + setRecords(Array.isArray(recs) ? recs : recs.records ?? []); + }; + + useEffect(() => { loadAll(); }, []); + + const handleAdd = async () => { + if (!form.yt_video_id || !form.revenue_usd || !form.views) return; + setSaving(true); + try { + await addRevenueRecord({ + yt_video_id: form.yt_video_id, + record_month: form.record_month, + revenue_usd: parseFloat(form.revenue_usd), + views: parseInt(form.views, 10), + country: form.country, + }); + setForm({ yt_video_id: '', record_month: currentMonth(), revenue_usd: '', views: '', country: 'BR' }); + await loadAll(); + } catch (e) { + console.error('addRevenueRecord:', e); + } finally { + setSaving(false); + } + }; + + const handleEditSave = async () => { + try { + await updateRevenueRecord(editingId, { + revenue_usd: parseFloat(editForm.revenue_usd), + views: parseInt(editForm.views, 10), + }); + setEditingId(null); + await loadAll(); + } catch (e) { + console.error('updateRevenueRecord:', e); + } + }; + + const handleDelete = async (id) => { + if (!window.confirm('이 기록을 삭제할까요?')) return; + try { + await deleteRevenueRecord(id); + await loadAll(); + } catch (e) { + console.error('deleteRevenueRecord:', e); + } + }; + + // 영상별 RPM 상위 5개 (bar chart 용) + const chartData = records + .filter(r => r.views > 0) + .map(r => ({ + label: r.yt_video_id, + rpm: (r.revenue_usd / r.views) * 1000, + })) + .sort((a, b) => b.rpm - a.rpm) + .slice(0, 5); + const maxRpm = chartData.length > 0 ? Math.max(...chartData.map(d => d.rpm)) : 1; + + return ( +
+ {/* 대시보드 카드 3개 */} +
+
+
총 수익
+
+ ${dashboard?.total_revenue_usd?.toFixed(2) ?? '—'} +
+
누적
+
+
+
총 조회수
+
+ {dashboard?.total_views != null + ? (dashboard.total_views >= 1000 + ? `${(dashboard.total_views / 1000).toFixed(1)}K` + : String(dashboard.total_views)) + : '—'} +
+
누적
+
+
+
평균 RPM
+
+ ${dashboard?.avg_rpm?.toFixed(2) ?? '—'} +
+
가중평균
+
+
+ + {/* 영상별 RPM 바 차트 */} + {chartData.length > 0 && ( +
+

영상별 RPM 비교

+
+ {chartData.map((d, i) => ( +
+
+ {d.label.slice(0, 11)} +
+
+
+
+
${d.rpm.toFixed(2)}
+
+ ))} +
+
+ )} + + {/* 수익 기록 추가 폼 */} +
+

+ 수익 기록 추가

+
+
+ + setForm(f => ({ ...f, yt_video_id: e.target.value }))} + placeholder="dQw4w9WgXcQ" + /> +
+
+ + setForm(f => ({ ...f, record_month: e.target.value }))} + /> +
+
+ + setForm(f => ({ ...f, revenue_usd: e.target.value }))} + placeholder="3.45" + /> +
+
+ + setForm(f => ({ ...f, views: e.target.value }))} + placeholder="1200" + /> +
+
+
+ + +
+
+ + {/* 수익 기록 테이블 */} +
+

수익 기록

+ {records.length === 0 ? ( +

수익 기록이 없습니다. 위 폼으로 추가해보세요.

+ ) : ( +
+
+ 영상 ID + + 수익 + 조회수 + RPM + +
+ {records.map(rec => ( + editingId === rec.id ? ( +
+ {rec.yt_video_id.slice(0, 11)} + {rec.record_month} + setEditForm(f => ({ ...f, revenue_usd: e.target.value }))} + /> + setEditForm(f => ({ ...f, views: e.target.value }))} + /> + +
+ + +
+
+ ) : ( +
{ + setEditingId(rec.id); + setEditForm({ revenue_usd: rec.revenue_usd, views: rec.views }); + }} + style={{ cursor: 'pointer' }} + > + {rec.yt_video_id.slice(0, 11)} + {rec.record_month} + ${rec.revenue_usd?.toFixed(2)} + {rec.views?.toLocaleString()} + + {rec.views > 0 + ? `$${((rec.revenue_usd / rec.views) * 1000).toFixed(2)}` + : '—'} + + +
+ ) + ))} +
+ )} +
+
+ ); }