Skip to content

Commit

Permalink
Merge pull request #93 from softeerbootcamp4th/feature/25-admin-login
Browse files Browse the repository at this point in the history
[feat] 어드민 페이지 로그인 기능 추가, 자동 로그아웃 로직 구현
  • Loading branch information
darkdulgi authored Aug 13, 2024
2 parents 3fd8ca8 + ee027c6 commit 62f7f26
Show file tree
Hide file tree
Showing 16 changed files with 361 additions and 29 deletions.
39 changes: 28 additions & 11 deletions src/adminPage/App.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
import { useEffect } from "react";
import { Route, Routes } from "react-router-dom";
import LoginPage from "./pages/LoginPage.jsx";
import EventsPage from "./pages/EventsPage.jsx";
import ProtectedRoute from "./pages/ProtectedRoute.jsx";
import RootRoute from "./pages/RootRoute.jsx";

import { initLoginState, logout } from "@admin/auth/store.js";
import useLogoutMiddleware from "@common/dataFetch/initLogoutMiddleware";

function App() {
useEffect(() => {
window.scrollTo(0, 0);
history.scrollRestoration = "manual";
initLoginState();
}, []);
useLogoutMiddleware(logout);

return (
<>
<Routes>
<Route
exact
path="/events/create"
element={<div>event 생성 화면</div>}
/>
<Route path="/events/:id" element={<div>event 보는 화면</div>} />
<Route path="/events" element={<div>이벤트 목록 화면</div>} />
<Route path="/comments/:id" element={<div>기대평 화면</div>} />
<Route path="/comments" element={<div>기대평 검색 화면</div>} />
<Route path="/login" element={<div>로그인 화면</div>} />
<Route path="/" element={<div>hello</div>} />
<Route element={<ProtectedRoute />}>
<Route
exact
path="/events/create"
element={<div>event 생성 화면</div>}
/>
<Route path="/events/:id" element={<div>event 보는 화면</div>} />
<Route path="/events" element={<EventsPage />} />
<Route path="/comments/:id" element={<div>기대평 화면</div>} />
<Route path="/comments" element={<div>기대평 검색 화면</div>} />
</Route>
<Route path="/login" element={<LoginPage />} />
<Route path="/" element={<RootRoute />} />
</Routes>
</>
);
Expand Down
3 changes: 2 additions & 1 deletion src/adminPage/mock.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { setupWorker } from "msw/browser";
import authHandler from "@admin/auth/mock.js";

// mocking은 기본적으로 각 feature 폴더 내의 mock.js로 정의합니다.
// 새로운 feature의 mocking을 추가하셨으면, mock.js의 setupWorker 내부 함수에 인자를 spread 연산자를 이용해 추가해주세요.
// 예시 : export default setupWorker(...authHandler, ...questionHandler, ...articleHandler);
export default setupWorker();
export default setupWorker(...authHandler);
10 changes: 10 additions & 0 deletions src/adminPage/pages/EventsPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Container from "@admin/components/Container.jsx";
function EventsPage() {
return (
<Container>
<div className="h-[300px]">이벤트 테스트</div>
</Container>
);
}

export default EventsPage;
11 changes: 11 additions & 0 deletions src/adminPage/pages/LoginPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Container from "@admin/components/Container.jsx";
import LoginSection from "@admin/auth/LoginSection.jsx";
function LoginPage() {
return (
<Container>
<LoginSection />
</Container>
);
}

export default LoginPage;
19 changes: 19 additions & 0 deletions src/adminPage/pages/ProtectedRoute.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect } from "react";
import { useNavigate, Outlet } from "react-router-dom";

import Container from "@admin/components/Container.jsx";
import useUserStore from "@admin/auth/store.js";

function ProtectedRoute() {
const isLogin = useUserStore((store) => store.isLogin);
const navigate = useNavigate();

useEffect(() => {
if (!isLogin) navigate("/login", { replace: true });
}, [isLogin, navigate]);

if (!isLogin) return <Container />;
return <Outlet />;
}

export default ProtectedRoute;
11 changes: 11 additions & 0 deletions src/adminPage/pages/RootRoute.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Navigate } from "react-router-dom";
import useUserStore from "@admin/auth/store.js";

function RootRoute() {
const isLogin = useUserStore((store) => store.isLogin);

if (isLogin) return <Navigate to="/events" replace />;
return <Navigate to="/login" replace />;
}

export default RootRoute;
78 changes: 78 additions & 0 deletions src/adminPage/shared/auth/LoginSection.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";

import { login } from "./store.js";
import { fetchServer, handleError } from "@common/dataFetch/fetchServer.js";
import Input from "@common/components/Input.jsx";
import Button from "@common/components/Button.jsx";

const loginErrorHandler = {
400: "잘못된 입력입니다!",
401: "로그인에 실패했습니다!",
};

