o2o-castad-frontend/src/pages/Dashboard/MyInfoContent.tsx

382 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { getSocialAccounts, getYouTubeConnectUrl, disconnectSocialAccount, TokenExpiredError, handleSocialReconnect, getUserCredits, requestCreditCharge } from '../../utils/api';
import { SocialAccount } from '../../types/api';
type TabType = 'basic' | 'payment' | 'business';
interface MyInfoContentProps {
initialTab?: TabType;
}
const MyInfoContent: React.FC<MyInfoContentProps> = ({ initialTab }) => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState<TabType>(initialTab || 'business');
const [businessUrl, setBusinessUrl] = useState('');
const [socialAccounts, setSocialAccounts] = useState<SocialAccount[]>([]);
const [isLoadingAccounts, setIsLoadingAccounts] = useState(false);
const [isConnecting, setIsConnecting] = useState<string | null>(null);
const [credits, setCredits] = useState<number | null>(null);
const [isLoadingCredits, setIsLoadingCredits] = useState(false);
const [showChargePopup, setShowChargePopup] = useState(false);
const [chargeAmount, setChargeAmount] = useState('');
const [chargeNote, setChargeNote] = useState('');
const [isRequesting, setIsRequesting] = useState(false);
const [chargeSuccess, setChargeSuccess] = useState(false);
// initialTab 변경 시 탭 업데이트
useEffect(() => {
if (initialTab) setActiveTab(initialTab);
}, [initialTab]);
// 크레딧 로드
useEffect(() => {
if (activeTab === 'payment') {
loadCredits();
}
}, [activeTab]);
const loadCredits = async () => {
setIsLoadingCredits(true);
try {
const data = await getUserCredits();
setCredits(data.credits);
} catch (error) {
console.error('Failed to load credits:', error);
} finally {
setIsLoadingCredits(false);
}
};
// 소셜 계정 목록 로드
useEffect(() => {
loadSocialAccounts();
}, []);
const loadSocialAccounts = async () => {
setIsLoadingAccounts(true);
try {
const response = await getSocialAccounts();
setSocialAccounts(response.accounts || []);
} catch (error) {
if (error instanceof TokenExpiredError) {
alert(t('myInfo.youtubeExpiredAlert'));
handleSocialReconnect(error.reconnectUrl);
return;
}
console.error('Failed to load social accounts:', error);
} finally {
setIsLoadingAccounts(false);
}
};
// YouTube 연결
const handleYoutubeConnect = async () => {
setIsConnecting('youtube');
try {
const response = await getYouTubeConnectUrl();
window.location.href = response.auth_url;
} catch (error) {
if (error instanceof TokenExpiredError) {
alert(t('myInfo.youtubeExpiredAlert'));
handleSocialReconnect(error.reconnectUrl);
return;
}
console.error('Failed to get YouTube connect URL:', error);
setIsConnecting(null);
}
};
// 소셜 계정 연결 해제 (특정 계정 ID로)
const handleDisconnectAccount = async (accountId: number) => {
try {
await disconnectSocialAccount(accountId);
setSocialAccounts(prev => prev.filter(acc => acc.id !== accountId));
} catch (error) {
if (error instanceof TokenExpiredError) {
alert(t('myInfo.youtubeExpiredAlert'));
handleSocialReconnect(error.reconnectUrl);
return;
}
console.error('Failed to disconnect:', error);
}
};
// 플랫폼별 연결된 모든 계정 가져오기
const getConnectedAccounts = (platform: string) => {
return socialAccounts.filter(acc => acc.platform === platform && acc.is_active);
};
const youtubeAccounts = getConnectedAccounts('youtube');
const instagramAccounts = getConnectedAccounts('instagram');
const hasConnectedAccounts = youtubeAccounts.length > 0 || instagramAccounts.length > 0;
const tabs = [
{ id: 'basic' as TabType, label: t('myInfo.tabBasic') },
{ id: 'payment' as TabType, label: t('myInfo.tabPayment') },
{ id: 'business' as TabType, label: t('myInfo.tabBusiness') },
];
return (
<>
{showChargePopup && (
<div className="myinfo-popup-overlay" onClick={() => { setShowChargePopup(false); setChargeSuccess(false); setChargeAmount(''); setChargeNote(''); }}>
<div className="myinfo-popup" onClick={e => e.stopPropagation()}>
{chargeSuccess ? (
<>
<p className="myinfo-popup-message">{t('myInfo.chargeSuccess')}</p>
<button className="myinfo-popup-close" onClick={() => { setShowChargePopup(false); setChargeSuccess(false); setChargeAmount(''); setChargeNote(''); }}>{t('myInfo.chargeConfirm')}</button>
</>
) : (
<>
<h3 className="myinfo-popup-title">{t('myInfo.chargePopupTitle')}</h3>
<div className="myinfo-popup-field">
<label className="myinfo-popup-label">{t('myInfo.chargeAmountLabel')}</label>
<div className="myinfo-popup-counter">
<button
className="myinfo-popup-counter-btn"
onClick={() => setChargeAmount(v => String(Math.max(1, Number(v || 0) - 1)))}
></button>
<input
type="number"
className="myinfo-popup-input myinfo-popup-input--center"
placeholder="0"
value={chargeAmount}
onChange={e => setChargeAmount(e.target.value.replace(/[^0-9]/g, ''))}
min={1}
/>
<button
className="myinfo-popup-counter-btn"
onClick={() => setChargeAmount(v => String(Number(v || 0) + 1))}
>+</button>
</div>
</div>
<div className="myinfo-popup-field">
<label className="myinfo-popup-label">{t('myInfo.chargeNoteLabel')}</label>
<textarea
className="myinfo-popup-textarea"
placeholder={t('myInfo.chargeNotePlaceholder')}
value={chargeNote}
onChange={e => setChargeNote(e.target.value)}
rows={3}
/>
</div>
<div className="myinfo-popup-actions">
<button className="myinfo-popup-cancel" onClick={() => { setShowChargePopup(false); setChargeAmount(''); setChargeNote(''); }}>{t('myInfo.chargeCancel')}</button>
<button
className="myinfo-popup-close"
disabled={!chargeAmount || isRequesting}
onClick={async () => {
setIsRequesting(true);
try {
await requestCreditCharge(Number(chargeAmount), chargeNote);
} catch (e) {
console.error('Charge request failed:', e);
} finally {
setIsRequesting(false);
setChargeSuccess(true);
}
}}
>
{isRequesting ? t('myInfo.chargeSubmitting') : t('myInfo.chargeSubmit')}
</button>
</div>
</>
)}
</div>
</div>
)}
<main className="myinfo-page">
<h1 className="myinfo-title">{t('myInfo.title')}</h1>
{/* 탭 네비게이션 */}
<div className="myinfo-tabs">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`myinfo-tab ${activeTab === tab.id ? 'active' : ''}`}
>
{tab.label}
</button>
))}
</div>
{/* 탭 컨텐츠 */}
<div className="myinfo-content">
{activeTab === 'basic' && (
<div className="myinfo-section">
<p className="myinfo-placeholder">{t('myInfo.basicPlaceholder')}</p>
</div>
)}
{activeTab === 'payment' && (
<div className="myinfo-section">
<h2 className="myinfo-section-title">{t('myInfo.creditsTitle')}</h2>
<div className="myinfo-credits-card">
{isLoadingCredits ? (
<p className="myinfo-placeholder">{t('myInfo.creditsLoading')}</p>
) : (
<>
<div className="myinfo-credits-row">
<span className="myinfo-credits-label">{t('myInfo.creditsLabel')}</span>
<span className="myinfo-credits-value">{credits !== null ? credits.toLocaleString() : '-'} {t('myInfo.creditsUnit')}</span>
</div>
<div className="myinfo-credits-row">
<p className="myinfo-credits-desc">{t('myInfo.creditsDesc')}</p>
<button
className="myinfo-credits-charge-btn"
onClick={() => setShowChargePopup(true)}
>
{t('myInfo.creditsChargeBtn')}
</button>
</div>
</>
)}
</div>
</div>
)}
{activeTab === 'business' && (
<>
{/* 내 비즈니스 섹션 */}
<div className="myinfo-section">
<h2 className="myinfo-section-title">{t('myInfo.myBusiness')}</h2>
<div className="myinfo-business-card">
<h3 className="myinfo-business-empty-title">{t('myInfo.noBusinessTitle')}</h3>
<p className="myinfo-business-empty-desc">
{t('myInfo.noBusinessDesc')}
</p>
<div className="myinfo-business-input-row">
<input
type="text"
value={businessUrl}
onChange={(e) => setBusinessUrl(e.target.value)}
placeholder={t('myInfo.naverMapUrlPlaceholder')}
className="myinfo-business-input"
/>
<button className="myinfo-business-submit">
{t('myInfo.registerBusiness')}
</button>
</div>
</div>
</div>
{/* 소셜 채널 섹션 */}
<div className="myinfo-section">
<h2 className="myinfo-section-title">{t('myInfo.socialChannels')}</h2>
{/* 연결 버튼들 (가로 배치) */}
<div className="myinfo-social-buttons">
<button
onClick={handleYoutubeConnect}
disabled={isConnecting === 'youtube'}
className={`myinfo-social-btn ${youtubeAccounts.length > 0 ? 'connected' : ''}`}
>
<img src="/assets/images/social-youtube.png" alt="YouTube" className="myinfo-social-btn-icon" />
<span>{isConnecting === 'youtube' ? t('myInfo.youtubeConnecting') : t('myInfo.youtubeConnect')}</span>
</button>
<button
disabled
className="myinfo-social-btn disabled"
>
<img src="/assets/images/social-instagram.png" alt="Instagram" className="myinfo-social-btn-icon" />
<span>{t('myInfo.instagramConnect')}</span>
</button>
</div>
{/* 연결된 계정 목록 */}
{hasConnectedAccounts && (
<div className="myinfo-connected-accounts">
{/* YouTube 계정들 */}
{youtubeAccounts.map(account => (
<div key={account.id} className="myinfo-connected-card">
<div className="myinfo-connected-info">
{account.profile_image ? (
<img
src={account.profile_image}
alt={account.display_name}
className="myinfo-connected-avatar"
/>
) : (
<div className="myinfo-connected-icon">
<img src="/assets/images/social-youtube.png" alt="YouTube" />
</div>
)}
<div className="myinfo-connected-text">
<span className="myinfo-connected-channel">{account.display_name}</span>
<span className="myinfo-connected-platform">YouTube</span>
</div>
</div>
<div className="myinfo-connected-actions">
<span className="myinfo-connected-badge">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<polyline points="20 6 9 17 4 12" />
</svg>
{t('myInfo.connected')}
</span>
<button
onClick={() => handleDisconnectAccount(account.id)}
className="myinfo-connected-disconnect"
>
{t('myInfo.disconnectAccount')}
</button>
</div>
</div>
))}
{/* Instagram 계정들 */}
{instagramAccounts.map(account => (
<div key={account.id} className="myinfo-connected-card">
<div className="myinfo-connected-info">
{account.profile_image ? (
<img
src={account.profile_image}
alt={account.display_name}
className="myinfo-connected-avatar"
/>
) : (
<div className="myinfo-connected-icon">
<img src="/assets/images/social-instagram.png" alt="Instagram" />
</div>
)}
<div className="myinfo-connected-text">
<span className="myinfo-connected-channel">{account.display_name}</span>
<span className="myinfo-connected-platform">Instagram</span>
</div>
</div>
<div className="myinfo-connected-actions">
<span className="myinfo-connected-badge">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<polyline points="20 6 9 17 4 12" />
</svg>
{t('myInfo.connected')}
</span>
<button
onClick={() => handleDisconnectAccount(account.id)}
className="myinfo-connected-disconnect"
>
{t('myInfo.disconnectAccount')}
</button>
</div>
</div>
))}
</div>
)}
{isLoadingAccounts && (
<p className="myinfo-loading">{t('myInfo.loadingAccounts')}</p>
)}
</div>
</>
)}
</div>
</main>
</>
);
};
export default MyInfoContent;