import React, { useState } from 'react'; import { createRoot } from 'react-dom/client'; import { ErrorBoundary } from './components/ErrorBoundary'; import { AuthModal } from './components/AuthModal'; import { ProfileModal } from './components/ProfileModal'; import { UserProfileMenu } from './components/UserProfile'; import { ImageUploadWithAuth } from './components/ImageUploadWithAuth'; import MultiImageUpload from './components/MultiImageUpload'; import { ImageAnalysisDisplay } from './components/ImageAnalysisDisplay'; import { AdminDashboard } from './components/AdminDashboard'; import { AuthProvider, useAuth } from './contexts/AuthContext'; import { LanguageProvider, useLanguage } from './contexts/LanguageContext'; import { LanguageToggle } from './components/LanguageToggle'; import './styles/global.css'; const AppContent: React.FC = () => { const { language, t } = useLanguage(); // AuthProvider에서 제공하는 useAuth 훅 사용 const { user, userProfile, loading, sessionExpired, signIn, signUp, signOut, refreshProfile, extendSession } = useAuth(); const [authModalOpen, setAuthModalOpen] = useState(false); const [authMode, setAuthMode] = useState<'login' | 'signup'>('login'); const [uploadedImage, setUploadedImage] = useState(null); const [isAnalyzing, setIsAnalyzing] = useState(false); const [recommendations, setRecommendations] = useState([]); const [error, setError] = useState(null); const [viewMode, setViewMode] = useState<'single' | 'multi' | 'personalized'>('single'); const [analysisResult, setAnalysisResult] = useState(null); const [profileModalOpen, setProfileModalOpen] = useState(false); const handleAuthSuccess = async (email: string, password: string, displayName?: string, role?: string, artistInfo?: any) => { const result = authMode === 'login' ? await signIn(email, password) : await signUp(email, password, displayName, role, artistInfo); if (result.success) { setAuthModalOpen(false); // 활동 감지로 세션 연장 extendSession(); } return result; }; const handleSignOut = async () => { await signOut(); }; // 로딩 상태 표시 if (loading) { return (

로딩 중...

); } // 세션 만료 알림 if (sessionExpired) { return (

세션이 만료되었습니다

