Skip to content

Commit 7969d20

Browse files
authored
Merge pull request #106 from part3-4team-Taskify/feature/Login
[Feat, Refactor, Fix] Login: 비로그인 상태에서 페이지 접근 시 /login 이동
2 parents 78b3d9f + da11778 commit 7969d20

File tree

15 files changed

+279
-100
lines changed

15 files changed

+279
-100
lines changed

src/api/apiRoutes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
const TEAM_ID = "13-4";
1+
import { TEAM_ID } from "@/constants/team";
22

33
export const apiRoutes = {
44
//로그인
5-
Login: () => `/${TEAM_ID}/login`, //post
5+
Login: () => `/${TEAM_ID}/auth/login`, //post
66
//비밀번호변경
77
Password: () => `/${TEAM_ID}/auth/password`, //put
88
//카드

src/api/auth.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import axiosInstance from "./axiosInstance";
2+
import { apiRoutes } from "./apiRoutes";
3+
import { UserResponse } from "./users";
4+
5+
interface AuthResponse extends UserResponse {
6+
accessToken: string;
7+
expiresIn: number;
8+
}
9+
10+
export const postAuthData = async ({
11+
email,
12+
password,
13+
}: {
14+
email: string;
15+
password: string;
16+
}) => {
17+
const response = await axiosInstance.post<AuthResponse>(apiRoutes.Login(), {
18+
email,
19+
password,
20+
});
21+
return response.data;
22+
};

src/api/axiosInstance.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// axiosInstance.ts
22

33
import axios from "axios";
4+
import useUserStore from "@/store/useUserStore";
45

56
console.log("🔐 BASE_URL:", process.env.NEXT_PUBLIC_BASE_URL);
67
console.log("🔐 API_TOKEN:", process.env.NEXT_PUBLIC_API_TOKEN);
@@ -9,7 +10,7 @@ const axiosInstance = axios.create({
910
baseURL: process.env.NEXT_PUBLIC_BASE_URL,
1011
});
1112

12-
// 👉 Authorization 헤더 자동 설정, 요청 보낼때 마다 localstorage에서 토큰 가져오기
13+
// 👉 요청 보낼 때마다 토큰 자동 추가
1314
axiosInstance.interceptors.request.use((config) => {
1415
const token = localStorage.getItem("accessToken"); // localStorage에서 토큰 가져오기
1516
if (token) {
@@ -18,11 +19,25 @@ axiosInstance.interceptors.request.use((config) => {
1819
return config;
1920
});
2021

21-
// 👉 요청 보낼 때마다 토큰 자동 추가
22+
// login 토큰 만료 체크 함수
23+
const isTokenExpired = () => {
24+
const expiresAt = localStorage.getItem("expiresAt");
25+
if (!expiresAt) return true;
26+
27+
return new Date().getTime() > parseInt(expiresAt, 10);
28+
};
29+
30+
// 인터셉터로 만료 체크 후 헤더에 추가
2231
axiosInstance.interceptors.request.use((config) => {
23-
const token = localStorage.getItem("accessToken"); // localStorage에서 토큰 가져오기
24-
if (token) {
25-
config.headers.Authorization = `Bearer ${token}`; // 헤더에 Authorization 추가
32+
const token = localStorage.getItem("accessToken");
33+
34+
if (token && !isTokenExpired()) {
35+
config.headers.Authorization = `Bearer ${token}`;
36+
} else {
37+
// 만료되거나 없는 경우
38+
localStorage.removeItem("accessToken");
39+
localStorage.removeItem("expiresAt");
40+
useUserStore.getState().clearUser(); // Zustand 상태 초기화
2641
}
2742
return config;
2843
});

src/api/user.ts renamed to src/api/users.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import axiosInstance from "./axiosInstance";
22

3-
interface UserResponse {
3+
export interface UserResponse {
44
id: number;
55
email: string;
66
nickname: string;

src/components/gnb/HeaderDashboard.tsx

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import SkeletonUser from "@/shared/skeletonUser";
44
import Image from "next/image";
55
import { MemberType, UserType } from "@/types/users";
66
import { getMembers } from "@/api/members";
7-
import { getUserInfo } from "@/api/user";
7+
import { getUserInfo } from "@/api/users";
88
import { getDashboardById } from "@/api/dashboards";
99
import { TEAM_ID } from "@/constants/team";
1010
import { MemberAvatars, UserAvatars } from "@/components/gnb/Avatars";
@@ -74,8 +74,10 @@ const HeaderDashboard: React.FC<HeaderDashboardProps> = ({
7474
setIsLoading(false);
7575
}
7676
};
77-
78-
fetchUser();
77+
const token = localStorage.getItem("accessToken");
78+
if (token) {
79+
fetchUser();
80+
}
7981
}, []);
8082

8183
/*대시보드 api 호출*/
@@ -205,31 +207,34 @@ const HeaderDashboard: React.FC<HeaderDashboardProps> = ({
205207
</div>
206208
)}
207209

208-
{/*드롭다운 메뉴 너비 지정 목적의 유저 정보 섹션 div*/}
210+
{/*드롭다운 메뉴 너비 지정 목적의 유저 섹션 div*/}
209211
<div className="relative flex items-center h-[60px] md:h-[70px] pr-[10px] md:pr-[30px] lg:pr-[80px]">
210212
{/*구분선*/}
211213
<div className="h-[34px] md:h-[38px] w-[1px] bg-[var(--color-gray3)]" />
212-
213-
{/*유저 정보*/}
214-
{isLoading ? (
215-
<SkeletonUser />
216-
) : (
217-
user && (
218-
<div
219-
onClick={() => setIsMenuOpen((prev) => !prev)}
220-
className="flex items-center gap-[12px] pl-[20px] md:pl-[30px] lg:pl-[35px] cursor-pointer"
221-
>
222-
<UserAvatars user={user} />
223-
<span className="hidden md:block text-black3 md:text-base md:font-medium">
224-
{user.nickname}
225-
</span>
226-
<UserMenu
227-
isMenuOpen={isMenuOpen}
228-
setIsMenuOpen={setIsMenuOpen}
229-
/>
230-
</div>
231-
)
232-
)}
214+
{/*유저 드롭다운 메뉴*/}
215+
<div
216+
onClick={() => setIsMenuOpen((prev) => !prev)}
217+
className="flex items-center gap-[12px] pl-[20px] md:pl-[30px] lg:pl-[35px] cursor-pointer"
218+
>
219+
<UserMenu
220+
user={user}
221+
isMenuOpen={isMenuOpen}
222+
setIsMenuOpen={setIsMenuOpen}
223+
/>
224+
{/*유저 프로필*/}
225+
{isLoading ? (
226+
<SkeletonUser />
227+
) : (
228+
user && (
229+
<>
230+
<UserAvatars user={user} />
231+
<span className="hidden md:block text-black3 md:text-base md:font-medium">
232+
{user.nickname}
233+
</span>
234+
</>
235+
)
236+
)}
237+
</div>
233238
</div>
234239
</div>
235240
</div>

src/components/gnb/HeaderDefault.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,20 @@ interface HeaderDefaultProps {
99

1010
const HeaderDefault: React.FC<HeaderDefaultProps> = ({ variant = "white" }) => {
1111
const router = useRouter();
12-
const { user, clearUser } = useUserStore();
12+
const user = useUserStore((state) => state.user);
13+
const { clearUser } = useUserStore();
1314

14-
const isLoggedIn = !user;
15+
const isLoggedIn = !!user;
1516
const isWhite = variant === "white";
1617

1718
const handleAuthClick = () => {
1819
if (isLoggedIn) {
1920
clearUser();
2021
localStorage.removeItem("accessToken");
2122
localStorage.removeItem("expiresAt");
22-
window.location.reload();
23+
router.push("/");
2324
} else {
24-
router.push("login");
25+
router.push("/login");
2526
}
2627
};
2728

src/components/gnb/UserMenu.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,30 @@ import React, { useRef } from "react";
22
import { useRouter } from "next/router";
33
import { useClosePopup } from "@/hooks/useClosePopup";
44
import { User, LogOut, FolderPen } from "lucide-react";
5+
import { UserType } from "@/types/users";
6+
import useUserStore from "@/store/useUserStore";
57

68
interface UserMenuProps {
9+
user: UserType | null;
710
isMenuOpen: boolean;
811
setIsMenuOpen: React.Dispatch<React.SetStateAction<boolean>>;
912
}
1013

1114
const UserMenu: React.FC<UserMenuProps> = ({ isMenuOpen, setIsMenuOpen }) => {
15+
const { clearUser } = useUserStore();
1216
const router = useRouter();
1317
const ref = useRef<HTMLDivElement>(null);
1418

1519
useClosePopup(ref, () => setIsMenuOpen(false));
1620

21+
const handleLogout = () => {
22+
localStorage.setItem("isLoggingOut", "true");
23+
clearUser();
24+
localStorage.removeItem("accessToken");
25+
localStorage.removeItem("expiresAt");
26+
router.push("/");
27+
};
28+
1729
return (
1830
<div
1931
ref={ref}
@@ -37,10 +49,7 @@ const UserMenu: React.FC<UserMenuProps> = ({ isMenuOpen, setIsMenuOpen }) => {
3749
<span className="hidden md:block">내 대시보드</span>
3850
</button>
3951
<button
40-
onClick={() => {
41-
localStorage.removeItem("accessToken");
42-
router.push("/login");
43-
}}
52+
onClick={handleLogout}
4453
className="flex justify-center items-center w-full pt-2 pb-3 font-16r text-black3 hover:bg-[var(--color-gray5)]"
4554
>
4655
<LogOut size={20} className="md:hidden" />

src/components/table/invited/InvitedDashBoard.tsx

Lines changed: 51 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import EmptyInvitations from "./EmptyInvitations";
55
import { apiRoutes } from "@/api/apiRoutes";
66
import axiosInstance from "@/api/axiosInstance";
77
import { Invite } from "@/types/invite";
8+
import useUserStore from "@/store/useUserStore";
89

910
const ITEMS_PER_PAGE = 6; // 한 번에 보여줄 개수
1011

@@ -179,6 +180,7 @@ function InvitedList({
179180
type CursorId = number;
180181

181182
export default function InvitedDashBoard() {
183+
const { user } = useUserStore();
182184
const [searchTitle, setSearchTitle] = useState("");
183185
const [invitationData, setInvitationData] = useState<Map<CursorId, Invite[]>>(
184186
new Map()
@@ -193,63 +195,67 @@ export default function InvitedDashBoard() {
193195
};
194196

195197
useEffect(() => {
196-
fetchNextPage(); // 초기 데이터 6개 불러오기
197-
}, []);
198+
if (user) {
199+
fetchNextPage();
200+
} // 초기 데이터 6개 불러오기
201+
}, [user]);
198202

199203
/* 초대 목록 데이터 불러오기 */
200204
const fetchNextPage = async () => {
201-
try {
202-
const existingCursorId =
203-
cursorId !== null && cursorId !== undefined
204-
? invitationData.get(cursorId)
205-
: undefined;
205+
if (user) {
206+
try {
207+
const existingCursorId =
208+
cursorId !== null && cursorId !== undefined
209+
? invitationData.get(cursorId)
210+
: undefined;
206211

207-
if (existingCursorId && existingCursorId.length > 0) {
208-
// 이미 데이터가 존재하면 더 이상 요청하지 않음
209-
return;
210-
}
212+
if (existingCursorId && existingCursorId.length > 0) {
213+
// 이미 데이터가 존재하면 더 이상 요청하지 않음
214+
return;
215+
}
211216

212-
if (isFetchingRef.current) return; // 이미 데이터가 불러와졌다면 중복 요청 방지
213-
isFetchingRef.current = true; // 데이터 요청 시작
217+
if (isFetchingRef.current) return; // 이미 데이터가 불러와졌다면 중복 요청 방지
218+
isFetchingRef.current = true; // 데이터 요청 시작
214219

215-
const res = await axiosInstance.get(apiRoutes.Invitations(), {
216-
params: {
217-
size: ITEMS_PER_PAGE,
218-
cursorId: cursorId || null,
219-
},
220-
});
220+
const res = await axiosInstance.get(apiRoutes.Invitations(), {
221+
params: {
222+
size: ITEMS_PER_PAGE,
223+
cursorId: cursorId || null,
224+
},
225+
});
221226

222-
if (res.data && Array.isArray(res.data.invitations)) {
223-
const newInvitations = res.data.invitations.map(
224-
(item: {
225-
id: number;
226-
dashboard: { title: string };
227-
inviter: { nickname: string };
228-
}) => ({
229-
id: item.id,
230-
title: item.dashboard.title,
231-
nickname: item.inviter.nickname,
232-
})
233-
) as Invite[];
227+
if (res.data && Array.isArray(res.data.invitations)) {
228+
const newInvitations = res.data.invitations.map(
229+
(item: {
230+
id: number;
231+
dashboard: { title: string };
232+
inviter: { nickname: string };
233+
}) => ({
234+
id: item.id,
235+
title: item.dashboard.title,
236+
nickname: item.inviter.nickname,
237+
})
238+
) as Invite[];
234239

235-
if (newInvitations.length > 0) {
236-
setCursorId(res.data.cursorId);
237-
}
240+
if (newInvitations.length > 0) {
241+
setCursorId(res.data.cursorId);
242+
}
238243

239-
setInvitationData((prev) => {
240-
const newMap = new Map(prev);
241-
newMap.set(cursorId as CursorId, newInvitations);
242-
return newMap;
243-
});
244+
setInvitationData((prev) => {
245+
const newMap = new Map(prev);
246+
newMap.set(cursorId as CursorId, newInvitations);
247+
return newMap;
248+
});
244249

245-
if (newInvitations.length < ITEMS_PER_PAGE) {
246-
setHasMore(false);
250+
if (newInvitations.length < ITEMS_PER_PAGE) {
251+
setHasMore(false);
252+
}
247253
}
254+
} catch (error) {
255+
console.error("초대내역 불러오는데 오류 발생:", error);
256+
} finally {
257+
isFetchingRef.current = false; // 데이터 요청 완료
248258
}
249-
} catch (error) {
250-
console.error("초대내역 불러오는데 오류 발생:", error);
251-
} finally {
252-
isFetchingRef.current = false; // 데이터 요청 완료
253259
}
254260
};
255261

0 commit comments

Comments
 (0)