Log In
diff --git a/src/features/auth/ui/LoginLoading.tsx b/src/features/auth/ui/LoginLoading.tsx
new file mode 100644
index 0000000..e90883b
--- /dev/null
+++ b/src/features/auth/ui/LoginLoading.tsx
@@ -0,0 +1,57 @@
+import { useEffect } from 'react';
+import { useNavigate, useSearchParams } from 'react-router-dom';
+
+import { loginWithToken, removeLocalAccessToken, setLocalAccessToken } from '../auth.api';
+
+import type { UserDataState } from '@/features/user/model/user.store';
+import { useUserStore } from '@/features/user/model/user.store';
+import { getMyProfile } from '@/features/user/user.api';
+import { customConfirm } from '@/shared/ui';
+
+export const LoginLoading = () => {
+ const [params] = useSearchParams();
+ const navigate = useNavigate();
+ const { setUserData } = useUserStore(state => state.actions);
+ const token = params.get('token') ?? '';
+
+ useEffect(() => {
+ const login = async () => {
+ try {
+ if (!token) {
+ await customConfirm({
+ title: '잘못된 접근',
+ text: '메인 페이지로 이동합니다.',
+ icon: 'error',
+ showCancelButton: false,
+ });
+ return;
+ }
+
+ const newAccessToken = await loginWithToken(token);
+
+ if (newAccessToken) {
+ setLocalAccessToken(newAccessToken);
+ }
+
+ const userData = (await getMyProfile().then(res => res.data)) as UserDataState;
+ setUserData(userData);
+
+ navigate('/');
+ } catch (error) {
+ console.error(error);
+ await customConfirm({
+ title: '로그인 실패',
+ text: '메인 페이지로 이동합니다.',
+ icon: 'error',
+ showCancelButton: false,
+ });
+ removeLocalAccessToken();
+ navigate('/');
+ }
+ };
+
+ void login();
+ }, []);
+
+ return
Redirect...
;
+};
diff --git a/src/features/portfolio/portfolio.api.ts b/src/features/portfolio/portfolio.api.ts
new file mode 100644
index 0000000..0cf99b2
--- /dev/null
+++ b/src/features/portfolio/portfolio.api.ts
@@ -0,0 +1,6 @@
+import type { PostPortfolioApiResponse, PostPortfolioDTO } from './portfolio.dto';
+
+import api from '@/shared/api/baseApi';
+
+export const postCreatePortfolio = (data: PostPortfolioDTO) =>
+ api.post
('/portfolio', data).then(res => res.data);
diff --git a/src/features/portfolio/portfolio.dto.ts b/src/features/portfolio/portfolio.dto.ts
new file mode 100644
index 0000000..58cbc12
--- /dev/null
+++ b/src/features/portfolio/portfolio.dto.ts
@@ -0,0 +1,9 @@
+import type { PostUserResponseDTO } from '../user/user.dto';
+
+import type { ApiResponse } from '@/shared/api';
+
+export interface PostPortfolioDTO {
+ portfolioURL: string;
+}
+
+export type PostPortfolioApiResponse = ApiResponse;
diff --git a/src/features/user/model/user.store.ts b/src/features/user/model/user.store.ts
new file mode 100644
index 0000000..31c19be
--- /dev/null
+++ b/src/features/user/model/user.store.ts
@@ -0,0 +1,49 @@
+import { create } from 'zustand';
+import { immer } from 'zustand/middleware/immer';
+
+import type { UserRole } from '../user.dto';
+
+export interface UserDataState {
+ userId: number;
+ name: string;
+ imageUrl: string;
+ role: UserRole;
+}
+
+interface UserState {
+ userData: UserDataState | null;
+}
+
+interface UserActions {
+ actions: {
+ setUserData: (data: UserDataState | null) => void;
+ updateUserData: (updatedData: Partial) => void;
+ clearUserData: () => void;
+ };
+}
+
+const initialState = {
+ userId: -1,
+ userData: null,
+};
+
+export const useUserStore = create(
+ immer(set => ({
+ userData: null,
+ actions: {
+ setUserData: data => {
+ set({ userData: data });
+ },
+ updateUserData: updatedData => {
+ set(state => {
+ if (state.userData) {
+ Object.assign(state.userData, updatedData);
+ }
+ });
+ },
+ clearUserData: () => {
+ set(initialState);
+ },
+ },
+ })),
+);
diff --git a/src/features/user/user.api.ts b/src/features/user/user.api.ts
new file mode 100644
index 0000000..5353d58
--- /dev/null
+++ b/src/features/user/user.api.ts
@@ -0,0 +1,29 @@
+import type {
+ GetEditUserApiResponse,
+ GetMyProfileApiResponse,
+ GetUserProfileApiResponse,
+ PostUserApiReponse,
+ PutEditUserApiResponse,
+ PutUserDTO,
+} from './user.dto';
+import type { GetUserDefaultApiResponse, PostUserDTO } from './user.dto';
+
+import api from '@/shared/api/baseApi';
+
+export const getUserDefault = () =>
+ api.get('/profile').then(res => res.data);
+
+export const postCreateUser = (data: PostUserDTO) =>
+ api.post('/profile', data).then(res => res.data);
+
+export const getUserProfile = (userId: number) =>
+ api.get(`/user/${userId}/profile`);
+
+export const getUserEdit = (userId: number) =>
+ api.get(`/user/${userId}/edit`).then(res => res.data);
+
+export const putUserEdit = (data: PutUserDTO, userId: number) =>
+ api.put(`/user/${userId}/edit`, data).then(res => res.data);
+
+export const getMyProfile = () =>
+ api.get('/user/my-info').then(res => res.data);
diff --git a/src/features/user/user.dto.ts b/src/features/user/user.dto.ts
new file mode 100644
index 0000000..299e39b
--- /dev/null
+++ b/src/features/user/user.dto.ts
@@ -0,0 +1,51 @@
+import type { Color } from '../archive';
+import type { UserDataState } from './model/user.store';
+
+import type { ApiResponse } from '@/shared/api';
+
+export type UserRole = 'REAL_NEWBIE' | 'JUST_NEWBIE' | 'OLD_NEWBIE' | 'USER' | 'ADMIN';
+
+export interface UserDefaultInfo {
+ email: string;
+ name: string;
+ imageUrl: string;
+}
+
+export interface BaseUserDTO {
+ name: string;
+ email: string;
+ briefIntro: string;
+ imageUrl: string;
+ majorJobGroup: string;
+ minorJobGroup: string;
+ jobTitle: string;
+ division: string;
+}
+
+export interface PostUserDTO extends BaseUserDTO {
+ url: string[];
+ s3StoredImageUrls: string[];
+}
+
+export interface PutUserDTO extends BaseUserDTO {
+ portfolioLink: string;
+ socials: string[];
+}
+
+export interface User extends BaseUserDTO {
+ socials: string[];
+ role: UserRole;
+ color: Color;
+ portfolioLink: string;
+}
+
+export interface PostUserResponseDTO {
+ userId: number;
+}
+
+export type GetUserDefaultApiResponse = ApiResponse;
+export type PostUserApiReponse = ApiResponse;
+export type GetUserProfileApiResponse = ApiResponse;
+export type GetEditUserApiResponse = ApiResponse;
+export type PutEditUserApiResponse = ApiResponse;
+export type GetMyProfileApiResponse = ApiResponse;
diff --git a/src/features/user/user.hook.ts b/src/features/user/user.hook.ts
new file mode 100644
index 0000000..a2d15b6
--- /dev/null
+++ b/src/features/user/user.hook.ts
@@ -0,0 +1,50 @@
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+
+import {
+ getUserDefault,
+ getUserEdit,
+ getUserProfile,
+ postCreateUser,
+ putUserEdit,
+} from './user.api';
+import type { PostUserDTO, PutUserDTO } from './user.dto';
+
+export const useGetUserDefault = () => {
+ return useQuery({
+ queryKey: ['/user', 'profile'],
+ queryFn: () => getUserDefault(),
+ });
+};
+
+export const useGetUserProfile = (userId: number) => {
+ return useQuery({
+ queryKey: ['/user', userId, 'profile'],
+ queryFn: () => getUserProfile(userId),
+ });
+};
+
+export const useGetUserEdit = (userId: number) => {
+ return useQuery({
+ queryKey: ['/user', userId, 'edit'],
+ queryFn: () => getUserEdit(userId),
+ });
+};
+
+export const useCreateUser = () => {
+ return useMutation({
+ mutationFn: ({ data }: { data: PostUserDTO }) => postCreateUser(data),
+ });
+};
+
+export const useEditUser = (userId: number) => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({ data }: { data: PutUserDTO }) => putUserEdit(data, userId),
+ onSettled: () => {
+ queryClient.invalidateQueries({ queryKey: ['/user', userId, 'edit'] }).catch(error => {
+ console.error('Failed to invalidate queries: ', error);
+ });
+ },
+ });
+};
diff --git a/src/shared/api/baseApi.ts b/src/shared/api/baseApi.ts
index a71b2cf..75777e3 100644
--- a/src/shared/api/baseApi.ts
+++ b/src/shared/api/baseApi.ts
@@ -1,38 +1,64 @@
import type { AxiosRequestConfig } from 'axios';
import axios from 'axios';
-import { useAuth } from '@/app/AuthProvider';
+import { getLocalAccessToken, reissueToken } from '@/features/auth/auth.api';
+
+let isRefreshing = false;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type EventHandler = (...args: any[]) => void;
+
+interface interceptorEventsTypes {
+ [event: string]: EventHandler;
+}
+
+const interceptorEvents: interceptorEventsTypes = {
+ logout: () => {},
+};
+
+export const setInterceptorEvents = (event: string, handler: EventHandler) => {
+ interceptorEvents[event] = handler;
+};
const api = axios.create({
- baseURL: ``, // Backend API Domain 주소
+ baseURL: `https://api.palettee.site`, // Backend API Domain 주소
timeout: 10_000,
withCredentials: true,
});
-// api.interceptors.request.use(config => {
-// const { accessToken } = useAuth();
-// if (accessToken) {
-// config.headers.Authorization = `Bearer ${accessToken}`;
-// }
-// return config;
-// });
+api.interceptors.request.use(config => {
+ const accessToken = getLocalAccessToken();
+ if (accessToken) {
+ config.headers.Authorization = `Bearer ${accessToken}`;
+ }
+ return config;
+});
api.interceptors.response.use(
response => response,
async error => {
- const { reissueToken } = useAuth();
-
+ console.log('에러 ㅣ ', error.response);
if (error.response?.status === 401) {
+ if (isRefreshing) {
+ return Promise.reject(error instanceof Error ? error : new Error('Unknown Error'));
+ }
+
+ isRefreshing = true;
+
try {
await reissueToken();
- const { accessToken } = useAuth();
+ const accessToken = getLocalAccessToken();
if (accessToken && error.config) {
- error.config.headers.Authorization = `Bearer ${accessToken}`;
+ error.config.headers.Authorization = `${accessToken}`;
return api.request(error.config as AxiosRequestConfig);
}
} catch (reissueError) {
+ interceptorEvents['logout']('토큰이 만료되었습니다.');
console.error('토큰 재발급 실패:', reissueError);
+ } finally {
+ // eslint-disable-next-line require-atomic-updates
+ isRefreshing = false;
}
}
diff --git a/src/widgets/Layout/index.tsx b/src/widgets/Layout/index.tsx
index 9ae3da2..39e7df3 100644
--- a/src/widgets/Layout/index.tsx
+++ b/src/widgets/Layout/index.tsx
@@ -10,6 +10,7 @@ import { ChattingBtn } from './ui/ChattingBtn/ChattingBtn';
import { Footer } from './ui/Footer/Footer';
import { Header } from './ui/Header/Header';
+import AuthProvider from '@/app/AuthProvider';
import { useArchiveStore } from '@/features';
import { usePageLifecycle } from '@/shared/hook';
@@ -26,14 +27,14 @@ export const Layout = () => {
});
return (
- <>
+
- >
+
);
};
diff --git a/src/widgets/Layout/ui/Header/Header.tsx b/src/widgets/Layout/ui/Header/Header.tsx
index f3647f9..861276d 100644
--- a/src/widgets/Layout/ui/Header/Header.tsx
+++ b/src/widgets/Layout/ui/Header/Header.tsx
@@ -4,25 +4,46 @@ import cn from 'classnames';
import React from 'react';
import { useState, useEffect } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
+import { useShallow } from 'zustand/shallow';
import styles from './Header.module.scss';
import { NAV_LINKS } from '../../constants';
//assets
+import { logout } from '@/features/auth/auth.api';
+import { useUserStore } from '@/features/user/model/user.store';
import Logo from '@/shared/assets/paletteLogo.svg?react';
//model
import { useModalStore } from '@/shared/model/modalStore';
//component
-import { Button } from '@/shared/ui';
+
+import { Button, customConfirm } from '@/shared/ui';
import { MenuModal } from '@/widgets/MenuModal/MenuModal';
export const Header = () => {
const { pathname } = useLocation();
const navigate = useNavigate();
const open = useModalStore(state => state.actions.open);
+ const { userData, actions } = useUserStore(
+ useShallow(state => ({
+ userData: state.userData,
+ actions: state.actions,
+ })),
+ );
const [isMobile, setIsMobile] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
+ const logoutHandler = async () => {
+ await logout();
+ actions.setUserData(null);
+ await customConfirm({
+ title: '로그아웃',
+ text: '로그아웃 되었습니다.',
+ icon: 'info',
+ showCancelButton: false,
+ });
+ };
+
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 1000);
@@ -86,13 +107,23 @@ export const Header = () => {
navigate('/like');
}}
/>
-
+ {userData ? (
+
+ ) : (
+
+ )}
{' '}
>
)}