'use client'; import './style.scss'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faGoogle } from '@fortawesome/free-brands-svg-icons'; import NavTabs from '../navTabs'; import { useConfigContext } from '@/contexts/configProvider'; import { fetchApi } from '@/lib/utils/client'; import { SnsLinkItem, SnsListResponse, SnsDisconnectResponse } from '@/types/sns'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; const STATE_STORAGE_KEY = 'antooza-sns-google-state'; const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'; const GOOGLE_SCOPES = 'openid email profile'; type ProviderMeta = { provider: 'Google' | 'Naver' | 'Kakao'; label: string; description: string; enabled: boolean; }; const PROVIDERS: ProviderMeta[] = [ { provider: 'Google', label: 'Google', description: 'Google 계정으로 개미투자에 빠르게 로그인할 수 있어요.', enabled: true }, { provider: 'Naver', label: 'Naver', description: '준비중입니다.', enabled: false }, { provider: 'Kakao', label: 'Kakao', description: '준비중입니다.', enabled: false } ]; function formatKoreanDateTime(iso: string|null): string { if (!iso) { return '-'; } const d = new Date(iso); if (Number.isNaN(d.getTime())) { return '-'; } const yyyy = d.getFullYear(); const mm = String(d.getMonth() + 1).padStart(2, '0'); const dd = String(d.getDate()).padStart(2, '0'); const hh = String(d.getHours()).padStart(2, '0'); const mi = String(d.getMinutes()).padStart(2, '0'); return `${yyyy}-${mm}-${dd} ${hh}:${mi}`; } export default function SnsLinkPage() { const config = useConfigContext(); const googleClientId = config?.external?.googleClientId ?? ''; const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [pendingProvider, setPendingProvider] = useState(null); const [confirmTarget, setConfirmTarget] = useState(null); const [statusMessage, setStatusMessage] = useState(null); const loadItems = useCallback(async () => { try { const res = await fetchApi('/api/mypage/sns', { silent: true }); if (res.data) { setItems(res.data.items); } } catch { // silent } finally { setLoading(false); } }, []); useEffect(() => { loadItems(); }, [loadItems]); useEffect(() => { const url = new URL(window.location.href); const connected = url.searchParams.get('connected'); const error = url.searchParams.get('error'); if (connected) { setStatusMessage(`${connected.toUpperCase()} 계정 연동이 완료되었습니다.`); url.searchParams.delete('connected'); window.history.replaceState({}, '', url.pathname + (url.search ? `${url.search}` : '')); } if (error) { setStatusMessage(`연동에 실패했습니다: ${error}`); url.searchParams.delete('error'); window.history.replaceState({}, '', url.pathname + (url.search ? `${url.search}` : '')); } }, []); const itemMap = useMemo(() => { const map = new Map(); items.forEach((item) => map.set(item.provider, item)); return map; }, [items]); const handleConnectGoogle = useCallback(() => { if (!googleClientId) { setStatusMessage('Google OAuth 설정이 누락되어 있습니다. 관리자에게 문의해주세요.'); return; } const state = (typeof crypto !== 'undefined' && 'randomUUID' in crypto) ? crypto.randomUUID() : Math.random().toString(36).slice(2); try { window.sessionStorage.setItem(STATE_STORAGE_KEY, state); } catch { // silent } const redirectUri = `${window.location.origin}/auth/sns/google/callback`; const params = new URLSearchParams({ response_type: 'code', access_type: 'offline', prompt: 'consent', state, redirect_uri: redirectUri, scope: GOOGLE_SCOPES, client_id: googleClientId, include_granted_scopes: 'true' }); window.location.href = `${GOOGLE_AUTH_URL}?${params.toString()}`; }, [googleClientId]); const handleDisconnect = useCallback(async (item: SnsLinkItem) => { setPendingProvider(item.provider); try { const res = await fetchApi(`/api/mypage/sns/${item.provider.toLowerCase()}`, { method: 'DELETE', silent: true }); if (res.success) { if (res.data?.revoked) { setStatusMessage(`${item.provider} 연동이 해제되었습니다.`); } else { setStatusMessage(`${item.provider} 연동을 해제했지만 Google 측 권한 회수 호출이 실패했습니다. 안전하게 하려면 Google 계정 설정의 "보안 → 타사 앱 액세스" 에서도 개미투자 권한을 직접 제거해주세요.`); } await loadItems(); } else { setStatusMessage(res.message || '연동 해제에 실패했습니다.'); } } catch { setStatusMessage('연동 해제 중 오류가 발생했습니다.'); } finally { setPendingProvider(null); setConfirmTarget(null); } }, [loadItems]); const handleToggle = useCallback((meta: ProviderMeta) => { if (!meta.enabled) { return; } const item = itemMap.get(meta.provider); if (item?.connected) { setConfirmTarget(item); return; } if (meta.provider === 'Google') { handleConnectGoogle(); } }, [itemMap, handleConnectGoogle]); return ( <> { if (!next) { setConfirmTarget(null); } }} > {confirmTarget?.provider} 연동을 해제할까요? 해제하면 개미투자에 저장된 OAuth 토큰이 삭제되고 Google 측 권한도 회수됩니다. 언제든 다시 연동할 수 있습니다. ); }