diff --git a/app/src/App.tsx b/app/src/App.tsx index 4a1ca92..6ed4c95 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,10 +1,13 @@ import React, { useEffect, useState } from 'react'; import { initDB, useIndexedDB } from "react-indexed-db-hook"; +import { onAuthStateChanged, User } from 'firebase/auth'; import { DBConfig } from './DBConfig'; +import { auth } from './firebase'; import { chatCompletions } from './api/ncloud-api'; import { getCurrentDate } from './modules/utils'; import FlashCardViewer from './pages/FlashCardViewer'; +import Login from './pages/Login'; import { getGithubData } from './services/github-service'; initDB(DBConfig); @@ -13,8 +16,26 @@ const dates = [1, 7, 30]; // days ago list const App: React.FC = () => { const { add, getByID } = useIndexedDB("data"); const [loading, setLoading] = useState(true); + const [user, setUser] = useState(null); + const [authLoading, setAuthLoading] = useState(true); + // 인증 상태 감지 useEffect(() => { + const unsubscribe = onAuthStateChanged(auth, (user) => { + setUser(user); + setAuthLoading(false); + }); + + return () => unsubscribe(); + }, []); + + // 사용자가 로그인한 경우에만 데이터 로드 + useEffect(() => { + if (!user) { + setLoading(false); + return; + } + (async () => { const todayData = await getByID(getCurrentDate()); if (todayData) { @@ -43,9 +64,44 @@ const App: React.FC = () => { } setLoading(false); })(); - }, [add, getByID]); - - if(loading) return null; + }, [add, getByID, user]); + + // 인증 로딩 중 + if (authLoading) { + return ( +
+ 로딩 중... +
+ ); + } + + // 로그인되지 않은 경우 + if (!user) { + return ; + } + + // 데이터 로딩 중 + if (loading) { + return ( +
+ 데이터를 불러오는 중... +
+ ); + } + + // 메인 앱 렌더링 return (
diff --git a/app/src/firebase.ts b/app/src/firebase.ts new file mode 100644 index 0000000..3cd4e51 --- /dev/null +++ b/app/src/firebase.ts @@ -0,0 +1,25 @@ +import { initializeApp } from 'firebase/app'; +import { getAuth, GithubAuthProvider } from 'firebase/auth'; + +const firebaseConfig = { + apiKey: import.meta.env.VITE_API_KEY, + authDomain: import.meta.env.VITE_AUTH_DOMAIN, + projectId: import.meta.env.VITE_PROJECT_ID, + storageBucket: import.meta.env.VITE_STORAGE_BUCKET, + messagingSenderId: import.meta.env.VITE_MESSAGING_SENDER_ID, + appId: import.meta.env.VITE_APP_ID, + measurementId: import.meta.env.VITE_MEASUREMENT_ID, +}; + +// Firebase 앱 초기화 +export const app = initializeApp(firebaseConfig); + +// Auth 인스턴스 생성 +export const auth = getAuth(app); + +// GitHub 프로바이더 생성 +export const githubProvider = new GithubAuthProvider(); + +// 스코프 설정 (필요한 GitHub 권한) +githubProvider.addScope('user:email'); +githubProvider.addScope('read:user'); diff --git a/app/src/main.tsx b/app/src/main.tsx index 74252ec..6521ded 100644 --- a/app/src/main.tsx +++ b/app/src/main.tsx @@ -3,20 +3,9 @@ import ReactDOM from 'react-dom/client'; import App from './App'; import './index.css'; import { getMessaging, getToken } from "firebase/messaging"; -import { initializeApp } from "firebase/app"; +import { app } from './firebase'; import { registerDeviceToken, registerSchedule, removeDeviceToken } from "./api/ncloud-api"; -const firebaseConfig = { - apiKey: import.meta.env.VITE_API_KEY, - authDomain: import.meta.env.VITE_AUTH_DOMAIN, - projectId: import.meta.env.VITE_PROJECT_ID, - storageBucket: import.meta.env.VITE_STORAGE_BUCKET, - messagingSenderId: import.meta.env.VITE_MESSAGING_SENDER_ID, - appId: import.meta.env.VITE_APP_ID, - measurementId: import.meta.env.VITE_MEASUREMENT_ID, -}; - -const app = initializeApp(firebaseConfig); const messaging = getMessaging(app); async function setupAlarm(): Promise { diff --git a/app/src/pages/Auth.tsx b/app/src/pages/Auth.tsx deleted file mode 100644 index 8401d9e..0000000 --- a/app/src/pages/Auth.tsx +++ /dev/null @@ -1,13 +0,0 @@ -// Auth 페이지 - 필요에 따라 구현 -import React from 'react'; - -const Auth: React.FC = () => { - return ( -
-

Authentication

