| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- 'use client';
- import { useEffect, useMemo, useState } from 'react';
- import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
- import { faCopy, faArrowUpRightFromSquare, faWallet, faCalendarDay, faCartShopping, faClock } from '@fortawesome/free-solid-svg-icons';
- import { fetchApi } from '@/lib/utils/client';
- import type { DashboardResponse, DashboardWidgetConfig } from '@/types/response/studio/dashboard';
- import './style.scss';
- type SimpleWidget = {
- key: string;
- label: string;
- description: string;
- path: string;
- };
- export default function DashboardPage()
- {
- const [data, setData] = useState<DashboardResponse|null>(null);
- const [loading, setLoading] = useState(true);
- const [copiedKey, setCopiedKey] = useState<string|null>(null);
- const [selectedGoalID, setSelectedGoalID] = useState<number|null>(null);
- const [selectedRankID, setSelectedRankID] = useState<number|null>(null);
- const [selectedCrewID, setSelectedCrewID] = useState<number|null>(null);
- useEffect(() => {
- loadDashboard();
- }, []);
- const loadDashboard = async () => {
- try {
- const res = await fetchApi<DashboardResponse>('/api/studio/dashboard', { silent: true });
- if (res.data) {
- setData(res.data);
- if (res.data.goalConfigs.length > 0) {
- setSelectedGoalID(res.data.goalConfigs[0].id);
- }
- if (res.data.rankConfigs.length > 0) {
- setSelectedRankID(res.data.rankConfigs[0].id);
- }
- if (res.data.crewConfigs.length > 0) {
- setSelectedCrewID(res.data.crewConfigs[0].id);
- }
- }
- } catch {} finally {
- setLoading(false);
- }
- };
- const getWidgetFullUrl = (path: string) => {
- if (typeof window === 'undefined') {
- return path;
- }
- return `${window.location.origin}${path}`;
- };
- const handleCopy = async (key: string, path: string) => {
- try {
- await navigator.clipboard.writeText(getWidgetFullUrl(path));
- setCopiedKey(key);
- setTimeout(() => setCopiedKey(null), 2000);
- } catch {}
- };
- const handleOpen = (key: string, path: string) => {
- const url = getWidgetFullUrl(path);
- if (key === 'remote' && typeof window !== 'undefined' && window.innerWidth >= 768) {
- const w = 880, h = 900;
- const left = window.screenX + (window.outerWidth - w) / 2;
- const top = window.screenY + (window.outerHeight - h) / 2;
- window.open(url, 'dpot-remote', `width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`);
- return;
- }
- window.open(url, '_blank');
- };
- const widgets = data?.widgets;
- const goalConfigs = data?.goalConfigs ?? [];
- const rankConfigs = data?.rankConfigs ?? [];
- const crewConfigs = data?.crewConfigs ?? [];
- const goalPath = useMemo(
- () => widgets && selectedGoalID ? `${widgets.goal}?configID=${selectedGoalID}` : '',
- [widgets, selectedGoalID]
- );
- const rankPath = useMemo(
- () => widgets && selectedRankID ? `${widgets.rank}?configID=${selectedRankID}` : '',
- [widgets, selectedRankID]
- );
- const crewPath = useMemo(
- () => widgets && selectedCrewID ? `${widgets.crew}?configID=${selectedCrewID}` : '',
- [widgets, selectedCrewID]
- );
- if (loading) {
- return (
- <div className="studio-page">
- <div className="dashboard">
- <h1 className="studio-page__title">대시보드</h1>
- <p className="studio-page__empty">준비 중...</p>
- </div>
- </div>
- );
- }
- const financial = data?.financial;
- const recentDonations = data?.recentDonations ?? [];
- const renderConfigSelector = (
- key: 'goal'|'rank'|'crew',
- label: string,
- description: string,
- configs: DashboardWidgetConfig[],
- selectedID: number|null,
- setSelectedID: (id: number|null) => void,
- path: string,
- basePath: string
- ) => {
- const isCopied = copiedKey === key;
- return (
- <div key={key} className="dashboard__widget-item">
- <div className="dashboard__widget-info">
- <span className="dashboard__widget-label">{label}</span>
- <span className="dashboard__widget-desc">{description}</span>
- </div>
- <div className="dashboard__widget-url">
- {configs.length === 0 ? (
- <span className="dashboard__widget-empty-inline">활성 설정이 없습니다.</span>
- ) : (
- <select
- className="dashboard__widget-select"
- value={selectedID ?? ''}
- onChange={e => setSelectedID(e.target.value ? parseInt(e.target.value, 10) : null)}
- aria-label={`${label} 설정 선택`}
- >
- {configs.map(c => (
- <option key={c.id} value={c.id}>
- {c.title || `#${c.id}`} - {getWidgetFullUrl(`${basePath}?configID=${c.id}`)}
- </option>
- ))}
- </select>
- )}
- </div>
- <div className="dashboard__widget-actions">
- <button
- type="button"
- className={`dashboard__widget-btn${isCopied ? ' dashboard__widget-btn--copied' : ''}`}
- onClick={() => handleCopy(key, path)}
- title="URL 복사"
- disabled={!path}
- >
- <FontAwesomeIcon icon={faCopy} />
- {isCopied ? '복사됨' : '복사'}
- </button>
- <button
- type="button"
- className="dashboard__widget-btn dashboard__widget-btn--open"
- onClick={() => handleOpen(key, path)}
- title="새 창에서 열기"
- disabled={!path}
- >
- <FontAwesomeIcon icon={faArrowUpRightFromSquare} />
- 열기
- </button>
- </div>
- </div>
- );
- };
- const renderSimpleWidget = ({ key, label, description, path }: SimpleWidget) => {
- const isCopied = copiedKey === key;
- return (
- <div key={key} className="dashboard__widget-item">
- <div className="dashboard__widget-info">
- <span className="dashboard__widget-label">{label}</span>
- <span className="dashboard__widget-desc">{description}</span>
- </div>
- <div className="dashboard__widget-url">
- <code className="dashboard__widget-url-text">{getWidgetFullUrl(path)}</code>
- </div>
- <div className="dashboard__widget-actions">
- <button
- type="button"
- className={`dashboard__widget-btn${isCopied ? ' dashboard__widget-btn--copied' : ''}`}
- onClick={() => handleCopy(key, path)}
- title="URL 복사"
- >
- <FontAwesomeIcon icon={faCopy} />
- {isCopied ? '복사됨' : '복사'}
- </button>
- <button
- type="button"
- className="dashboard__widget-btn dashboard__widget-btn--open"
- onClick={() => handleOpen(key, path)}
- title={key === 'remote' ? '리모콘 팝업 열기' : '새 창에서 열기'}
- >
- <FontAwesomeIcon icon={faArrowUpRightFromSquare} />
- 열기
- </button>
- </div>
- </div>
- );
- };
- return (
- <div className="studio-page">
- <div className="dashboard">
- <h1 className="studio-page__title mb-4">대시보드</h1>
- {/* 재무 요약 카드 */}
- <div className="dashboard__cards">
- <div className="dashboard__card">
- <span className="dashboard__card-icon">
- <FontAwesomeIcon icon={faWallet} />
- </span>
- <span className="dashboard__card-label">출금 가능 잔액</span>
- <span className="dashboard__card-value dashboard__card-value--primary">
- {(financial?.availableBalance ?? 0).toLocaleString()}원
- </span>
- </div>
- <div className="dashboard__card">
- <span className="dashboard__card-icon">
- <FontAwesomeIcon icon={faCalendarDay} />
- </span>
- <span className="dashboard__card-label">오늘 후원</span>
- <span className="dashboard__card-value">
- {(financial?.todayDonations ?? 0).toLocaleString()}원
- </span>
- </div>
- <div className="dashboard__card">
- <span className="dashboard__card-icon">
- <FontAwesomeIcon icon={faCartShopping} />
- </span>
- <span className="dashboard__card-label">이번 달 상점 판매</span>
- <span className="dashboard__card-value">
- {(financial?.monthStoreSales ?? 0).toLocaleString()}원
- </span>
- </div>
- <div className="dashboard__card">
- <span className="dashboard__card-icon">
- <FontAwesomeIcon icon={faClock} />
- </span>
- <span className="dashboard__card-label">출금 대기</span>
- <span className="dashboard__card-value dashboard__card-value--danger">
- {(financial?.pendingWithdrawal ?? 0).toLocaleString()}원
- </span>
- </div>
- </div>
- {/* 위젯 URL */}
- {widgets && (
- <section className="dashboard__section">
- <h2 className="dashboard__section-title">위젯 URL</h2>
- <p className="dashboard__section-desc">
- OBS 브라우저 소스에 아래 URL을 등록하세요. 후원 목표/순위는 등록한 활성 설정 중 원하는 항목을 선택해 주세요.
- </p>
- <div className="dashboard__widget-list">
- {renderSimpleWidget({ key: 'alert', label: '후원 알림', description: 'OBS에 후원 알림 표시', path: widgets.alert })}
- {renderConfigSelector('goal', '후원 목표', '목표 금액 진행률 표시', goalConfigs, selectedGoalID, setSelectedGoalID, goalPath, widgets.goal)}
- {renderConfigSelector('rank', '후원 순위', '후원자 순위 표시', rankConfigs, selectedRankID, setSelectedRankID, rankPath, widgets.rank)}
- {renderConfigSelector('crew', '크루 리더보드', '크루 순위 표시', crewConfigs, selectedCrewID, setSelectedCrewID, crewPath, widgets.crew)}
- {renderSimpleWidget({ key: 'remote', label: '리모콘', description: '후원 알림 제어', path: widgets.remote })}
- </div>
- </section>
- )}
- {/* 최근 후원 */}
- <section className="dashboard__section">
- <h2 className="dashboard__section-title">최근 후원</h2>
- {recentDonations.length === 0 ? (
- <div className="dashboard__empty">아직 후원 내역이 없습니다.</div>
- ) : (
- <div className="dashboard__donation-list">
- {recentDonations.map(d => (
- <div key={d.id} className="dashboard__donation-item">
- <div className="dashboard__donation-info">
- <span className="dashboard__donation-name">{d.sendName}</span>
- <span className="dashboard__donation-amount">{d.amount.toLocaleString()}원</span>
- </div>
- {d.message && (
- <div className="dashboard__donation-msg">{d.message}</div>
- )}
- <div className="dashboard__donation-time">
- {new Date(d.createdAt).toLocaleString('ko-KR')}
- </div>
- </div>
- ))}
- </div>
- )}
- </section>
- </div>
- </div>
- );
- }
|