1시간 동안 활동이 없어 자동으로 로그아웃되었습니다.

); } const handleImageUpload = async (file: File, uploadId?: string) => { // Reset state setError(null); setRecommendations([]); setAnalysisResult(null); const imageUrl = URL.createObjectURL(file); setUploadedImage(imageUrl); setIsAnalyzing(true); // 이미지 업로드 시 세션 연장 extendSession(); try { const formData = new FormData(); formData.append('image', file); // 로그인한 사용자의 경우 사용자 정보 추가 if (user) { formData.append('userId', user.id); if (uploadId) { formData.append('uploadId', uploadId); } } const response = await fetch('/api/analyze', { method: 'POST', body: formData, }); const result = await response.json(); // API 응답 수신 완료 if (!response.ok) { if (response.status === 429) { throw new Error('일일 분석 제한에 도달했습니다. 내일 다시 시도해주세요.'); } throw new Error(result.error || `서버 오류: ${response.status}`); } // 분석 결과 저장 if (result.analysis) { // 분석 결과 저장 setAnalysisResult(result.analysis); } else { // 분석 필드 누락 경고 } if (result.recommendations && result.recommendations.length > 0) { setRecommendations(result.recommendations); } else { setError('추천할 작품을 찾을 수 없습니다.'); } } catch (error) { // 이미지 분석 실패 setError(error instanceof Error ? error.message : '이미지 분석 중 오류가 발생했습니다.'); } finally { setIsAnalyzing(false); } }; const handleRecommendationClick = async (recommendation: any) => { // 추천 클릭 기록 (로그인한 사용자만) if (user && recommendation.id) { try { await fetch('/api/recommendations/click', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ recommendationId: recommendation.id, userId: user.id, }), }); } catch (error) { console.error('Failed to record click:', error); } } }; // 기본 유사성 이유 생성 함수 const getDefaultSimilarityReason = (artwork: any, rec: any) => { const similarity = rec.similarity || rec.similarity_score?.total || 0; const category = artwork.category || artwork.medium || '작품'; const artist = artwork.artist || '작가'; if (similarity > 0.8) { return `${category} 스타일과 색감이 매우 유사한 작품입니다.`; } else if (similarity > 0.6) { return `구도와 분위기가 비슷한 ${artist}의 작품입니다.`; } else if (similarity > 0.4) { return `색상 조합과 주제가 연관성이 있는 작품입니다.`; } else { return `같은 카테고리의 추천 작품입니다.`; } }; // URL에 따른 라우팅 처리 const pathname = window.location.pathname; // 관리자 대시보드 표시 (임시로 역할 체크 비활성화) if (pathname === '/dashboard') { return ( window.location.href = '/'} /> ); } return (
{/* Floating Background Blobs */}
{/* Header - ArtVibe AI Style */}
{/* Logo */}
{/* Outer frame */} {/* Inner abstract art shape */} {/* Accent dots */} Trouv ART
{/* User Menu */}
{user ? ( ) : ( )}
{/* Hero Section */}

{t('heroTitle1')}
{t('heroTitle2')}

{t('heroSubtitle')}

{/* Main Content */}
{/* Feature Cards - 설명 영역 */}
❤️

{t('uploadMethod')}

{t('uploadMethodDesc')}

{t('imageUpload')}

{t('imageUploadDesc')}

{t('aiAnalysis')}

{t('aiAnalysisDesc')}

{/* Upload Section */}
{viewMode === 'single' && (
{ setAuthMode('login'); setAuthModalOpen(true); }} />
)}
{/* Multi Image Upload */} {viewMode === 'multi' && (
{ // 다중 이미지 분석 완료 }} />
)} {viewMode === 'single' && uploadedImage && (

{t('uploadedImage')}

Uploaded {isAnalyzing && (

{t('analyzing')}

)}
)} {/* 이미지 분석 결과 표시 */} {viewMode === 'single' && ( )} {viewMode === 'single' && error && (

⚠️ {error}

)} {viewMode === 'single' && isAnalyzing && (

{t('analyzingImage')}...

)} {viewMode === 'single' && recommendations.length > 0 && (

{t('recommendations')}

{/* 외부 플랫폼 갤러리 추천 (무료 10개 제한) */}

{t('galleryRecommendations')} ({t('externalPlatforms')})

{t('freeLimit')} / {t('premiumRequired')}
{recommendations .filter((rec) => { // 텀블벅, 그라폴리오, 대학 졸업작품, 학생 작품 필터링 const artwork = rec.artwork || rec; const isTumblbug = artwork.platform === 'tumblbug' || artwork.source === '텀블벅' || artwork.search_source === '텀블벅' || (artwork.source_url && artwork.source_url.includes('tumblbug.com')) || artwork.project_type === '크라우드펀딩'; const isGrafolio = artwork.platform === 'grafolio' || artwork.source === '그라폴리오' || artwork.search_source === '그라폴리오' || (artwork.source_url && artwork.source_url.includes('grafolio.naver.com')); const isKoreanUniversity = artwork.platform === 'university' || artwork.source === '대학 졸업전시' || artwork.category === 'student_work' || artwork.search_source === 'graduation' || (artwork.university && ( artwork.university.includes('대학') || artwork.university.includes('대학교') || artwork.university.includes('University') )) || (artwork.source_url && ( artwork.source_url.includes('.ac.kr') || artwork.source_url.includes('univ.') || artwork.source_url.includes('university') || artwork.source_url.includes('college') || artwork.source_url.includes('graduation') )) || (artwork.source && ( artwork.source.includes('졸업전시') || artwork.source.includes('졸업작품') || artwork.source.includes('대학') || artwork.source.includes('University') || artwork.source.includes('College') )) || (artwork.title && ( artwork.title.includes('졸업작품') || artwork.title.includes('졸업전시') )); const isStudentWork = artwork.student_work === true || artwork.platform === 'academy_art_university' || artwork.platform === 'sva_bfa' || artwork.platform === 'artsonia' || artwork.category === 'professional_student_work'; return !isTumblbug && !isGrafolio && !isKoreanUniversity && !isStudentWork; }) .slice(0, 10) .map((rec, index) => { const artwork = rec.artwork || rec; let imageUrl = artwork.image_url || artwork.thumbnail_url || artwork.primaryImage || 'https://via.placeholder.com/300x300/f0f0f0/666666?text=No+Image'; // Use proxy for external images to avoid CORS issues if (imageUrl && (imageUrl.includes('artsper.com') || imageUrl.includes('metmuseum.org') || imageUrl.includes('wikiart.org'))) { imageUrl = `/api/image-proxy?url=${encodeURIComponent(imageUrl)}`; } const sourceUrl = artwork.source_url || artwork.objectURL || artwork.eventSite || '#'; const title = artwork.title || '제목 없음'; const artist = artwork.artist || artwork.artistDisplayName || '작가 미상'; const source = artwork.search_source || artwork.source || 'Unknown'; return (
handleRecommendationClick(rec)} >
{title} { const target = e.target as HTMLImageElement; if (process.env.NODE_ENV === 'development') { console.log('🖼️ Image load failed:', target.src, 'for artwork:', title); } if (!target.dataset.retried) { target.dataset.retried = 'true'; const fallbackUrl = artwork.thumbnail_url || artwork.primaryImageSmall || 'https://via.placeholder.com/300x300/e5e7eb/6b7280?text=Image+Unavailable'; if (process.env.NODE_ENV === 'development') { console.log('🔄 Trying fallback:', fallbackUrl); } target.src = fallbackUrl; } else { if (process.env.NODE_ENV === 'development') { console.log('❌ Final fallback for:', title); } target.src = 'https://via.placeholder.com/300x300/e5e7eb/6b7280?text=Image+Error'; } }} onLoad={(e) => { const target = e.target as HTMLImageElement; if (process.env.NODE_ENV === 'development') { console.log('✅ Image loaded successfully:', target.src, 'for artwork:', title); } target.style.opacity = '1'; }} style={{ opacity: '0', transition: 'opacity 0.3s ease' }} /> {/* 로딩 상태 표시 */}
{/* 그라디언트 오버레이 */}
{/* 외부 링크 아이콘 */} {sourceUrl !== '#' && (
)}

{title}

{artist}

{Math.round((rec.similarity || rec.similarity_score?.total || 0) * 100)}%
{source}
{/* 유사성 설명 */} {rec.reasons && rec.reasons.length > 0 ? (

{language === 'en' ? 'Similar Features:' : '유사한 특징:'}

{rec.reasons.join(', ')}

) : rec.similarity_score ? (

분석 결과:

{rec.similarity_score.color && (
색상 유사도: {Math.round(rec.similarity_score.color * 100)}%
)} {rec.similarity_score.composition && (
구도 유사도: {Math.round(rec.similarity_score.composition * 100)}%
)} {rec.similarity_score.style && (
스타일 유사도: {Math.round(rec.similarity_score.style * 100)}%
)}
) : (

추천 이유:

{getDefaultSimilarityReason(artwork, rec)}

)} {artwork.price && (

₩{artwork.price.toLocaleString()}

)} {/* 링크 버튼 */} {sourceUrl !== '#' && ( )}
); })}
{/* 학생 작품 섹션 (별도 분리) */} {recommendations.filter((rec) => { const artwork = rec.artwork || rec; return artwork.student_work === true || artwork.platform === 'academy_art_university' || artwork.platform === 'sva_bfa' || artwork.platform === 'artsonia' || artwork.category === 'professional_student_work'; }).length > 0 && (

학생 작품 추천

🎓 교육적 목적
{recommendations .filter((rec) => { const artwork = rec.artwork || rec; return artwork.student_work === true || artwork.platform === 'academy_art_university' || artwork.platform === 'sva_bfa' || artwork.platform === 'artsonia' || artwork.category === 'professional_student_work'; }) .slice(0, 6) .map((rec, index) => { const artwork = rec.artwork || rec; const imageUrl = artwork.image_url || artwork.thumbnail_url || artwork.primaryImage || 'https://via.placeholder.com/300x300/f0f0f0/666666?text=Student+Work'; const sourceUrl = artwork.source_url || artwork.objectURL || artwork.eventSite || '#'; const title = artwork.title || '학생 작품'; const artist = artwork.artist || '학생'; const source = artwork.search_source || artwork.source || 'Student Work'; return (
handleRecommendationClick(rec)} >
{title} { const target = e.target as HTMLImageElement; if (!target.dataset.retried) { target.dataset.retried = 'true'; target.src = artwork.thumbnail_url || artwork.primaryImageSmall || 'https://via.placeholder.com/300x300/e5e7eb/6b7280?text=Student+Work'; } else { target.src = 'https://via.placeholder.com/300x300/e5e7eb/6b7280?text=Student+Art'; } }} onLoad={(e) => { const target = e.target as HTMLImageElement; target.style.opacity = '1'; }} style={{ opacity: '0', transition: 'opacity 0.3s ease' }} /> {/* 로딩 상태 표시 */}
{/* 학생 작품 배지 */}
🎓 학생
{/* 외부 링크 아이콘 */} {sourceUrl !== '#' && (
)}

{title}

{artist}

{language === 'en' ? 'Similarity:' : '유사도:'} {Math.round((rec.similarity || rec.similarity_score?.total || 0) * 100)}%

{source}
{artwork.school && (

{artwork.school} {artwork.academic_level && `- ${artwork.academic_level}`}

)} {rec.reasons && rec.reasons.length > 0 && (

{rec.reasons[0]}

)} {/* 링크 버튼 */} {sourceUrl !== '#' && ( )}
); })}
💡 학생 작품은 교육적 목적으로 표시되며, 작가의 성장과 발전을 지원합니다.
)} {/* 더 보기 버튼 (유료 결제 안내) */} {recommendations.filter((rec) => { const artwork = rec.artwork || rec; const isTumblbug = artwork.platform === 'tumblbug' || artwork.source === '텀블벅' || artwork.search_source === '텀블벅'; const isGrafolio = artwork.platform === 'grafolio' || artwork.source === '그라폴리오' || artwork.search_source === '그라폴리오'; const isKoreanUniversity = artwork.platform === 'university' || artwork.source === '대학 졸업전시' || artwork.category === 'student_work' || artwork.search_source === 'graduation' || (artwork.university && ( artwork.university.includes('대학') || artwork.university.includes('대학교') || artwork.university.includes('University') )) || (artwork.source_url && ( artwork.source_url.includes('.ac.kr') || artwork.source_url.includes('univ.') || artwork.source_url.includes('university') || artwork.source_url.includes('college') || artwork.source_url.includes('graduation') )) || (artwork.source && ( artwork.source.includes('졸업전시') || artwork.source.includes('졸업작품') || artwork.source.includes('대학') || artwork.source.includes('University') || artwork.source.includes('College') )) || (artwork.title && ( artwork.title.includes('졸업작품') || artwork.title.includes('졸업전시') )); const isStudentWork = artwork.student_work === true || artwork.platform === 'academy_art_university' || artwork.platform === 'sva_bfa' || artwork.platform === 'artsonia'; return !isTumblbug && !isGrafolio && !isKoreanUniversity && !isStudentWork; }).length > 10 && (

{t('moreRecommendations')}

{language === 'kr' ? `추가 ${recommendations.length - 10}개의 작품을 확인하려면 프리미엄 플랜이 필요합니다.` : `To see additional ${recommendations.length - 10} artworks, premium plan is required.` }

)}
)}
{/* Auth Modal */} setAuthModalOpen(false)} onAuthSuccess={handleAuthSuccess} onSwitchMode={() => setAuthMode(authMode === 'login' ? 'signup' : 'login')} /> {/* Profile Modal */} setProfileModalOpen(false)} user={user} userProfile={userProfile} onSignOut={handleSignOut} refreshProfile={refreshProfile} /> {/* Mobile Bottom Navigation */}
); }; // AuthProvider와 LanguageProvider로 감싸는 실제 App 컴포넌트 const App: React.FC = () => { return ( ); }; // Safe root element creation with error handling const rootElement = document.getElementById('root'); if (!rootElement) { document.body.innerHTML = '
⚠️ 페이지를 로드할 수 없습니다. (root element not found)
'; } else { const root = createRoot(rootElement); root.render( ); }