- {/* 인증 관련 UI 구현 */} -
- ); -}; - -export default Auth; diff --git a/app/src/pages/Login.css b/app/src/pages/Login.css new file mode 100644 index 0000000..25ddc71 --- /dev/null +++ b/app/src/pages/Login.css @@ -0,0 +1,178 @@ +.login-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 20px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; +} + +.login-card { + background: white; + border-radius: 16px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + padding: 40px; + max-width: 400px; + width: 100%; + text-align: center; + animation: slideUp 0.6s ease-out; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.login-header h1 { + color: #333; + margin-bottom: 8px; + font-size: 2rem; + font-weight: 700; +} + +.login-header p { + color: #666; + margin-bottom: 32px; + font-size: 1rem; + line-height: 1.5; +} + +.github-login-button { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + width: 100%; + padding: 16px 24px; + background: #24292e; + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + margin-bottom: 24px; +} + +.github-login-button:hover:not(:disabled) { + background: #1a1e22; + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(36, 41, 46, 0.3); +} + +.github-login-button:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; +} + +.github-icon { + width: 20px; + height: 20px; +} + +.loading-spinner { + width: 40px; + height: 40px; + border: 4px solid #f3f3f3; + border-top: 4px solid #667eea; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 16px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.user-info { + margin-bottom: 32px; +} + +.profile-image { + width: 80px; + height: 80px; + border-radius: 50%; + margin-bottom: 16px; + border: 4px solid #f0f0f0; +} + +.user-info h2 { + color: #333; + margin-bottom: 8px; + font-size: 1.5rem; + font-weight: 600; +} + +.user-email { + color: #666; + font-size: 0.9rem; + margin: 0; +} + +.logout-button { + width: 100%; + padding: 12px 24px; + background: #dc3545; + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.logout-button:hover { + background: #c82333; + transform: translateY(-1px); + box-shadow: 0 4px 15px rgba(220, 53, 69, 0.3); +} + +.error-message { + background: #f8d7da; + color: #721c24; + padding: 12px 16px; + border-radius: 8px; + margin-bottom: 24px; + font-size: 0.9rem; + border: 1px solid #f5c6cb; +} + +.login-footer p { + color: #999; + font-size: 0.8rem; + margin: 0; + line-height: 1.4; +} + +/* 반응형 디자인 */ +@media (max-width: 480px) { + .login-container { + padding: 16px; + } + + .login-card { + padding: 24px; + } + + .login-header h1 { + font-size: 1.5rem; + } + + .github-login-button { + padding: 14px 20px; + font-size: 0.9rem; + } +} diff --git a/app/src/pages/Login.tsx b/app/src/pages/Login.tsx new file mode 100644 index 0000000..873a260 --- /dev/null +++ b/app/src/pages/Login.tsx @@ -0,0 +1,118 @@ +import React, { useState } from 'react'; +import { signInWithPopup, signOut, onAuthStateChanged, User } from 'firebase/auth'; +import { auth, githubProvider } from '../firebase'; +import './Login.css'; + +const Login: React.FC = () => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + // 인증 상태 변경 감지 + React.useEffect(() => { + const unsubscribe = onAuthStateChanged(auth, (user) => { + setUser(user); + setLoading(false); + }); + + return () => unsubscribe(); + }, []); + + // GitHub 로그인 함수 + const handleGitHubLogin = async () => { + try { + setLoading(true); + setError(''); + const result = await signInWithPopup(auth, githubProvider); + console.log('로그인 성공:', result.user); + } catch (error: any) { + console.error('로그인 실패:', error); + setError('로그인에 실패했습니다. 다시 시도해주세요.'); + } finally { + setLoading(false); + } + }; + + // 로그아웃 함수 + const handleLogout = async () => { + try { + await signOut(auth); + console.log('로그아웃 성공'); + } catch (error) { + console.error('로그아웃 실패:', error); + } + }; + + if (loading) { + return ( +
+
+
+

로그인 중...

+
+
+ ); + } + + if (user) { + return ( +
+
+
+ 프로필 +

안녕하세요, {user.displayName || user.email}님!

+

{user.email}

+
+ +
+
+ ); + } + + return ( +
+
+
+

Today I Learned

+

GitHub 계정으로 로그인하여 학습을 시작하세요

+
+ + {error && ( +
+ {error} +
+ )} + + + +
+

로그인하면 GitHub의 공개 정보에 접근할 수 있습니다

+
+
+
+ ); +}; + +export default Login; diff --git a/app/vite.config.ts b/app/vite.config.ts index 0a6a079..08d6cbb 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -30,6 +30,7 @@ export default defineConfig({ }) ], server: { + open: true, proxy: { '/question-generator': { target: 'https://clovastudio.apigw.ntruss.com',