function LoginSection() {
const navigate = useNavigate();
const [id, setId] = useState("");
const [password, setPassword] = useState("");
const [errorMessage, setErrorMessage] = useState("");
async function onSubmit(e) {
e.preventDefault();
const config = { method: "post", body: { userName: id, password } };
setErrorMessage("");
fetchServer("/api/v1/admin/auth/signin", config)
.then(({ token }) => {
login(token);
navigate("/events", { replace: true });
})
.catch(handleError(loginErrorHandler))
.catch((e) => {
setId("");
setPassword("");
setErrorMessage(e.message);
});
}
return (
<form
className="w-full h-full max-w-[32rem] max-h-[40rem] p-8 flex flex-col gap-8 bg-white shadow-md rounded-3xl group"
onSubmit={onSubmit}
>
<div className="flex flex-col gap-4">
<label>
<span className="text-detail-l text-neutral-600">아이디</span>
<Input
text={id}
setText={setId}
placeholder="ID를 입력하세요."
required
maxLength="12"
pattern="^[\-\w]+$"
/>
</label>
<label>
<span className="text-detail-l text-neutral-600">비밀번호</span>
<Input
text={password}
setText={setPassword}
type="password"
placeholder="비밀번호 입력하세요."
required
minLength="8"
maxLength="16"
pattern="^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,16}$"
/>
</label>
</div>
<div className="flex flex-col relative items-center">
<Button className="w-full" type="submit">
로그인
</Button>
<span className="absolute -bottom-4 text-detail-l text-red-400">
{errorMessage}
</span>
</div>
</form>
);
}

export default LoginSection;
14 changes: 14 additions & 0 deletions src/adminPage/shared/auth/mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { http, HttpResponse } from "msw";

const handlers = [
http.post("/api/v1/admin/auth/signin", async ({ request }) => {
const { username, password } = await request.json();
if (username !== "admin" && password !== "password1!") {
return HttpResponse.json({ return: false }, { status: 401 });
}

return HttpResponse.json({ token: "test_token" });
}),
];

export default handlers;
26 changes: 26 additions & 0 deletions src/adminPage/shared/auth/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { create } from "zustand";
import tokenSaver from "@common/dataFetch/tokenSaver.js";
import { ADMIN_TOKEN_ID } from "@common/constants.js";

const userStore = create(() => ({
isLogin: false,
}));

export function login(token) {
tokenSaver.set(token);
userStore.setState(() => ({ isLogin: true }));
}

export function logout() {
tokenSaver.remove();
userStore.setState(() => ({ isLogin: false }));
}

export function initLoginState() {
tokenSaver.init(ADMIN_TOKEN_ID);
const token = tokenSaver.get(ADMIN_TOKEN_ID);
if (token === null) userStore.setState(() => ({ isLogin: false }));
else userStore.setState(() => ({ isLogin: true }));
}

export default userStore;
14 changes: 14 additions & 0 deletions src/adminPage/shared/components/Container.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import NavBar from "./NavBar.jsx";

function Container({ children }) {
return (
<div className="w-full min-h-screen flex">
<NavBar />
<div className="w-full h-full min-h-screen flex-grow flex justify-center items-center">
{children}
</div>
</div>
);
}

export default Container;
30 changes: 30 additions & 0 deletions src/adminPage/shared/components/NavBar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useNavigate } from "react-router-dom";

import NavBarItem from "./NavBarItem.jsx";
import useAuthStore, { logout } from "@admin/auth/store.js";

function NavBar() {
const navigate = useNavigate();
const isLogin = useAuthStore((store) => store.isLogin);
function onLogoutClick() {
logout();
navigate("/login");
}

return (
<nav className="w-36 h-screen sticky flex-shrink-0 top-0 bg-black flex flex-col items-center p-4 gap-4">
<p className="text-white py-4 border-b-2 border-white">관리자 페이지</p>
<ul className="w-full flex flex-col justify-center">
<NavBarItem disabled={!isLogin} to="/events">
events
</NavBarItem>
<NavBarItem disabled={!isLogin} to="/comments">
기대평
</NavBarItem>
{isLogin && <NavBarItem onClick={onLogoutClick}>LOGOUT</NavBarItem>}
</ul>
</nav>
);
}

export default NavBar;
38 changes: 38 additions & 0 deletions src/adminPage/shared/components/NavBarItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { NavLink } from "react-router-dom";

function NavBarItem({ to, onClick, disabled, children }) {
const commonStyle = `w-full h-full flex justify-center items-center
hover:border-2 hover:border-white
active:border-2 active:border-neutral-400`;

const commonTextStyle = `text-neutral-200 hover:text-white active:text-neutral-400 invalid:text-neutral-600`;

const navLink = (
<NavLink
to={to}
className={({ isActive }) =>
`${commonStyle} ${disabled ? "pointer-events-none text-neutral-600" : ""} ${isActive ? "text-blue-400" : commonTextStyle}`
}
>
{children}
</NavLink>
);

const button = (
<button
onClick={onClick}
disabled={disabled}
className={`${commonStyle} ${commonTextStyle}`}
>
{children}
</button>
);

return (
<li className="w-full h-12 flex justify-center items-center">
{to ? navLink : button}
</li>
);
}

export default NavBarItem;
Loading

0 comments on commit 62f7f26

Please sign in to comment.