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",
+});