Skip to content

Commit ab42a2a

Browse files
feat(admin): aside-navigation-menu 추가 (#94)
* chore(admin): init lucide icons * feat(admin): pretendard 폰트 적용 * feat(admin): aside-navigation-menu 추가 * refactor: bg 색상 변경 * refactor: height 조정 * refactor(admin): use-auth에서 유틸함수로 분리 * feat: add mainpage * refactor: use-auth에서 refreshTokens 분리 * feat: 로그인 후 메인페이지로 리다이랙션 * feat: 로그인 연장 및 로그아웃 기능 * chore: change file name --------- Co-authored-by: NoaH <85290394+noahyunjun@users.noreply.github.com>
1 parent 5f5184e commit ab42a2a

16 files changed

+307
-94
lines changed

apps/admin/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@tanstack/react-router": "^1.104.1",
2929
"antd": "^5.24.1",
3030
"ky": "^1.7.5",
31+
"lucide-react": "^0.462.0",
3132
"react": "catalog:react19",
3233
"react-dom": "catalog:react19",
3334
"tailwindcss": "^4.0.6"

apps/admin/src/assets/.gitkeep

Whitespace-only changes.

apps/admin/src/assets/logo.svg

+1
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { Link } from '@tanstack/react-router';
2+
import { Menu, type MenuProps } from 'antd';
3+
import {
4+
Clipboard,
5+
Clock,
6+
FlaskConical,
7+
GraduationCap,
8+
Speech,
9+
Users,
10+
} from 'lucide-react';
11+
12+
import LOGO from '~/assets/logo.svg';
13+
import { useRefreshTokens } from '~/hooks/use-refresh-token';
14+
import { useTokenExpiration } from '~/hooks/use-token-expiration';
15+
import { authServices } from '~/utils/auth';
16+
import { formatExpireTime } from '~/utils/utils';
17+
18+
type MenuItem = Required<MenuProps>['items'][number];
19+
20+
const items: MenuItem[] = [
21+
//TODO: Link 내부 url 변경
22+
{
23+
key: 'user',
24+
label: '회원 관리',
25+
icon: <Users size={20} />,
26+
},
27+
{
28+
key: 'about',
29+
label: '소개',
30+
icon: <GraduationCap size={20} />,
31+
children: [
32+
{ key: 'dept', label: <Link to="/">학부 소개</Link> },
33+
{ key: 'club', label: <Link to="/">동아리 소개</Link> },
34+
{ key: 'contact', label: <Link to="/">찾아오시는 길</Link> },
35+
],
36+
},
37+
{
38+
key: 'professor',
39+
label: <Link to="/">교수진 소개</Link>,
40+
icon: <Speech size={20} />,
41+
},
42+
{
43+
key: 'lab',
44+
label: <Link to="/">연구실 소개</Link>,
45+
icon: <FlaskConical size={20} />,
46+
},
47+
{
48+
key: 'board',
49+
label: '게시판',
50+
icon: <Clipboard size={20} />,
51+
children: [
52+
{ key: 'notice', label: <Link to="/">공지사항</Link> },
53+
{ key: 'news', label: <Link to="/">학부 소식</Link> },
54+
],
55+
},
56+
];
57+
58+
function AsideHeader() {
59+
return (
60+
<div className="flex items-center justify-center gap-2 font-bold border-r border-gray-200 h-22">
61+
<img src={LOGO} alt="logo" />
62+
<div className="leading-4.5">
63+
<p>AI컴퓨터공학부</p>
64+
<p>관리자 시스템</p>
65+
</div>
66+
</div>
67+
);
68+
}
69+
70+
function AsideFooter() {
71+
const { expireTime } = useTokenExpiration();
72+
const refreshMutation = useRefreshTokens();
73+
const { logout } = authServices();
74+
75+
const handleRefreshToken = () => {
76+
refreshMutation.mutate();
77+
};
78+
79+
return (
80+
<div className="flex items-center text-sm p-2 border-r border-gray-200 justify-between">
81+
<div className="flex gap-1.5 items-center">
82+
<Clock size={16} />
83+
<span>{formatExpireTime(expireTime)}</span>
84+
</div>
85+
<div>
86+
<button
87+
type="button"
88+
className="p-2 transition-colors duration-150 rounded-md cursor-pointer hover:bg-gray-300"
89+
onClick={handleRefreshToken}
90+
>
91+
시간연장
92+
</button>
93+
<button
94+
type="button"
95+
className="p-2 transition-colors duration-150 rounded-md cursor-pointer hover:bg-gray-300"
96+
onClick={logout}
97+
>
98+
로그아웃
99+
</button>
100+
</div>
101+
</div>
102+
);
103+
}
104+
105+
export default function AsideNavigationMenu() {
106+
return (
107+
<aside className="flex flex-col h-full select-none bg-slate-100 w-80">
108+
<AsideHeader />
109+
<Menu mode="inline" items={items} className="flex-grow" />
110+
<AsideFooter />
111+
</aside>
112+
);
113+
}

apps/admin/src/hooks/use-auth.ts

-67
This file was deleted.
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useMutation } from '@tanstack/react-query';
2+
import { END_POINT } from '~/constants/api';
3+
import { getToken } from '~/utils/api';
4+
import { authServices } from '~/utils/auth';
5+
import { authHttp } from '~/utils/http';
6+
import type { Tokens } from './use-sign-in';
7+
8+
interface RefreshToken {
9+
refreshToken: string;
10+
}
11+
12+
const useRefreshTokens = () => {
13+
const { setTokens, logout } = authServices();
14+
15+
const refreshMutation = useMutation({
16+
mutationFn: () => {
17+
const [accessToken, refreshToken] = getToken();
18+
19+
if (!accessToken || !refreshToken) {
20+
throw new Error('토큰이 존재하지 않습니다');
21+
}
22+
23+
const response = authHttp.post<RefreshToken>(END_POINT.REISSUE, {
24+
json: { refreshToken },
25+
});
26+
return response.json<Tokens>();
27+
},
28+
onSuccess: (newTokens) => {
29+
setTokens(newTokens);
30+
},
31+
onError: () => {
32+
alert('오류가 발생하여 로그아웃합니다.');
33+
logout();
34+
},
35+
});
36+
37+
return refreshMutation;
38+
};
39+
40+
export { type RefreshToken, useRefreshTokens };

