diff --git a/src/app/routes/App.tsx b/src/app/routes/App.tsx index 1091a0a..a886275 100644 --- a/src/app/routes/App.tsx +++ b/src/app/routes/App.tsx @@ -2,6 +2,11 @@ import type { JSX } from "react"; import { lazy } from "react"; import { BrowserRouter, Routes, Route } from "react-router-dom"; +import AuthLayout from "@app/routes/AuthLayout"; +import MainLayout from "@app/routes/MainLayout"; + +import { useAuthObserver } from "@shared/hooks/useAuthObserver"; + const HomePage = lazy(() => import("@pages/home/ui/HomePage")); const NotFoundPage = lazy(() => import("@pages/not-found/ui/NotFoundPage")); const UserProfilePage = lazy( @@ -20,17 +25,26 @@ const LoginPage = lazy(() => import("@pages/login/ui/LoginPage")); const SignUpPage = lazy(() => import("@pages/signup/ui/SignUpPage")); function App(): JSX.Element { + useAuthObserver(); + return ( - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + {/* 헤더 없는 레이아웃 (로그인/회원가입 전용) */} + }> + } /> + } /> + + + {/* 헤더 포함 레이아웃 (메인 페이지) */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + ); diff --git a/src/app/routes/AuthLayout.tsx b/src/app/routes/AuthLayout.tsx new file mode 100644 index 0000000..7cdee93 --- /dev/null +++ b/src/app/routes/AuthLayout.tsx @@ -0,0 +1,12 @@ +import type { JSX } from "react"; +import { Outlet } from "react-router-dom"; + +const AuthLayout = (): JSX.Element => { + return ( +
+ +
+ ); +}; + +export default AuthLayout; diff --git a/src/app/routes/MainLayout.tsx b/src/app/routes/MainLayout.tsx new file mode 100644 index 0000000..f3d122c --- /dev/null +++ b/src/app/routes/MainLayout.tsx @@ -0,0 +1,17 @@ +import type { JSX } from "react"; +import { Outlet } from "react-router-dom"; + +import Header from "@widgets/Header/Header"; + +const MainLayout = (): JSX.Element => { + return ( + <> +
+
+ +
+ + ); +}; + +export default MainLayout; diff --git a/src/features/auth/hooks/useGithubLogin.ts b/src/features/auth/hooks/useGithubLogin.ts deleted file mode 100644 index 1109506..0000000 --- a/src/features/auth/hooks/useGithubLogin.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { signInWithPopup, fetchSignInMethodsForEmail } from "firebase/auth"; -import { useNavigate } from "react-router-dom"; - -import { auth, githubProvider } from "@shared/firebase/firebase"; - -// ✅ 반환 타입 명시 -const useGithubLogin = (): { githubLogin: () => Promise } => { - const navigate = useNavigate(); - - // ✅ 함수 타입 명시 - const githubLogin = async (): Promise => { - try { - const result = await signInWithPopup(auth, githubProvider); - const user = result.user; - - console.log("GitHub 로그인 성공: ", user); - navigate("/"); - } catch (error: any) { - console.error("GitHub 로그인 실패: ", error); - - if (error.code === "auth/account-exists-with-different-credential") { - const email = error.customData?.email; - - if (email) { - const methods = await fetchSignInMethodsForEmail(auth, email); - console.warn("이미 가입된 로그인 방법:", methods); - - if (methods.length > 0) { - if (methods.includes("google.com")) { - alert( - "이미 Google 계정으로 가입된 이메일입니다. Google 로그인을 이용해주세요." - ); - } else { - alert(`이미 가입된 로그인 방법: ${methods.join(", ")}`); - } - } else { - alert( - "이미 다른 로그인 방법으로 가입된 이메일입니다. 다른 로그인 방법을 사용해주세요." - ); - } - } else { - alert( - "이미 다른 로그인 방법으로 가입된 계정입니다. 이메일 정보를 가져올 수 없습니다." - ); - } - } - } - }; - - return { githubLogin }; -}; - -export { useGithubLogin }; diff --git a/src/features/auth/hooks/useGoogleLogin.ts b/src/features/auth/hooks/useGoogleLogin.ts deleted file mode 100644 index 89f3e55..0000000 --- a/src/features/auth/hooks/useGoogleLogin.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { signInWithPopup } from "firebase/auth"; -import { useNavigate } from "react-router-dom"; - -import { auth, googleProvider } from "@shared/firebase/firebase"; - -const useGoogleLogin = (): { googleLogin: () => Promise } => { - const navigate = useNavigate(); - - const googleLogin = async (): Promise => { - try { - const result = await signInWithPopup(auth, googleProvider); - const user = result.user; - - console.log("Google 로그인 성공: ", user); - navigate("/"); - } catch (error) { - console.error("Google 로그인 실패: ", error); - } - }; - - return { googleLogin }; -}; - -export { useGoogleLogin }; diff --git a/src/features/auth/hooks/useSocialLogin.ts b/src/features/auth/hooks/useSocialLogin.ts new file mode 100644 index 0000000..953aecd --- /dev/null +++ b/src/features/auth/hooks/useSocialLogin.ts @@ -0,0 +1,48 @@ +import { signInWithPopup, fetchSignInMethodsForEmail } from "firebase/auth"; +import type { AuthProvider } from "firebase/auth"; +import { useNavigate } from "react-router-dom"; + +import { auth } from "@shared/firebase/firebase"; + +export const useSocialLogin = (): { + socialLogin: (provider: AuthProvider) => Promise; +} => { + const navigate = useNavigate(); + + const socialLogin = async (provider: AuthProvider): Promise => { + try { + const result = await signInWithPopup(auth, provider); + const user = result.user; + + console.log("소셜 로그인 성공: ", user); + navigate("/"); + } catch (error: any) { + console.error("소셜 로그인 실패: ", error); + + if (error.code !== "auth/account-exists-with-different-credential") + return; + + const email = error.customData?.email; + if (!email) { + alert("이메일 정보를 가져올 수 없습니다."); + return; + } + + const methods = await fetchSignInMethodsForEmail(auth, email); + if (methods.length === 0) { + alert("이미 다른 로그인 방법으로 가입된 이메일입니다."); + return; + } + + if (methods.includes("google.com")) { + alert( + "이미 Google 계정으로 가입된 이메일입니다. Google 로그인을 이용해주세요." + ); + } else { + alert(`이미 가입된 로그인 방법: ${methods.join(", ")}`); + } + } + }; + + return { socialLogin }; +}; diff --git a/src/features/auth/ui/LoginButton.tsx b/src/features/auth/ui/LoginButton.tsx new file mode 100644 index 0000000..89ed306 --- /dev/null +++ b/src/features/auth/ui/LoginButton.tsx @@ -0,0 +1,15 @@ +import { Button } from "@mui/material"; +import type { JSX } from "react"; +import { useNavigate } from "react-router-dom"; + +const LoginButton = (): JSX.Element => { + const navigate = useNavigate(); + + return ( + + ); +}; + +export default LoginButton; diff --git a/src/features/auth/ui/LoginForm.tsx b/src/features/auth/ui/LoginForm.tsx index ed8f917..a999df1 100644 --- a/src/features/auth/ui/LoginForm.tsx +++ b/src/features/auth/ui/LoginForm.tsx @@ -1,15 +1,15 @@ import { Divider } from "@mui/material"; import type { JSX } from "react"; -import { useGithubLogin } from "@features/auth/hooks/useGithubLogin"; -import { useGoogleLogin } from "@features/auth/hooks/useGoogleLogin"; +import { useSocialLogin } from "@features/auth/hooks/useSocialLogin"; import { LoginTitle } from "@features/auth/ui/LoginTitle"; import { SignupGuide } from "@features/auth/ui/SignupGuide"; import { SocialLoginButton } from "@features/auth/ui/SocialLoginButton"; +import { githubProvider, googleProvider } from "@shared/firebase/firebase"; + const LoginForm = (): JSX.Element => { - const { googleLogin } = useGoogleLogin(); - const { githubLogin } = useGithubLogin(); + const { socialLogin } = useSocialLogin(); return ( <> @@ -18,12 +18,12 @@ const LoginForm = (): JSX.Element => { socialLogin(googleProvider)} /> socialLogin(githubProvider)} /> 또는 diff --git a/src/features/auth/ui/LogoutButton.tsx b/src/features/auth/ui/LogoutButton.tsx new file mode 100644 index 0000000..8049c16 --- /dev/null +++ b/src/features/auth/ui/LogoutButton.tsx @@ -0,0 +1,23 @@ +import { Button } from "@mui/material"; +import { signOut } from "firebase/auth"; +import type { JSX } from "react"; + +import { auth } from "@shared/firebase/firebase"; +import { useAuthStore } from "@shared/stores/authStore"; + +const LogoutButton = (): JSX.Element => { + const logout = useAuthStore((state) => state.logout); + + const handleLogout = async (): Promise => { + await signOut(auth); + logout(); + }; + + return ( + + ); +}; + +export default LogoutButton; diff --git a/src/features/auth/ui/SocialLoginButton.tsx b/src/features/auth/ui/SocialLoginButton.tsx index e10047a..ba902b4 100644 --- a/src/features/auth/ui/SocialLoginButton.tsx +++ b/src/features/auth/ui/SocialLoginButton.tsx @@ -7,15 +7,6 @@ interface SocialLoginButtonProps { onClick: () => void; } -const SocialButton = styled(Button)({ - width: "100%", - marginBottom: "16px", - textTransform: "none", - display: "flex", - alignItems: "center", - justifyContent: "center", -}); - const SocialLoginButton = ({ label, logo, @@ -34,3 +25,12 @@ const SocialLoginButton = ({ }; export { SocialLoginButton }; + +const SocialButton = styled(Button)({ + width: "100%", + marginBottom: "16px", + textTransform: "none", + display: "flex", + alignItems: "center", + justifyContent: "center", +}); diff --git a/src/pages/login/ui/LoginPage.tsx b/src/pages/login/ui/LoginPage.tsx index 019dc5e..3f8f057 100644 --- a/src/pages/login/ui/LoginPage.tsx +++ b/src/pages/login/ui/LoginPage.tsx @@ -3,25 +3,6 @@ import type { JSX } from "react"; import { LoginForm } from "@features/auth/ui/LoginForm"; -const PageWrapper = styled(Box)({ - display: "flex", - flexDirection: "column", - justifyContent: "center", - alignItems: "center", - minHeight: "100vh", - backgroundColor: "#f5f7fb", -}); - -const LoginBox = styled(Box)({ - width: "100%", - maxWidth: "400px", - padding: "32px", - borderRadius: "12px", - backgroundColor: "#fff", - boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)", - textAlign: "center", -}); - const LoginPage = (): JSX.Element => { console.log("API_KEY: ", import.meta.env.VITE_API_KEY); return ( @@ -41,3 +22,22 @@ const LoginPage = (): JSX.Element => { }; export default LoginPage; + +const PageWrapper = styled(Box)({ + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + minHeight: "100vh", + backgroundColor: "#f5f7fb", +}); + +const LoginBox = styled(Box)({ + width: "100%", + maxWidth: "400px", + padding: "32px", + borderRadius: "12px", + backgroundColor: "#fff", + boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)", + textAlign: "center", +}); diff --git a/src/shared/hooks/useAuthObserver.ts b/src/shared/hooks/useAuthObserver.ts new file mode 100644 index 0000000..494a15a --- /dev/null +++ b/src/shared/hooks/useAuthObserver.ts @@ -0,0 +1,19 @@ +import { onAuthStateChanged } from "firebase/auth"; +import { useEffect } from "react"; + +import { auth } from "@shared/firebase/firebase"; +import { useAuthStore } from "@shared/stores/authStore"; + +export const useAuthObserver = (): void => { + const setUser = useAuthStore((state) => state.setUser); + const setLoading = useAuthStore((state) => state.setLoading); + + useEffect(() => { + const unsubscribe = onAuthStateChanged(auth, (user) => { + setUser(user); + setLoading(false); + }); + + return () => unsubscribe(); + }, [setUser, setLoading]); +}; diff --git a/src/shared/stores/authStore.ts b/src/shared/stores/authStore.ts new file mode 100644 index 0000000..a67bd00 --- /dev/null +++ b/src/shared/stores/authStore.ts @@ -0,0 +1,20 @@ +import type { User } from "firebase/auth"; +import { create } from "zustand"; + +interface AuthState { + user: User | null; + isLoading: boolean; + setUser: (user: User | null) => void; + setLoading: (loading: boolean) => void; + logout: () => void; +} + +export const useAuthStore = create((set) => ({ + user: null, + isLoading: true, + + setUser: (user) => set({ user }), + setLoading: (loading) => set({ isLoading: loading }), + + logout: () => set({ user: null }), +})); diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx new file mode 100644 index 0000000..6b3576f --- /dev/null +++ b/src/widgets/Header/Header.tsx @@ -0,0 +1,28 @@ +import { Box, styled } from "@mui/material"; +import type { JSX } from "react"; + +import LoginButton from "@features/auth/ui/LoginButton"; +import LogoutButton from "@features/auth/ui/LogoutButton"; + +import { useAuthStore } from "@shared/stores/authStore"; + +const Header = (): JSX.Element => { + const user = useAuthStore((state) => state.user); + + return ( + +

🔥 Project Jam

+ {user ? : } +
+ ); +}; + +export default Header; + +const HeaderContainer = styled(Box)({ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + padding: "0 2rem", + backgroundColor: "#f5f5f5", +});