Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bb5777f
chore: zustand 라이브러리 추가
kwonsaebom Sep 12, 2025
467589a
feat: 유저 & 함수 타입 지정
kwonsaebom Sep 12, 2025
3969e9f
feat: zuestand 기반 useAuthStore 구현
kwonsaebom Sep 12, 2025
5a640d9
chore: 함수 이름 통일
kwonsaebom Sep 12, 2025
02c7186
refactor: 중복된 타입 파일 제거
kwonsaebom Sep 12, 2025
d9af138
feat: useAuthStore 훅 적용
kwonsaebom Sep 12, 2025
45593c1
fix: 에러 수정
kwonsaebom Sep 12, 2025
4c11908
feat: userModal 컴포넌트에 user 상태 적용
kwonsaebom Sep 12, 2025
769697e
feat: 상태 관리에 isLoggedIn 추가
kwonsaebom Sep 15, 2025
1575b23
feat: 테스트 코드
kwonsaebom Sep 15, 2025
5e1ec24
refactor: user의 기본 값 변경
kwonsaebom Sep 15, 2025
14d8eb4
feat: 분기 처리 조건 수정
kwonsaebom Sep 15, 2025
1e48f00
refactor: 유저 데이터가 존재할 때만 로컬 스토리지에 저장하도록 수정
kwonsaebom Sep 15, 2025
bda0d57
fix: 린트 에러 해결
kwonsaebom Sep 15, 2025
94f1d18
refactor: 객체 형식으로 상태 useAuthStore 호출
kwonsaebom Sep 17, 2025
839e240
refactor: button으로 img 감싸서 접근성 높이기
kwonsaebom Sep 17, 2025
27f87b6
refactor: 토큰을 기준으로 로그인 기준 처리
kwonsaebom Sep 17, 2025
1def14c
fix: 주석 제거
kwonsaebom Sep 17, 2025
8a711b3
feat: 목표 작성 상태에 따른 분기 처리
kwonsaebom Sep 17, 2025
193ddb4
feat: 리프레시 토큰 발급 추가 & axiosInstance 응답 변경
kwonsaebom Sep 17, 2025
44b39f3
fix: 충돌 해결
kwonsaebom Sep 17, 2025
2e641c6
Merge branch 'develop' into feat/#187/loginState
kwonsaebom Sep 19, 2025
38d38a8
fix: 리뷰 수정
kwonsaebom Sep 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.6.3"
"react-router-dom": "^7.6.3",
"zustand": "^5.0.8"
},
"devDependencies": {
"@chromatic-com/storybook": "^4.0.1",
Expand Down
28 changes: 28 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions src/api/auth/refreshToken/index.ts
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3) 여기서 에러가 터지면 상위 인터셉터에서만 확인해야 해서 디버깅이 어려울 수 있다고 생각합니다 ! try/catch로 래핑해서 에러 메시지를 명확히 하거나 그대로 throw 하되, 로그를 남기면 좋을 것 같습니다 !!

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import axiosInstance from '@/api/axiosInstance';
import { END_POINT } from '@/api/constant/endPoint';
import type { BaseResponse } from '@/type/api';

export type RefreshResponse = {
accessToken: string;
message: string;
};

export const postRefreshToken = async () => {
const { data } = await axiosInstance.post<BaseResponse<RefreshResponse>>(
`/${END_POINT.AUTH}/refresh`,
);
return data.data;
};
16 changes: 14 additions & 2 deletions src/api/axiosInstance.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import axios from 'axios';

import { HTTP_STATUS } from '@/api/constant/httpStatus';
import { postRefreshToken } from '@/api/auth/refreshToken';