apps/admin/src/hooks/use-sign-in.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useMutation } from '@tanstack/react-query';
2+
import { useNavigate } from '@tanstack/react-router';
23
import { END_POINT } from '~/constants/api';
3-
import { useAuth } from '~/hooks/use-auth';
4+
import { authServices } from '~/utils/auth';
45
import { authHttp } from '~/utils/http';
56

67
interface SignInData {
@@ -14,15 +15,16 @@ interface Tokens {
1415
}
1516

1617
const useSignIn = () => {
17-
const { setTokens } = useAuth();
18+
const { setTokens } = authServices();
19+
const navigate = useNavigate();
1820

1921
return useMutation({
2022
mutationFn: (data: SignInData) => {
2123
return authHttp.post(END_POINT.SIGN_IN, { json: data }).json<Tokens>();
2224
},
2325
onSuccess: (token) => {
24-
console.log('success : ', token);
2526
setTokens(token);
27+
navigate({ to: '/main' });
2628
},
2729
});
2830
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { useEffect, useState } from 'react';
2+
import { getAccessToken } from '~/utils/api';
3+
import { authServices } from '~/utils/auth';
4+
import { decodeJwt } from '~/utils/jwt';
5+
6+
export const useTokenExpiration = () => {
7+
const { logout } = authServices();
8+
const [expireTime, setExpireTime] = useState<number | null>(null);
9+
const accessToken = getAccessToken();
10+
11+
useEffect(() => {
12+
if (accessToken) {
13+
const decoded = decodeJwt(accessToken);
14+
if (decoded?.exp) {
15+
const expiresIn = decoded.exp * 1000 - Date.now();
16+
setExpireTime(expiresIn);
17+
}
18+
}
19+
}, [accessToken]);
20+
21+
useEffect(() => {
22+
if (expireTime !== null && expireTime <= 0) {
23+
logout();
24+
} else if (expireTime !== null && expireTime > 0) {
25+
const timerId = setTimeout(() => {
26+
setExpireTime(expireTime - 1000);
27+
}, 1000);
28+
return () => clearTimeout(timerId);
29+
}
30+
}, [expireTime, logout]);
31+
32+
return { expireTime };
33+
};

apps/admin/src/routes/__root.tsx

+24-6
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,35 @@
11
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
2-
import { Outlet, createRootRoute } from '@tanstack/react-router';
2+
import { Outlet, createRootRoute, useLocation } from '@tanstack/react-router';
33
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
4+
import { ConfigProvider } from 'antd';
5+
import AsideNavigationMenu from '~/components/aside-navigation-menu';
46

57
export const Route = createRootRoute({
68
component: App,
79
});
810

911
function App() {
12+
const location = useLocation();
13+
const isSigninPage = location.pathname === '/';
14+
1015
return (
11-
<>
12-
<Outlet />
13-
<TanStackRouterDevtools position="top-right" />
14-
<ReactQueryDevtools initialIsOpen={false} />
15-
</>
16+
<ConfigProvider
17+
theme={{
18+
components: {
19+
Menu: {
20+
itemBg: '#f1f5f9',
21+
},
22+
},
23+
}}
24+
>
25+
<main className="relative flex h-dvh">
26+
{!isSigninPage && <AsideNavigationMenu />}
27+
<div className="flex flex-col items-center justify-center w-full h-full">
28+
<Outlet />
29+
</div>
30+
<TanStackRouterDevtools position="top-right" />
31+
<ReactQueryDevtools initialIsOpen={false} />
32+
</main>
33+
</ConfigProvider>
1634
);
1735
}

apps/admin/src/routes/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ function SignInPage() {
3232
};
3333

3434
return (
35-
<div className="flex justify-center items-center h-screen bg-gray-100">
36-
<Card className="w-96 shadow-lg">
35+
<div className="flex items-center justify-center w-full h-full bg-gray-100">
36+
<Card className="shadow-lg w-96">
3737
<Typography.Title level={3} className="text-center">
3838
로그인
3939
</Typography.Title>

apps/admin/src/routes/main/index.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createFileRoute } from '@tanstack/react-router';
2+
3+
export const Route = createFileRoute('/main/')({
4+
component: MainPage,
5+
});
6+
7+
function MainPage() {
8+
return <div>main</div>;
9+
}

apps/admin/src/styles/globals.css

+15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
1+
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.8/dist/web/variable/pretendardvariable-dynamic-subset.css");
12
@import "tailwindcss";
23

4+
:root {
5+
font-family:
6+
"Pretendard Variable", Pretendard, -apple-system,
7+
BlinkMacSystemFont, system-ui, Roboto, "Helvetica Neue", "Segoe UI",
8+
"Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji",
9+
"Segoe UI Emoji", "Segoe UI Symbol", sans-serif;
10+
font-synthesis: none;
11+
text-rendering: optimizeLegibility;
12+
-webkit-font-smoothing: antialiased;
13+
-moz-osx-font-smoothing: grayscale;
14+
-webkit-text-size-adjust: 100%;
15+
font-size: 16px;
16+
}
17+
318
.ProseMirror ul ul,
419
.ProseMirror ol ol,
520
.ProseMirror ul ol,

apps/admin/src/utils/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)