// src/renderer/components/CloudSyncSection.tsx // Phase V2-4: Settings에 표시되는 Cloud Sync 섹션 // 로그인 상태 / Sync Now 버튼 / 마지막 동기화 / 환경변수 설정 import { useEffect, useState } from 'react' import { Box, Button, Stack, TextField, Alert, CircularProgress } from '@mui/material' import CloudIcon from '@mui/icons-material/Cloud' import CloudDoneIcon from '@mui/icons-material/CloudDone' import GoogleIcon from '@mui/icons-material/Google' import GitHubIcon from '@mui/icons-material/GitHub' import { d3roPalette, typoSx } from '@d3ro/ui/theme' import { useI18n } from '@d3ro/i18n' interface CloudSyncState { authenticated: boolean userEmail: string | null lastSyncAt: number | null syncing: boolean } interface SyncProgress { current: number total: number table: string } export function CloudSyncSection(): React.ReactElement { const { t: _t } = useI18n() const [state, setState] = useState({ authenticated: false, userEmail: null, lastSyncAt: null, syncing: false }) const [supabaseUrl, setSupabaseUrl] = useState('') const [anonKey, setAnonKey] = useState('') const [progress, setProgress] = useState(null) const [error, setError] = useState(null) const [info, setInfo] = useState(null) const [busy, setBusy] = useState(false) // 초기 상태 로드 + 이벤트 구독 useEffect(() => { void window.electronAPI.cloudSync.getState().then((r) => { if (r.success) setState(r.data) }) // 저장된 supabase 설정 로드 void window.electronAPI.config.get({ key: 'supabaseUrl' as never }).then((r) => { if (r.success && typeof r.data === 'string') setSupabaseUrl(r.data) }) void window.electronAPI.config.get({ key: 'supabaseAnonKey' as never }).then((r) => { if (r.success && typeof r.data === 'string') setAnonKey(r.data) }) const unsubAuth = window.electronAPI.cloudSync.onAuthChanged((payload) => { setState((prev) => ({ ...prev, authenticated: payload.user !== null, userEmail: payload.user?.email ?? null })) }) const unsubProgress = window.electronAPI.cloudSync.onSyncProgress((p) => { setProgress(p) }) const unsubComplete = window.electronAPI.cloudSync.onSyncComplete((p) => { setProgress(null) setInfo(`동기화 완료: ${p.pushed}건${p.errors.length > 0 ? ` (오류 ${p.errors.length})` : ''}`) setBusy(false) setState((prev) => ({ ...prev, syncing: false, lastSyncAt: Date.now() })) }) const unsubError = window.electronAPI.cloudSync.onSyncError((p) => { setProgress(null) setError(p.error) setBusy(false) setState((prev) => ({ ...prev, syncing: false })) }) return () => { unsubAuth() unsubProgress() unsubComplete() unsubError() } }, []) async function handleConfigure(): Promise { setError(null) setBusy(true) try { const r = await window.electronAPI.cloudSync.configure({ url: supabaseUrl, anonKey }) if (!r.success) { setError(r.error.message) } else { setInfo('Supabase 연결 설정 저장됨') } } finally { setBusy(false) } } async function handleSignIn(provider: 'google' | 'github'): Promise { setError(null) setBusy(true) try { const r = await window.electronAPI.cloudSync.signIn({ provider }) if (!r.success) { setError(r.error.message) } else { setInfo('브라우저에서 로그인을 완료해주세요...') } } finally { setBusy(false) } } async function handleSignOut(): Promise { setBusy(true) try { await window.electronAPI.cloudSync.signOut() setInfo('로그아웃되었습니다') } finally { setBusy(false) } } async function handleSync(): Promise { setError(null) setBusy(true) setState((prev) => ({ ...prev, syncing: true })) try { await window.electronAPI.cloudSync.pushAll() } finally { // 완료/에러 이벤트로 setBusy(false) 처리됨 } } async function handlePull(): Promise { setError(null) setBusy(true) setState((prev) => ({ ...prev, syncing: true })) try { await window.electronAPI.cloudSync.pullAll() } finally { // 완료/에러 이벤트로 setBusy(false) 처리됨 } } const lastSyncText = state.lastSyncAt ? new Date(state.lastSyncAt).toLocaleString('ko-KR') : '없음' return ( {state.authenticated ? ( ) : ( )} Cloud Sync {!state.authenticated && ( SUPABASE 설정 (env 미설정 시 직접 입력) setSupabaseUrl(e.target.value)} placeholder="https://your-project.supabase.co" fullWidth /> setAnonKey(e.target.value)} placeholder="eyJ..." type="password" fullWidth /> OAuth 로그인 )} {state.authenticated && ( 로그인됨: {state.userEmail ?? '(이메일 없음)'} 마지막 동기화: {lastSyncText} {progress && ( {progress.table}: {progress.current}/{progress.total} 0 ? (progress.current / progress.total) * 100 : 0}%`, bgcolor: d3roPalette.accent.amber, transition: 'width 200ms' }} /> )} )} {error && ( {error} )} {info && !error && ( {info} )} ) }