diff --git a/.eslintrc.js b/.eslintrc.js index be6643c..b42111d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,13 +4,13 @@ module.exports = { es2021: true, }, globals: { - 'JSX': true + JSX: true, }, extends: [ 'standard-with-typescript', 'eslint:recommended', 'plugin:prettier/recommended', - 'next/core-web-vitals' + 'next/core-web-vitals', ], parserOptions: { ecmaVersion: 'latest', @@ -19,7 +19,7 @@ module.exports = { ignorePatterns: ['/src/assets/**', '/src/styles/**'], rules: { '@typescript-eslint/strict-boolean-expressions': 'off', - 'prettier/prettier': ["error", { "endOfLine": "auto" }], - '@typescript-eslint/explicit-function-return-type': 'off' + 'prettier/prettier': ['error', { endOfLine: 'auto' }], + '@typescript-eslint/explicit-function-return-type': 'off', }, }; diff --git a/next.config.js b/next.config.js index 680e9ca..408b379 100644 --- a/next.config.js +++ b/next.config.js @@ -1,6 +1,9 @@ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, + images: { + domains: ['mara-s3bucket.s3.ap-northeast-2.amazonaws.com'], + }, webpack: (config) => { return { ...config, @@ -22,8 +25,8 @@ const nextConfig = { source: '/', destination: '/home', permanent: true, - } - ] + }, + ]; }, }; diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts index dd0fbbc..ad0c8d9 100644 --- a/src/api/axiosInstance.ts +++ b/src/api/axiosInstance.ts @@ -9,9 +9,50 @@ export interface ApiResponseDTO { const axiosInstance = axios.create({ baseURL: process.env.NEXT_PUBLIC_BASE_URI, timeout: 5000, - headers: { - 'Content-Type': 'application/json', - }, }); +axiosInstance.interceptors.request.use((config) => { + config.headers['Content-Type'] = 'application/json'; + + if (typeof window !== 'undefined') { + config.headers.Authorization = `Bearer ${localStorage.getItem('accessToken')}`; + } + + return config; +}); + +axiosInstance.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config; + + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + + const refreshToken = + typeof window !== 'undefined' + ? localStorage.getItem('refreshToken') + : null; + + try { + const refreshResponse = await axios.post('/users/kakao-login', { + refreshToken, + }); + + if (typeof window !== 'undefined') { + localStorage.setItem('accessToken', refreshResponse.data.accessToken); + } + + originalRequest.headers.Authorization = `Bearer ${refreshResponse.data.accessToken}`; + return await axiosInstance(originalRequest); + } catch (refreshError) { + console.error('Error refreshing token:', refreshError); + throw refreshError; + } + } + + return await Promise.reject(error); + }, +); + export default axiosInstance; diff --git a/src/components/organisms/FridgeBoard.tsx b/src/components/organisms/FridgeBoard.tsx index 4e406be..cdef6ec 100644 --- a/src/components/organisms/FridgeBoard.tsx +++ b/src/components/organisms/FridgeBoard.tsx @@ -6,22 +6,20 @@ import { IngredientItemBox, } from '@/components/molecules'; -const FridgeBoard: React.FC = () => { +const FridgeBoard: React.FC<{ data?: any | null }> = ({ data }) => { const [currentTabName, setCurrentTabName] = useState<'냉장' | '냉동'>('냉장'); const handleTabNameChange: (tabName: '냉장' | '냉동') => void = (tabName) => { setCurrentTabName(tabName); }; - const datas = ['d']; - return ( - {datas.length !== 0 ? ( + {data !== null || (Array.isArray(data) && data?.length !== 0) ? (
diff --git a/src/components/organisms/FridgeListModal.tsx b/src/components/organisms/FridgeListModal.tsx index d09deaa..60de454 100644 --- a/src/components/organisms/FridgeListModal.tsx +++ b/src/components/organisms/FridgeListModal.tsx @@ -21,6 +21,7 @@ const FridgeListModal: React.FC<{
{FRIDGE_NAME_LIST.map((fridgeName) => ( { diff --git a/src/components/templates/withLogin.tsx b/src/components/templates/withLogin.tsx index a722b8c..170957f 100644 --- a/src/components/templates/withLogin.tsx +++ b/src/components/templates/withLogin.tsx @@ -1,16 +1,13 @@ import React, { useEffect } from 'react'; import { useRouter } from 'next/router'; -import useToast from '@/hooks/useToast'; const withLogin = (InnerComponent: React.FC) => { return () => { const router = useRouter(); - const token = localStorage.getItem('token'); - const { showToast } = useToast(); + const token = localStorage.getItem('accessToken'); const redirectToLogin: () => Promise = async () => { if (!token) { - showToast('로그인이 필요합니다.', 'info'); try { await router.push('/login'); } catch (error) { diff --git a/src/hooks/queries/fridge/useGetIngredientList.ts b/src/hooks/queries/fridge/useGetIngredientList.ts index c98760a..63a83fb 100644 --- a/src/hooks/queries/fridge/useGetIngredientList.ts +++ b/src/hooks/queries/fridge/useGetIngredientList.ts @@ -5,7 +5,7 @@ import { useBaseQuery } from '../useBaseQuery'; const useGetIngredientList = () => { // const testApiEndpoint = 'https://jsonplaceholder.typicode.com/todos'; - return useBaseQuery(queryKeys.INGREDIENT(), '/ingrs'); + return useBaseQuery(queryKeys.INGREDIENT(), '/regrigs/my'); }; export default useGetIngredientList; diff --git a/src/hooks/queries/login/index.ts b/src/hooks/queries/login/index.ts index cd9fc0e..ffcc97c 100644 --- a/src/hooks/queries/login/index.ts +++ b/src/hooks/queries/login/index.ts @@ -1 +1,2 @@ export { default as useGetKakaoToken } from './useGetKakaoToken'; +export { default as usePostUser } from './usePostUser'; diff --git a/src/hooks/queries/login/useGetKakaoToken.ts b/src/hooks/queries/login/useGetKakaoToken.ts index 8b1d9d3..452c2a7 100644 --- a/src/hooks/queries/login/useGetKakaoToken.ts +++ b/src/hooks/queries/login/useGetKakaoToken.ts @@ -1,13 +1,25 @@ +import { useRouter } from 'next/router'; import { queryKeys } from '../queryKeys'; import { useBaseQuery } from '../useBaseQuery'; const useGetKakaoToken = (code: string | null = '') => { - const { data } = useBaseQuery<{ data: { accessToken: string } }>( - queryKeys.KAKAO(), - `/users/kakao-login?code=${code}`, - ); - if (data) { - localStorage.setItem('token', data.data.accessToken); + const router = useRouter(); + const { data } = useBaseQuery<{ + accessToken: string; + refreshToken: string; + kakaoId: number; + kakaoEmail: string; + }>(queryKeys.KAKAO(), `/users/kakao-login?code=${code}`, true); + + if (data?.data?.accessToken === undefined) { + void router.push( + `/mypage/profile?kakaoId=${data?.data?.kakaoId}&kakaoEmail=${data?.data?.kakaoEmail}`, + ); + } + if (data?.data) { + localStorage.setItem('accessToken', data.data.accessToken); + localStorage.setItem('refreshToken', data.data.refreshToken); + void router.push('/home'); } }; diff --git a/src/hooks/queries/login/usePostUser.ts b/src/hooks/queries/login/usePostUser.ts new file mode 100644 index 0000000..10534dd --- /dev/null +++ b/src/hooks/queries/login/usePostUser.ts @@ -0,0 +1,35 @@ +import { useRouter } from 'next/router'; +import { queryKeys } from '../queryKeys'; +import { useBaseMutation } from '../useBaseMutation'; + +interface PostUserBodyType { + nickName: string; + kakaoId: number; + kakaoEmail: string; + googleEmail: string; + profileImage: string; +} + +const usePostUser = () => { + const router = useRouter(); + const onSuccess = ({ + data, + }: { + data: { + accessToken: string; + refreshToken: string; + email: string; + nickName: string; + }; + }) => { + localStorage.setItem('accessToken', data.accessToken); + localStorage.setItem('refreshToken', data.refreshToken); + void router.push('/home'); + }; + return useBaseMutation( + queryKeys.KAKAO(), + `/users`, + onSuccess, + ); +}; +export default usePostUser; diff --git a/src/hooks/queries/useBaseMutation.ts b/src/hooks/queries/useBaseMutation.ts new file mode 100644 index 0000000..15008ae --- /dev/null +++ b/src/hooks/queries/useBaseMutation.ts @@ -0,0 +1,22 @@ +import axiosInstance from '@/api/axiosInstance'; +import { useMutation } from '@tanstack/react-query'; + +export const fetchData = async (url: string, body: T) => { + const response = await axiosInstance.post<{ data: T }>(url, body); + return response.data; +}; + +export const useBaseMutation = ( + mutationKey: any, + url: string, + onSuccess: (any: any) => void, +) => { + return useMutation({ + mutationKey, + mutationFn: async (body: T) => { + const response = await fetchData(url, body); + + onSuccess(response.data); + }, + }); +}; diff --git a/src/hooks/queries/useBaseQuery.ts b/src/hooks/queries/useBaseQuery.ts index 61eb4ae..2fe6ddd 100644 --- a/src/hooks/queries/useBaseQuery.ts +++ b/src/hooks/queries/useBaseQuery.ts @@ -1,14 +1,29 @@ import axiosInstance from '@/api/axiosInstance'; import { useSuspenseQuery } from '@tanstack/react-query'; -export const fetchData = async (url: string) => { - const response = await axiosInstance.get<{ data: T }>(url); - return response.data; +export const fetchData = async (url: string, isNotCatch: boolean) => { + try { + const response = await axiosInstance.get<{ data: T; status?: number }>(url); + return response.data; + } catch (error: any) { + if (!isNotCatch) { + if (error.response && error.response.status === 404) { + return await Promise.resolve({ data: null }); + } else { + throw error; + } + } + } }; - -export const useBaseQuery = (queryKey: any, url: string) => { +export const useBaseQuery = ( + queryKey: any, + url: string, + isNotCatch: boolean = false, +) => { return useSuspenseQuery({ queryKey, - queryFn: async () => await fetchData(url).then((res) => res.data), + queryFn: async () => { + return await fetchData(url, isNotCatch); + }, }); }; diff --git a/src/pages/fridge/index.tsx b/src/pages/fridge/index.tsx index 4b9423e..25d05d9 100644 --- a/src/pages/fridge/index.tsx +++ b/src/pages/fridge/index.tsx @@ -13,7 +13,7 @@ import { ModalContent, useDisclosure, } from '@chakra-ui/react'; -// import { useGetIngredientList } from '@/hooks/queries/fridge'; +import { useGetIngredientList } from '@/hooks/queries/fridge'; const FridgePage: NextPage = () => { const { @@ -27,8 +27,8 @@ const FridgePage: NextPage = () => { onOpen: onOpenFridgeListModal, onClose: onCloseFridgeListModal, } = useDisclosure(); - // const data = useGetIngredientList(); - // console.log('받아올 데이터', data); + + const { data } = useGetIngredientList(); return ( <> @@ -81,7 +81,7 @@ const FridgePage: NextPage = () => { toggleIsOpenFridgeListModal={onOpenFridgeListModal} toggleIsOpenIngredientAddModal={onOpenIngredientAddModal} /> - +
diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index e28952e..e1250e5 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -8,8 +8,9 @@ import { IngredientBoard } from '@/components/organisms'; import Header from '@/components/organisms/Header'; import Link from 'next/link'; import { AlarmIcon } from '@/assets/icons'; +import withLogin from '@/components/templates/withLogin'; -const NEAR_EXPIRATION_COUNT_MOCK_DATA=2; +const NEAR_EXPIRATION_COUNT_MOCK_DATA = 2; const Home: NextPage = () => { const isNearExpirationWarn = true; @@ -26,7 +27,10 @@ const Home: NextPage = () => { />
{isNearExpirationWarn && ( - + )}
{
); }; -export default Home; +export default withLogin(Home); diff --git a/src/pages/mypage/profile/index.tsx b/src/pages/mypage/profile/index.tsx index 423f798..e238f7f 100644 --- a/src/pages/mypage/profile/index.tsx +++ b/src/pages/mypage/profile/index.tsx @@ -5,6 +5,30 @@ import { Button, ExclamationAlertSpan } from '@/components/atoms'; import React, { useCallback, useState } from 'react'; import Header from '@/components/organisms/Header'; import { debounceFunction } from '@/utils/debounceUtil'; +import usePostUser from '@/hooks/queries/login/usePostUser'; + +const PROPILES = [ + { + string: 'GREEN', + imgUrl: + 'https://mara-s3bucket.s3.ap-northeast-2.amazonaws.com/images/profiles/green-nor.svg', + }, + { + string: 'RED', + imgUrl: + 'https://mara-s3bucket.s3.ap-northeast-2.amazonaws.com/images/profiles/red-nor.svg', + }, + { + string: 'BLUE', + imgUrl: + 'https://mara-s3bucket.s3.ap-northeast-2.amazonaws.com/images/profiles/blue-nor.svg', + }, + { + string: 'YELLOW', + imgUrl: + 'https://mara-s3bucket.s3.ap-northeast-2.amazonaws.com/images/profiles/yellow-nor.svg', + }, +]; const FriendsListPage: NextPage = () => { const [selectedImageSrc, setSelectedImageSrc] = useState(ProfileImg); @@ -12,6 +36,8 @@ const FriendsListPage: NextPage = () => { const [isNicknameAvailable, setIsNicknameAvailable] = useState(false); const [isNicknameChecked, setIsNicknameChecked] = useState(false); + const postUser = usePostUser(); + const handleImageClick: (src: string) => void = (src) => { // imgURL로 변경 console.log('선택한이미지SRC', src); @@ -29,13 +55,31 @@ const FriendsListPage: NextPage = () => { const debouncedHandleNicknameChange = useCallback( debounceFunction((currentNickname: string) => { - console.log('변경할 닉네임', currentNickname); setIsNicknameChecked(true); setIsNicknameAvailable(false); }, 1000), [], ); + const handleSumbit = () => { + const urlParams = + typeof window !== 'undefined' + ? new URLSearchParams(window.location.search) + : null; + const kakaoId = Number(urlParams?.get('kakaoId')); + const kakaoEmail = urlParams?.get('kakaoEmail'); + + if (!kakaoId || !kakaoEmail) return; + + postUser.mutate({ + nickName: nickname, + kakaoId, + kakaoEmail, + googleEmail: '', + profileImage: 'BLUE', + }); + }; + return (
@@ -75,53 +119,26 @@ const FriendsListPage: NextPage = () => { ))}
- 프로필 이미지) => { - handleImageClick(e.currentTarget.src); - }} - /> - 프로필 이미지) => { - handleImageClick(e.currentTarget.src); - }} - /> - 프로필 이미지) => { - handleImageClick(e.currentTarget.src); - }} - /> - 프로필 이미지) => { - handleImageClick(e.currentTarget.src); - }} - /> + {PROPILES.map((profile) => ( + 프로필 이미지 { + handleImageClick(profile.string); + }} + /> + ))}
-
); diff --git a/src/types/common/index.d.ts b/src/types/common/index.d.ts index 9624569..3c13eef 100644 --- a/src/types/common/index.d.ts +++ b/src/types/common/index.d.ts @@ -7,3 +7,10 @@ export interface SortLabel { label: string; value: string; } + +export interface ResErrorType { + timestamp: string; + status: number; + error: string; + path: string; +}