page.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. 'use client';
  2. import { useEffect, useState } from 'react';
  3. import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
  4. import { faCopy, faArrowUpRightFromSquare, faWallet, faCalendarDay, faCalendar, faClock } from '@fortawesome/free-solid-svg-icons';
  5. import { fetchApi } from '@/lib/utils/client';
  6. import type { DashboardResponse, DashboardWidgetUrls } from '@/types/response/studio/dashboard';
  7. import './style.scss';
  8. const WIDGET_LABELS: { key: keyof Omit<DashboardWidgetUrls, 'widgetToken'>; label: string; description: string }[] = [
  9. { key: 'alert', label: '후원 알림', description: 'OBS에 후원 알림 표시' },
  10. { key: 'goal', label: '후원 목표', description: '목표 금액 진행률 표시' },
  11. { key: 'rank', label: '후원 순위', description: '후원자 순위 표시' },
  12. { key: 'crew', label: '크루 리더보드', description: '크루 순위 표시' },
  13. { key: 'remote', label: '리모콘', description: '후원 알림 제어' },
  14. ];
  15. export default function DashboardPage() {
  16. const [data, setData] = useState<DashboardResponse|null>(null);
  17. const [loading, setLoading] = useState(true);
  18. const [copiedKey, setCopiedKey] = useState<string|null>(null);
  19. useEffect(() => {
  20. loadDashboard();
  21. }, []);
  22. const loadDashboard = async () => {
  23. try {
  24. const res = await fetchApi<DashboardResponse>('/api/studio/dashboard', { silent: true });
  25. if (res.data) {
  26. setData(res.data);
  27. }
  28. } catch {} finally {
  29. setLoading(false);
  30. }
  31. };
  32. const getWidgetFullUrl = (path: string) => {
  33. if (typeof window === 'undefined') {
  34. return path;
  35. }
  36. return `${window.location.origin}${path}`;
  37. };
  38. const handleCopy = async (key: string, path: string) => {
  39. try {
  40. await navigator.clipboard.writeText(getWidgetFullUrl(path));
  41. setCopiedKey(key);
  42. setTimeout(() => setCopiedKey(null), 2000);
  43. } catch {}
  44. };
  45. const handleOpen = (path: string) => {
  46. window.open(getWidgetFullUrl(path), '_blank');
  47. };
  48. if (loading) {
  49. return (
  50. <div className="studio-page">
  51. <div className="dashboard">
  52. <h1 className="studio-page__title">대시보드</h1>
  53. <p className="studio-page__empty">준비 중...</p>
  54. </div>
  55. </div>
  56. );
  57. }
  58. const financial = data?.financial;
  59. const widgets = data?.widgets;
  60. const recentDonations = data?.recentDonations ?? [];
  61. return (
  62. <div className="studio-page">
  63. <div className="dashboard">
  64. <h1 className="studio-page__title">대시보드</h1>
  65. {/* 재무 요약 카드 */}
  66. <div className="dashboard__cards">
  67. <div className="dashboard__card">
  68. <span className="dashboard__card-icon">
  69. <FontAwesomeIcon icon={faWallet} />
  70. </span>
  71. <span className="dashboard__card-label">출금 가능 잔액</span>
  72. <span className="dashboard__card-value dashboard__card-value--primary">
  73. {(financial?.availableBalance ?? 0).toLocaleString()}원
  74. </span>
  75. </div>
  76. <div className="dashboard__card">
  77. <span className="dashboard__card-icon">
  78. <FontAwesomeIcon icon={faCalendarDay} />
  79. </span>
  80. <span className="dashboard__card-label">오늘 후원</span>
  81. <span className="dashboard__card-value">
  82. {(financial?.todayDonations ?? 0).toLocaleString()}원
  83. </span>
  84. </div>
  85. <div className="dashboard__card">
  86. <span className="dashboard__card-icon">
  87. <FontAwesomeIcon icon={faCalendar} />
  88. </span>
  89. <span className="dashboard__card-label">이번 달 후원</span>
  90. <span className="dashboard__card-value">
  91. {(financial?.monthDonations ?? 0).toLocaleString()}원
  92. </span>
  93. </div>
  94. <div className="dashboard__card">
  95. <span className="dashboard__card-icon">
  96. <FontAwesomeIcon icon={faClock} />
  97. </span>
  98. <span className="dashboard__card-label">출금 대기</span>
  99. <span className="dashboard__card-value dashboard__card-value--danger">
  100. {(financial?.pendingWithdrawal ?? 0).toLocaleString()}원
  101. </span>
  102. </div>
  103. </div>
  104. {/* 위젯 URL */}
  105. {widgets && (
  106. <section className="dashboard__section">
  107. <h2 className="dashboard__section-title">위젯 URL</h2>
  108. <p className="dashboard__section-desc">OBS 브라우저 소스에 아래 URL을 등록하세요.</p>
  109. <div className="dashboard__widget-list">
  110. {WIDGET_LABELS.map(({ key, label, description }) => {
  111. const path = widgets[key];
  112. const isCopied = copiedKey === key;
  113. return (
  114. <div key={key} className="dashboard__widget-item">
  115. <div className="dashboard__widget-info">
  116. <span className="dashboard__widget-label">{label}</span>
  117. <span className="dashboard__widget-desc">{description}</span>
  118. </div>
  119. <div className="dashboard__widget-url">
  120. <code className="dashboard__widget-url-text">{getWidgetFullUrl(path)}</code>
  121. </div>
  122. <div className="dashboard__widget-actions">
  123. <button
  124. type="button"
  125. className={`dashboard__widget-btn${isCopied ? ' dashboard__widget-btn--copied' : ''}`}
  126. onClick={() => handleCopy(key, path)}
  127. title="URL 복사"
  128. >
  129. <FontAwesomeIcon icon={faCopy} />
  130. {isCopied ? '복사됨' : '복사'}
  131. </button>
  132. <button
  133. type="button"
  134. className="dashboard__widget-btn dashboard__widget-btn--open"
  135. onClick={() => handleOpen(path)}
  136. title="새 창에서 열기"
  137. >
  138. <FontAwesomeIcon icon={faArrowUpRightFromSquare} />
  139. 열기
  140. </button>
  141. </div>
  142. </div>
  143. );
  144. })}
  145. </div>
  146. </section>
  147. )}
  148. {/* 최근 후원 */}
  149. <section className="dashboard__section">
  150. <h2 className="dashboard__section-title">최근 후원</h2>
  151. {recentDonations.length === 0 ? (
  152. <div className="dashboard__empty">아직 후원 내역이 없습니다.</div>
  153. ) : (
  154. <div className="dashboard__donation-list">
  155. {recentDonations.map(d => (
  156. <div key={d.id} className="dashboard__donation-item">
  157. <div className="dashboard__donation-info">
  158. <span className="dashboard__donation-name">{d.sendName}</span>
  159. <span className="dashboard__donation-amount">{d.amount.toLocaleString()}원</span>
  160. </div>
  161. {d.message && (
  162. <div className="dashboard__donation-msg">{d.message}</div>
  163. )}
  164. <div className="dashboard__donation-time">
  165. {new Date(d.createdAt).toLocaleString('ko-KR')}
  166. </div>
  167. </div>
  168. ))}
  169. </div>
  170. )}
  171. </section>
  172. </div>
  173. </div>
  174. );
  175. }