const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
Expand All @@ -24,12 +25,23 @@ axiosInstance.interceptors.request.use(
// 응답 인터셉터
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
async (error) => {
const originalRequest = error.config;

if (error.response) {
const { status } = error.response;

if (status === HTTP_STATUS.UNAUTHORIZED) {
// 인증 실패 처리
try {
const { accessToken } = await postRefreshToken();
localStorage.setItem('accessToken', accessToken);

axiosInstance.defaults.headers.Authorization = `Bearer ${accessToken}`;
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
} catch (refreshError) {
console.error('리프레시 토큰 만료, 재로그인 필요');
return Promise.reject(refreshError);
}
}

if (status === HTTP_STATUS.INTERNAL_SERVER_ERROR) {
Expand Down
4 changes: 2 additions & 2 deletions src/api/domain/signup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { END_POINT } from '@/api/constant/endPoint';
import type { JobResponse } from '@/api/domain/signup/type/JobResponse';
import type { PersonaResponse } from '@/api/domain/signup/type/PersonaResponse';
import type { SignupResponse } from '@/api/domain/signup/type/SignupResponse';
import type { UserInfoResponse } from '@/api/domain/signup/type/UserInfoResponse';
import type { UserType } from '@/store/types/authTypes';
import type { BaseResponse } from '@/type/api';

export const getJobList = async () => {
Expand All @@ -21,7 +21,7 @@ export const postSignUp = async (payload: SignupResponse) => {
};

export const getUser = async () => {
const { data } = await axiosInstance.get<BaseResponse<UserInfoResponse>>('/users/info');
const { data } = await axiosInstance.get<BaseResponse<UserType>>('/users/info');
return data.data;
};

Expand Down
6 changes: 0 additions & 6 deletions src/api/domain/signup/type/UserInfoResponse.ts

This file was deleted.

16 changes: 11 additions & 5 deletions src/common/component/UserModal/UserModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@

import { IcDivider } from '@/assets/svg';
import * as styles from '@/common/component/UserModal/UserModal.css';
import { useGetUser } from '@/api/domain/signup/hook/useGetUser';
import { usePostLogout } from '@/api/domain/signup/hook/usePostLogout';
import { useAuthStore } from '@/store/useAuthStore';

Check warning on line 6 in src/common/component/UserModal/UserModal.tsx

View workflow job for this annotation

GitHub Actions / build

There should be at least one empty line between import groups

Check warning on line 6 in src/common/component/UserModal/UserModal.tsx

View workflow job for this annotation

GitHub Actions / build

There should be at least one empty line between import groups
import { useNavigate } from 'react-router-dom';

Check warning on line 7 in src/common/component/UserModal/UserModal.tsx

View workflow job for this annotation

GitHub Actions / build

`react-router-dom` import should occur before import of `@/assets/svg`

Check warning on line 7 in src/common/component/UserModal/UserModal.tsx

View workflow job for this annotation

GitHub Actions / build

There should be at least one empty line between import groups

Check warning on line 7 in src/common/component/UserModal/UserModal.tsx

View workflow job for this annotation

GitHub Actions / build

`react-router-dom` import should occur before import of `@/assets/svg`

Check warning on line 7 in src/common/component/UserModal/UserModal.tsx

View workflow job for this annotation

GitHub Actions / build

There should be at least one empty line between import groups
import { PATH } from '@/route';

interface UserModalProps {
onClose: () => void;
}

const UserModal = ({ onClose }: UserModalProps) => {
const modalRef = useRef<HTMLDivElement>(null);
const { data: user, isLoading, isError } = useGetUser();
const navigate = useNavigate();

const user = useAuthStore((state) => state.user);
const resetUser = useAuthStore((state) => state.resetUser);

const { mutate: logoutMutate } = usePostLogout();

const handleClickOutside = useCallback(
Expand All @@ -26,9 +32,9 @@
const handleLogout = () => {
logoutMutate(undefined, {
onSuccess: () => {
localStorage.removeItem('accessToken');
resetUser();
onClose();
window.location.reload();
navigate(PATH.ROOT);
},
onError: (error) => {
console.error('로그아웃 실패:', error);
Expand All @@ -44,7 +50,7 @@
};
}, [handleClickOutside]);

if (isLoading || isError || !user) {
if (!user) {
return null;
}

Expand Down
8 changes: 2 additions & 6 deletions src/common/hook/useGoogleAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,10 @@ import getGoogleAuthCode from '@/api/auth/googleLogin/util/getGoogleAuthCode';
import getAccessToken from '@/api/auth/googleLogin/util/getAccessToken';

interface UserData {
email: string;
name: string;
profileImageUrl: string;
socialProvider: string;
socialToken: string;
exists: boolean;
userId?: number;
accessToken?: string;
onboardingCompleted?: boolean;
onboardingPage: string;
}

export const useGoogleAuth = (): UserData | null => {
Expand Down
15 changes: 9 additions & 6 deletions src/page/callback/GoogleCallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@ const GoogleCallback = () => {
return;
}

const isExistingUser = userData.exists;
const { exists, onboardingPage } = userData;

if (isExistingUser) {
navigate(PATH.INTRO, {
state: { isWritten: userData.onboardingCompleted },
});
} else {
if (!exists) {
navigate(PATH.SIGNUP, {
state: { userData },
});
return;
}

if (onboardingPage === 'ONBOARDING_COMPLETED') {
navigate(PATH.MANDAL);
} else {
navigate(PATH.INTRO, { state: { pageState: onboardingPage } });
}
}, [userData, navigate]);

Expand Down
27 changes: 21 additions & 6 deletions src/page/intro/Intro.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { useLocation, useNavigate } from 'react-router-dom';

import * as styles from '@/page/intro/Intro.css';
import { PATH } from '@/route';

type PageStateType = 'MANDALART' | 'CORE_GOAL' | 'SUB_GOALS';

const ROUTE_BY_STATE: Record<PageStateType, string> = {
MANDALART: PATH.TODO,
CORE_GOAL: PATH.TODO_UPPER,
SUB_GOALS: PATH.TODO_LOWER,
};

const MESSAGE = {
START: {
Expand All @@ -16,13 +25,19 @@ const MESSAGE = {
const Intro = () => {
const navigate = useNavigate();
const location = useLocation();
const isWritten = location.state?.isWritten ?? false; // 기본값 false

const handleNavigateToTodo = () => {
navigate('/todo');
};
const pageState = (location.state as { pageState?: PageStateType })?.pageState;
const isStart = pageState === 'MANDALART';

const content = isStart ? MESSAGE.START : MESSAGE.CONTINUE;

const content = isWritten ? MESSAGE.CONTINUE : MESSAGE.START;
const handleGoTodo = () => {
if (!pageState) {
navigate(PATH.TODO);
return;
}
navigate(ROUTE_BY_STATE[pageState]);
};

const renderTitle = content.title.split('<br/>').map((line, index) => (
<span key={index}>
Expand All @@ -34,7 +49,7 @@ const Intro = () => {
return (
<main className={styles.introContainer}>
<h1 className={styles.introText}>{renderTitle}</h1>
<button className={styles.buttonContainer} onClick={handleNavigateToTodo}>
<button className={styles.buttonContainer} onClick={handleGoTodo}>
{content.button}
</button>
</main>
Expand Down
48 changes: 34 additions & 14 deletions src/shared/component/Layout/layoutHeader/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useNavigate, useLocation, Link } from 'react-router-dom';
import { useShallow } from 'zustand/react/shallow';

import * as styles from './Header.css';

Expand All @@ -9,6 +10,7 @@
import { useOverlayModal } from '@/common/hook/useOverlayModal';
import { useGetUser } from '@/api/domain/signup/hook/useGetUser';
import UserModal from '@/common/component/UserModal/UserModal';
import { useAuthStore } from '@/store/useAuthStore';

const MENUS = [
{ label: '나의 할 일', path: PATH.TODO },
Expand All @@ -20,7 +22,16 @@
const navigate = useNavigate();
const location = useLocation();

const { data: user, isLoading } = useGetUser();
const { data: userData, isLoading } = useGetUser();

const { user, isLoggedIn, setUser, resetUser } = useAuthStore(
useShallow((s) => ({
user: s.user,
isLoggedIn: s.isLoggedIn,
setUser: s.setUser,
resetUser: s.resetUser,
})),
);

const findActiveMenu = MENUS.find((menu) => location.pathname.startsWith(menu.path));
const initialMenu = findActiveMenu ? findActiveMenu.label : '';
Expand All @@ -30,6 +41,18 @@

const { openModal, closeModal } = useOverlayModal();

useEffect(() => {
if (isLoading) {
return;
}

if (userData) {
setUser(userData);
} else {
resetUser();
}
}, [userData, setUser, resetUser]);

Check warning on line 54 in src/shared/component/Layout/layoutHeader/Header.tsx

View workflow job for this annotation

GitHub Actions / build

React Hook useEffect has a missing dependency: 'isLoading'. Either include it or remove the dependency array

Check warning on line 54 in src/shared/component/Layout/layoutHeader/Header.tsx

View workflow job for this annotation

GitHub Actions / build

React Hook useEffect has a missing dependency: 'isLoading'. Either include it or remove the dependency array

const handleLogin = () => {
openModal(<LoginModal onClose={closeModal} />);
};
Expand Down Expand Up @@ -62,17 +85,14 @@
);
})}
</nav>
{!isLoading && user && (
<>
<img
src={user.profileImageUrl}
alt="유저 프로필 이미지"
className={styles.profilePlaceholder}
onClick={handleProfile}
/>
{openProfile && <UserModal onClose={handleProfile} />}
</>
)}
<button onClick={handleProfile}>
<img
src={user.profileImageUrl}
alt="유저 프로필 이미지"
className={styles.profilePlaceholder}
/>
</button>
{openProfile && <UserModal onClose={handleProfile} />}
</>
);

Expand All @@ -83,7 +103,7 @@
<IcLogo className={styles.logoImage} />
</Link>

{!isLoading && user ? (
{isLoggedIn ? (
renderNavMenu()
) : (
<button className={styles.loginButton} onClick={handleLogin} type="button">
Expand Down
22 changes: 22 additions & 0 deletions src/store/types/authTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export interface AnswerType {
questionId: number;
choiceId: number;
}

export interface UserType {
id?: number;
name: string;
email: string;
birthday?: string;
job?: string;
profileImageUrl: string;
answers?: AnswerType[];
}

export interface AuthStoreType {
user: UserType;
isLoggedIn: boolean;
setUser: (newUser: UserType) => void;
resetUser: () => void;
updateLoginStatus: () => void;
}
Loading