382 lines
15 KiB
TypeScript
382 lines
15 KiB
TypeScript
|
||
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;
|