From 5676084aecb23c028bb8d4f1919c3cdd1e5be6ba Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Wed, 31 Jul 2024 20:29:25 +0900 Subject: [PATCH 01/26] =?UTF-8?q?docs:=201=EB=8B=A8=EA=B3=84=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3eaeec280..c5b2b508b 100644 --- a/README.md +++ b/README.md @@ -1 +1,11 @@ -# react-deploy \ No newline at end of file +# react-deploy +FE 카카오 선물하기 6주차 과제: 배포 & 협업 +### 🌱 1단계 - API 명세 협의 & 반영 +작성한 API 문서를 기반으로 팀 내에서 지금까지 만든 API를 검토하고 통일하여 변경 사항을 반영 +- 팀 내에서 일관된 기준을 정하여 API 명세를 결정 +- 때로는 클라이언트의 편의를 위해 API 명세를 결정하는 것이 좋음 +

+- [ ] 팀 내에 배포될 API가 여러 개일 경우 상단 내비게이션 바에서 선택 가능 + - 프론트엔드의 경우 사용자가 팀 내 여러 서버 중 하나를 선택하여 서비스를 이용 + - [ ] 팀 내 백엔드 엔지니어의 이름을 넣고, 이름을 선택하면 해당 엔지니어의 API로 API 통신을 하도록 구현 + - [ ] 기본 선택은 제일 첫 번째 이름 \ No newline at end of file From 508e62cb376e8bfd30d2813c09057bbcac38548e Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Thu, 1 Aug 2024 15:03:34 +0900 Subject: [PATCH 02/26] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=9D=84=20API=20=EB=AA=85=EC=84=B8=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/hooks/auth.mock.ts | 14 ++++---------- src/api/instance/index.ts | 7 +++++++ src/pages/Login/index.tsx | 5 ++--- src/pages/Register/index.tsx | 10 ++++------ 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/api/hooks/auth.mock.ts b/src/api/hooks/auth.mock.ts index 4853553fe..70008328d 100644 --- a/src/api/hooks/auth.mock.ts +++ b/src/api/hooks/auth.mock.ts @@ -4,22 +4,16 @@ const VALID_EMAIL = 'test@example.com'; const VALID_PASSWORD = 'password1234'; export const authMockHandler = [ - rest.post('https://api.example.com/api/members/register', async (req, res, ctx) => { - const { email, password } = await req.json<{ email: string; password: string }>(); - - if (!email || !password) { - return res(ctx.status(400), ctx.json({ message: 'Invalid input' })); - } - - return res(ctx.status(201), ctx.json({ email, token: 'fake-token' })); + rest.post('https://api.example.com/api/members/register', async (_, res, ctx) => { + return res(ctx.status(200), ctx.text('User registered successfully')); }), rest.post('https://api.example.com/api/members/login', async (req, res, ctx) => { const { email, password } = await req.json<{ email: string; password: string }>(); if (email === VALID_EMAIL && password === VALID_PASSWORD) { - return res(ctx.status(200), ctx.json({ email, token: 'fake-token' })); + return res(ctx.status(200), ctx.text('token')); } - return res(ctx.status(403), ctx.json({ message: 'Invalid email or password' })); + return res(ctx.status(403), ctx.text('Invalid email or password')); }), ]; diff --git a/src/api/instance/index.ts b/src/api/instance/index.ts index b83ca1407..46c2c6857 100644 --- a/src/api/instance/index.ts +++ b/src/api/instance/index.ts @@ -11,6 +11,13 @@ const initInstance = (config: AxiosRequestConfig): AxiosInstance => { 'Content-Type': 'application/json', ...config.headers, }, + transformResponse: [(data, headers) => { + if (headers['content-type']?.includes('text/plain')) { + return data; + } + + return JSON.parse(data); + }], }); return instance; diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index 96970fc23..934694698 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -24,10 +24,9 @@ export const LoginPage = () => { } try { - const data = await login(email, password); + const token = await login(email, password); - sessionStorage.setItem('authEmail', data.email); - authSessionStorage.set(data.token); + authSessionStorage.set(token); const redirectUrl = queryParams.get('redirect') ?? `${window.location.origin}/`; window.location.replace(redirectUrl); diff --git a/src/pages/Register/index.tsx b/src/pages/Register/index.tsx index c75ed41b5..dbd7fa8b8 100644 --- a/src/pages/Register/index.tsx +++ b/src/pages/Register/index.tsx @@ -8,7 +8,6 @@ import { Button } from '@/components/common/Button'; import { UnderlineTextField } from '@/components/common/Form/Input/UnderlineTextField'; import { Spacing } from '@/components/common/layouts/Spacing'; import { breakpoints } from '@/styles/variants'; -import { authSessionStorage } from '@/utils/storage'; export const RegisterPage = () => { const [email, setEmail] = useState(''); @@ -22,12 +21,11 @@ export const RegisterPage = () => { } try { - const data = await register(email, password); + const message = await register(email, password); - sessionStorage.setItem('authEmail', data.email); - authSessionStorage.set(data.token); - - alert('회원가입이 완료되었습니다.'); + if (message === 'User registered successfully') { + alert('회원가입이 완료되었습니다.'); + } const redirectUrl = queryParams.get('redirect') ?? `${window.location.origin}/`; return window.location.replace(redirectUrl); From de1c5fd21712f3d2d460fc3c378bb4c1b295c6ca Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Thu, 1 Aug 2024 15:57:22 +0900 Subject: [PATCH 03/26] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=84=B1=EA=B3=B5=20=EC=8B=9C=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=9D=B4=EB=A6=84=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/hooks/auth.mock.ts | 2 +- src/pages/Login/index.tsx | 2 ++ src/pages/MyAccount/index.tsx | 1 + src/pages/Register/index.tsx | 1 + src/provider/Auth/index.tsx | 14 +++++++++----- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/api/hooks/auth.mock.ts b/src/api/hooks/auth.mock.ts index 70008328d..3126b9c98 100644 --- a/src/api/hooks/auth.mock.ts +++ b/src/api/hooks/auth.mock.ts @@ -5,7 +5,7 @@ const VALID_PASSWORD = 'password1234'; export const authMockHandler = [ rest.post('https://api.example.com/api/members/register', async (_, res, ctx) => { - return res(ctx.status(200), ctx.text('User registered successfully')); + return res(ctx.status(201), ctx.text('User registered successfully')); }), rest.post('https://api.example.com/api/members/login', async (req, res, ctx) => { const { email, password } = await req.json<{ email: string; password: string }>(); diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index 934694698..b3108b394 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -26,6 +26,8 @@ export const LoginPage = () => { try { const token = await login(email, password); + sessionStorage.setItem('authEmail', email); + sessionStorage.setItem('authToken', token); authSessionStorage.set(token); const redirectUrl = queryParams.get('redirect') ?? `${window.location.origin}/`; diff --git a/src/pages/MyAccount/index.tsx b/src/pages/MyAccount/index.tsx index 5c59297f7..7d201a73b 100644 --- a/src/pages/MyAccount/index.tsx +++ b/src/pages/MyAccount/index.tsx @@ -12,6 +12,7 @@ export const MyAccountPage = () => { const handleLogout = () => { authSessionStorage.set(undefined); + sessionStorage.removeItem('authEmail'); const redirectURL = `${window.location.origin}${RouterPath.home}`; window.location.replace(redirectURL); diff --git a/src/pages/Register/index.tsx b/src/pages/Register/index.tsx index dbd7fa8b8..5662daca7 100644 --- a/src/pages/Register/index.tsx +++ b/src/pages/Register/index.tsx @@ -24,6 +24,7 @@ export const RegisterPage = () => { const message = await register(email, password); if (message === 'User registered successfully') { + sessionStorage.setItem('authEmail', email); alert('회원가입이 완료되었습니다.'); } diff --git a/src/provider/Auth/index.tsx b/src/provider/Auth/index.tsx index 77cbb73a9..06c921c31 100644 --- a/src/provider/Auth/index.tsx +++ b/src/provider/Auth/index.tsx @@ -4,7 +4,7 @@ import { createContext, useContext, useEffect, useState } from 'react'; import { authSessionStorage } from '@/utils/storage'; type AuthInfo = { - id: string; + email: string; name: string; token: string; }; @@ -12,21 +12,25 @@ type AuthInfo = { export const AuthContext = createContext(undefined); export const AuthProvider = ({ children }: { children: ReactNode }) => { + const currentAuthEmail = sessionStorage.getItem('authEmail'); const currentAuthToken = authSessionStorage.get(); + const [isReady, setIsReady] = useState(!currentAuthToken); const [authInfo, setAuthInfo] = useState(undefined); useEffect(() => { - if (currentAuthToken) { + if (currentAuthEmail && currentAuthToken) { + const name = currentAuthEmail.split('@')[0]; + setAuthInfo({ - id: currentAuthToken, // TODO: 임시로 로그인 페이지에서 입력한 이름을 ID, token, name으로 사용 - name: currentAuthToken, + email: currentAuthEmail, + name: name, token: currentAuthToken, }); setIsReady(true); } - }, [currentAuthToken]); + }, [currentAuthEmail, currentAuthToken]); if (!isReady) return <>; return {children}; From 31899903be4934cbc762500741f9875ac4827808 Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Thu, 1 Aug 2024 18:59:17 +0900 Subject: [PATCH 04/26] =?UTF-8?q?refactor:=20=ED=98=91=EC=9D=98=EB=90=9C?= =?UTF-8?q?=20API=20=EB=AA=85=EC=84=B8=EC=84=9C=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=9D=BC=20=EB=AA=A9=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B0=8F?= =?UTF-8?q?=20=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/hooks/categories.mock.ts | 2 +- src/api/hooks/products.mock.ts | 14 +++++++++----- src/types/index.ts | 1 - 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/api/hooks/categories.mock.ts b/src/api/hooks/categories.mock.ts index f8d65843c..aa21ca5d7 100644 --- a/src/api/hooks/categories.mock.ts +++ b/src/api/hooks/categories.mock.ts @@ -4,7 +4,7 @@ import { getCategoriesPath } from './useGetCategorys'; export const categoriesMockHandler = [ rest.get(getCategoriesPath(), (_, res, ctx) => { - return res(ctx.json(CATEGORIES_RESPONSE_DATA)); + return res(ctx.status(200), ctx.json(CATEGORIES_RESPONSE_DATA)); }), ]; diff --git a/src/api/hooks/products.mock.ts b/src/api/hooks/products.mock.ts index 6cef11235..282d7b3af 100644 --- a/src/api/hooks/products.mock.ts +++ b/src/api/hooks/products.mock.ts @@ -10,7 +10,7 @@ export const productsMockHandler = [ categoryId: '2920', }), (_, res, ctx) => { - return res(ctx.json(PRODUCTS_MOCK_DATA)); + return res(ctx.status(200), ctx.json(PRODUCTS_MOCK_DATA)); }, ), rest.get( @@ -18,26 +18,25 @@ export const productsMockHandler = [ categoryId: '2930', }), (_, res, ctx) => { - return res(ctx.json(PRODUCTS_MOCK_DATA)); + return res(ctx.status(200), ctx.json(PRODUCTS_MOCK_DATA)); }, ), rest.get(getProductDetailPath(':productId'), (_, res, ctx) => { - return res(ctx.json(PRODUCTS_MOCK_DATA.content[0])); + return res(ctx.status(200), ctx.json(PRODUCTS_MOCK_DATA.content[0])); }), rest.get(getProductOptionsPath(':productId'), (_, res, ctx) => { return res( + ctx.status(200), ctx.json([ { id: 1, name: 'Option A', quantity: 10, - productId: 1, }, { id: 2, name: 'Option B', quantity: 20, - productId: 1, }, ]), ); @@ -52,6 +51,7 @@ const PRODUCTS_MOCK_DATA = { imageUrl: 'https://st.kakaocdn.net/product/gift/product/20240215083306_8e1db057580145829542463a84971ae3.png', price: 145000, + categoryId: '2920', }, { id: 2263833, @@ -59,6 +59,7 @@ const PRODUCTS_MOCK_DATA = { imageUrl: 'https://st.kakaocdn.net/product/gift/product/20200513102805_4867c1e4a7ae43b5825e9ae14e2830e3.png', price: 100000, + categoryId: '2920', }, { id: 6502823, @@ -66,6 +67,7 @@ const PRODUCTS_MOCK_DATA = { imageUrl: 'https://st.kakaocdn.net/product/gift/product/20240215112140_11f857e972bc4de6ac1d2f1af47ce182.jpg', price: 108000, + categoryId: '2920', }, { id: 1181831, @@ -73,6 +75,7 @@ const PRODUCTS_MOCK_DATA = { imageUrl: 'https://st.kakaocdn.net/product/gift/product/20240214150740_ad25267defa64912a7c030a7b57dc090.jpg', price: 122000, + categoryId: '2920', }, { id: 1379982, @@ -80,6 +83,7 @@ const PRODUCTS_MOCK_DATA = { imageUrl: 'https://st.kakaocdn.net/product/gift/product/20240118135914_a6e1a7442ea04aa49add5e02ed62b4c3.jpg', price: 133000, + categoryId: '2920', }, ], number: 0, diff --git a/src/types/index.ts b/src/types/index.ts index 33c397459..2c994ffed 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -18,7 +18,6 @@ export type ProductOptionsData = { id: number; name: string; quantity: number; - productId: number; }; export type GoodsDetailOptionItemData = { From b7b939091df861423268c5c7bb0bdc42ae485d19 Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Thu, 1 Aug 2024 21:33:26 +0900 Subject: [PATCH 05/26] =?UTF-8?q?refactor:=20=EC=9C=84=EC=8B=9C=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20API=20=ED=86=B5=ED=95=A9=20=EB=B0=8F=20?= =?UTF-8?q?=EB=AA=A9=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20API=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/hooks/useGetWishlist.ts | 85 ++++++++++++++++++++++ src/api/hooks/wishlist.mock.ts | 81 ++++++++++++++++++++- src/components/features/Wishlist/index.tsx | 61 ++++++---------- 3 files changed, 186 insertions(+), 41 deletions(-) create mode 100644 src/api/hooks/useGetWishlist.ts diff --git a/src/api/hooks/useGetWishlist.ts b/src/api/hooks/useGetWishlist.ts new file mode 100644 index 000000000..425851624 --- /dev/null +++ b/src/api/hooks/useGetWishlist.ts @@ -0,0 +1,85 @@ +import { + type InfiniteData, + useInfiniteQuery, + type UseInfiniteQueryResult, +} from '@tanstack/react-query'; + +import { useAuth } from '@/provider/Auth'; + +type WishlistItem = { + id: number; + name: string; + price: number; + imageUrl: string; +}; + +type WishlistResponseData = { + content: WishlistItem[]; + pageable: { + sort: { + sorted: boolean; + empty: boolean; + }; + pageNumber: number; + pageSize: number; + offset: number; + unpaged: boolean; + paged: boolean; + }; + totalPages: number; + totalElements: number; + last: boolean; + number: number; + size: number; + numberOfElements: number; + first: boolean; + empty: boolean; +}; + +type RequestParams = { + pageToken?: string; + maxResults?: number; +}; + +const getWishlistPath = ({ pageToken, maxResults }: RequestParams) => { + const params = new URLSearchParams(); + params.append('sort', 'createdDate,desc'); + if (pageToken) params.append('page', pageToken); + if (maxResults) params.append('size', maxResults.toString()); + + return `/api/wishes?${params.toString()}`; +}; + +export const getWishlist = async (params: RequestParams, token: string): Promise => { + const url = getWishlistPath(params); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }); + + const data: WishlistResponseData = await response.json(); + return data; +}; + +export const useGetWishlist = ({ + maxResults = 10, + initPageToken, +}: { maxResults?: number; initPageToken?: string }): UseInfiniteQueryResult> => { + const authInfo = useAuth(); + + return useInfiniteQuery({ + queryKey: ['wishlist', maxResults, initPageToken], + queryFn: async ({ pageParam = initPageToken }) => { + if (!authInfo?.token) { + throw new Error('Authentication token is missing'); + } + return getWishlist({ pageToken: pageParam, maxResults }, authInfo.token); + }, + initialPageParam: initPageToken, + getNextPageParam: (lastPage) => (lastPage.last ? undefined : (lastPage.number + 1).toString()), + }); +}; diff --git a/src/api/hooks/wishlist.mock.ts b/src/api/hooks/wishlist.mock.ts index fa1d155c3..2035ea1f9 100644 --- a/src/api/hooks/wishlist.mock.ts +++ b/src/api/hooks/wishlist.mock.ts @@ -1,5 +1,21 @@ import { rest } from 'msw'; +interface WishlistItem { + id: number; + product: { + id: number; + name: string; + price: number; + imageUrl: string; + }; +} + +const getPagedData = (page: number, size: number) => { + const start = page * size; + const end = start + size; + return WISHLIST_MOCK_DATA.content.slice(start, end); +}; + export const wishlistMockHandler = [ rest.get('/api/wishes', async (req, res, ctx) => { const token = req.headers.get('Authorization'); @@ -8,7 +24,44 @@ export const wishlistMockHandler = [ return res(ctx.status(401), ctx.json({ message: 'Invalid or missing token' })); } - return res(ctx.status(200), ctx.json(WISHLIST_MOCK_DATA)); + const page = parseInt(req.url.searchParams.get('page') || '0', 10); + const size = parseInt(req.url.searchParams.get('size') || '10', 10); + + const pagedContent = getPagedData(page, size); + const totalElements = WISHLIST_MOCK_DATA.content.length; + const totalPages = Math.ceil(totalElements / size); + + return res( + ctx.status(200), + ctx.json({ + content: pagedContent.map((item) => ({ + id: item.id, + name: item.product.name, + price: item.product.price, + imageUrl: item.product.imageUrl, + })), + pageable: { + sort: { + sorted: true, + unsorted: false, + empty: false, + }, + pageNumber: page, + pageSize: size, + offset: page * size, + unpaged: false, + paged: true, + }, + totalPages, + totalElements, + last: page + 1 === totalPages, + number: page, + size: size, + numberOfElements: pagedContent.length, + first: page === 0, + empty: pagedContent.length === 0, + }) + ); }), rest.delete('/api/wishes/:wishId', (req, res, ctx) => { const { wishId } = req.params; @@ -21,7 +74,31 @@ export const wishlistMockHandler = [ }), ]; -const WISHLIST_MOCK_DATA = { +interface WishlistMockData { + content: WishlistItem[]; + pageable: { + sort: { + sorted: boolean; + unsorted: boolean; + empty: boolean; + }; + pageNumber: number; + pageSize: number; + offset: number; + unpaged: boolean; + paged: boolean; + }; + totalPages: number; + totalElements: number; + last: boolean; + number: number; + size: number; + numberOfElements: number; + first: boolean; + empty: boolean; +} + +const WISHLIST_MOCK_DATA: WishlistMockData = { content: [ { id: 1, diff --git a/src/components/features/Wishlist/index.tsx b/src/components/features/Wishlist/index.tsx index 9227cc3a3..48a19934c 100644 --- a/src/components/features/Wishlist/index.tsx +++ b/src/components/features/Wishlist/index.tsx @@ -1,43 +1,15 @@ import { Box, Button, Heading, List, ListItem, Text } from '@chakra-ui/react'; -import { useEffect, useState } from 'react'; +import { useGetWishlist } from '@/api/hooks/useGetWishlist'; +import { VisibilityLoader } from '@/components/common/VisibilityLoader'; import { useAuth } from '@/provider/Auth'; -type WishlistItem = { - id: number; - product: { - id: number; - name: string; - price: number; - imageUrl: string; - }; -}; - export const Wishlist = () => { + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useGetWishlist({ maxResults: 10 }); const authInfo = useAuth(); - const [wishlist, setWishList] = useState([]); - - useEffect(() => { - const fetchData = async () => { - if (authInfo?.token) { - try { - const response = await fetch('/api/wishes', { - headers: { - Authorization: `Bearer ${authInfo.token}`, - }, - }); - const wishData = await response.json(); - setWishList(wishData.content); - } catch (error) { - console.error(error); - } - } - }; - - fetchData(); - }, [authInfo]); const handleDelete = async (wishId: number) => { + if (authInfo?.token) { const response = await fetch(`/api/wishes/${wishId}`, { method: 'DELETE', @@ -46,41 +18,52 @@ export const Wishlist = () => { }, }); if (response.status === 204) { - setWishList((prev) => prev.filter((item) => item.id !== wishId)); + } else { alert('삭제 중 오류가 발생했습니다.'); } } }; + const flattenWishlist = data?.pages.flatMap((page) => page.content).flat() || []; + return ( 관심 목록 - {wishlist.length === 0 ? ( + {flattenWishlist.length === 0 ? ( 관심 상품이 없습니다. ) : ( - {wishlist.map((item) => ( + {flattenWishlist.map((item) => - {item.product.name} + {item.name} - {item.product.name} + {item.name} - {item.product.price}원 + {item.price}원 - ))} + )} )} + {hasNextPage && ( + { + if (!isFetchingNextPage) { + fetchNextPage(); + } + }} + /> + )} ); From 5974a9ff122661e91cd58ac3ba857e7cea7103a5 Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Thu, 1 Aug 2024 22:18:51 +0900 Subject: [PATCH 06/26] =?UTF-8?q?fix:=20=EC=9C=84=EC=8B=9C=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=97=90=EC=84=9C=20=EC=83=81=ED=92=88?= =?UTF-8?q?=EC=9D=B4=20=EC=82=AD=EC=A0=9C=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/features/Wishlist/index.tsx | 26 +++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/components/features/Wishlist/index.tsx b/src/components/features/Wishlist/index.tsx index 48a19934c..1ad28e95a 100644 --- a/src/components/features/Wishlist/index.tsx +++ b/src/components/features/Wishlist/index.tsx @@ -1,12 +1,30 @@ import { Box, Button, Heading, List, ListItem, Text } from '@chakra-ui/react'; +// import { useQueryClient } from '@tanstack/react-query'; +import { useEffect, useState } from 'react'; import { useGetWishlist } from '@/api/hooks/useGetWishlist'; import { VisibilityLoader } from '@/components/common/VisibilityLoader'; import { useAuth } from '@/provider/Auth'; +type WishlistItem = { + id: number; + name: string; + price: number; + imageUrl: string; +}; + export const Wishlist = () => { const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useGetWishlist({ maxResults: 10 }); const authInfo = useAuth(); + // const queryClient = useQueryClient(); + const [wishlist, setWishlist] = useState([]); + + useEffect(() => { + if (data) { + const flattenWishlist = data.pages.flatMap((page) => page.content).flat(); + setWishlist(flattenWishlist); + } + }, [data]); const handleDelete = async (wishId: number) => { @@ -18,26 +36,24 @@ export const Wishlist = () => { }, }); if (response.status === 204) { - + setWishlist((prevWishlist) => prevWishlist.filter((item) => item.id !== wishId)); } else { alert('삭제 중 오류가 발생했습니다.'); } } }; - const flattenWishlist = data?.pages.flatMap((page) => page.content).flat() || []; - return ( 관심 목록 - {flattenWishlist.length === 0 ? ( + {wishlist.length === 0 ? ( 관심 상품이 없습니다. ) : ( - {flattenWishlist.map((item) => + {wishlist.map((item) => From 3f916d9cd122abf6314dd8f63b8296c6bce40ad6 Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Thu, 1 Aug 2024 22:34:22 +0900 Subject: [PATCH 07/26] =?UTF-8?q?refactor:=20=EC=9C=84=EC=8B=9C=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F?= =?UTF-8?q?=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/hooks/wishlist.mock.ts | 90 +++++++++++++++++++ .../features/Goods/Detail/OptionSection.tsx | 3 +- src/components/features/Wishlist/index.tsx | 6 +- 3 files changed, 94 insertions(+), 5 deletions(-) diff --git a/src/api/hooks/wishlist.mock.ts b/src/api/hooks/wishlist.mock.ts index 2035ea1f9..5b98b9682 100644 --- a/src/api/hooks/wishlist.mock.ts +++ b/src/api/hooks/wishlist.mock.ts @@ -120,6 +120,96 @@ const WISHLIST_MOCK_DATA: WishlistMockData = { 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292020231106_MXMUB.png', }, }, + { + id: 3, + product: { + id: 3, + name: 'Product C', + price: 150, + imageUrl: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292020231106_MXMUB.png', + }, + }, + { + id: 4, + product: { + id: 4, + name: 'Product D', + price: 150, + imageUrl: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292020231106_MXMUB.png', + }, + }, + { + id: 5, + product: { + id: 5, + name: 'Product E', + price: 150, + imageUrl: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292020231106_MXMUB.png', + }, + }, + { + id: 6, + product: { + id: 6, + name: 'Product F', + price: 150, + imageUrl: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292020231106_MXMUB.png', + }, + }, + { + id: 7, + product: { + id: 7, + name: 'Product G', + price: 150, + imageUrl: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292020231106_MXMUB.png', + }, + }, + { + id: 8, + product: { + id: 8, + name: 'Product H', + price: 150, + imageUrl: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292020231106_MXMUB.png', + }, + }, + { + id: 9, + product: { + id: 9, + name: 'Product I', + price: 150, + imageUrl: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292020231106_MXMUB.png', + }, + }, + { + id: 10, + product: { + id: 10, + name: 'Product J', + price: 150, + imageUrl: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292020231106_MXMUB.png', + }, + }, + { + id: 11, + product: { + id: 11, + name: 'Product K', + price: 150, + imageUrl: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292020231106_MXMUB.png', + }, + }, ], pageable: { sort: { diff --git a/src/components/features/Goods/Detail/OptionSection.tsx b/src/components/features/Goods/Detail/OptionSection.tsx index 74a6b61da..a06a254db 100644 --- a/src/components/features/Goods/Detail/OptionSection.tsx +++ b/src/components/features/Goods/Detail/OptionSection.tsx @@ -7,6 +7,7 @@ import { useGetProductDetail, } from '@/api/hooks/useGetProductDetail'; import { useGetProductOptions } from '@/api/hooks/useGetProductOptions'; +import { BASE_URL } from '@/api/instance'; import { Button } from '@/components/common/Button'; import { useAuth } from '@/provider/Auth'; import { getDynamicPath, RouterPath } from '@/routes/path'; @@ -47,7 +48,7 @@ export const OptionSection = ({ productId }: Props) => { const handleAddToWishlist = async () => { try { - await fetch('/api/wishes', { + await fetch(`${BASE_URL}/api/wishes'`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/components/features/Wishlist/index.tsx b/src/components/features/Wishlist/index.tsx index 1ad28e95a..40b871bd4 100644 --- a/src/components/features/Wishlist/index.tsx +++ b/src/components/features/Wishlist/index.tsx @@ -1,8 +1,8 @@ import { Box, Button, Heading, List, ListItem, Text } from '@chakra-ui/react'; -// import { useQueryClient } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; import { useGetWishlist } from '@/api/hooks/useGetWishlist'; +import { BASE_URL } from '@/api/instance'; import { VisibilityLoader } from '@/components/common/VisibilityLoader'; import { useAuth } from '@/provider/Auth'; @@ -16,7 +16,6 @@ type WishlistItem = { export const Wishlist = () => { const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useGetWishlist({ maxResults: 10 }); const authInfo = useAuth(); - // const queryClient = useQueryClient(); const [wishlist, setWishlist] = useState([]); useEffect(() => { @@ -27,9 +26,8 @@ export const Wishlist = () => { }, [data]); const handleDelete = async (wishId: number) => { - if (authInfo?.token) { - const response = await fetch(`/api/wishes/${wishId}`, { + const response = await fetch(`${BASE_URL}/api/wishes/${wishId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${authInfo.token}`, From ddd4c504b8f1d59155e6b4721bc608d888a26650 Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Thu, 1 Aug 2024 22:41:34 +0900 Subject: [PATCH 08/26] =?UTF-8?q?refactor:=20=ED=98=91=EC=9D=98=EB=90=9C?= =?UTF-8?q?=20=EC=83=81=ED=92=88=20API=20=EB=AA=85=EC=84=B8=EC=84=9C?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=AA=A9=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=B0=8F=20=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/hooks/products.mock.ts | 25 +++++++++++++++++++++++-- src/api/hooks/useGetProducts.ts | 25 +++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/api/hooks/products.mock.ts b/src/api/hooks/products.mock.ts index 282d7b3af..7c20ca314 100644 --- a/src/api/hooks/products.mock.ts +++ b/src/api/hooks/products.mock.ts @@ -86,8 +86,29 @@ const PRODUCTS_MOCK_DATA = { categoryId: '2920', }, ], - number: 0, + pageable: { + offset: 0, + sort: { + empty: false, + unsorted: true, + sorted: false, + }, + unpaged: false, + paged: true, + pageSize: 10, + pageNumber: 0, + }, + last: true, + totalPages: 1, totalElements: 5, size: 10, - last: true, + number: 0, + sort: { + empty: false, + unsorted: true, + sorted: false, + }, + first: true, + numberOfElements: 5, + empty: false, }; diff --git a/src/api/hooks/useGetProducts.ts b/src/api/hooks/useGetProducts.ts index 432f90d93..2368fb023 100644 --- a/src/api/hooks/useGetProducts.ts +++ b/src/api/hooks/useGetProducts.ts @@ -26,10 +26,31 @@ type ProductsResponseData = { type ProductsResponseRawData = { content: ProductData[]; - number: number; + pageable: { + offset: number; + sort: { + empty: boolean; + unsorted: boolean; + sorted: boolean; + }; + unpaged: boolean; + paged: boolean; + pageSize: number; + pageNumber: number; + }; + last: boolean; + totalPages: number; totalElements: number; size: number; - last: boolean; + number: number; + sort: { + empty: boolean; + unsorted: boolean; + sorted: boolean; + }; + first: boolean; + numberOfElements: number; + empty: boolean; }; export const getProductsPath = ({ categoryId, pageToken, maxResults }: RequestParams) => { From 3f745cc77d4ea32db849eb3ccca7df691cc16f8b Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Fri, 2 Aug 2024 02:22:29 +0900 Subject: [PATCH 09/26] =?UTF-8?q?feat:=20API=20=EC=84=A0=ED=83=9D=ED=95=98?= =?UTF-8?q?=EB=8A=94=20UI=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/features/Layout/Header.tsx | 30 ++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/components/features/Layout/Header.tsx b/src/components/features/Layout/Header.tsx index eeac55071..da2dc3bd5 100644 --- a/src/components/features/Layout/Header.tsx +++ b/src/components/features/Layout/Header.tsx @@ -23,6 +23,14 @@ export const Header = () => { /> + + + + + + + + {authInfo ? ( navigate(RouterPath.myAccount)}>내 계정 ) : ( @@ -49,7 +57,13 @@ export const Wrapper = styled.header` const Logo = styled.img` height: ${HEADER_HEIGHT}; `; -const RightWrapper = styled.div``; +const RightWrapper = styled.div` + width: 100%; + display: flex; + justify-content: flex-end; + align-items: center; + gap: 20px; +`; const LinkButton = styled.p` align-items: center; @@ -58,3 +72,17 @@ const LinkButton = styled.p` text-decoration: none; cursor: pointer; `; + +const ApiSelector = styled.select` + width: 200px; + height: 40px; + border: 2px solid #444; + border-radius: 4px; + padding: 5px 10px; + font-size: 16px; + cursor: pointer; + + &:hover { + border-color: #888; + } +`; From 49ef6eed362ae2020c6f5ab7814aab5bc64712c0 Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Fri, 2 Aug 2024 14:24:51 +0900 Subject: [PATCH 10/26] =?UTF-8?q?feat:=20=EB=B0=B1=EC=97=94=EB=93=9C=20API?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=20=EC=A4=80=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 3 +- package.json | 1 + src/App.tsx | 9 +++-- src/api/hooks/useGetCategorys.ts | 4 +- src/api/hooks/useGetProductDetail.ts | 4 +- src/api/hooks/useGetProductOptions.ts | 4 +- src/api/hooks/useGetProducts.ts | 3 +- src/api/instance/index.ts | 28 +++++++++----- .../features/Goods/Detail/OptionSection.tsx | 5 ++- src/components/features/Layout/Header.tsx | 35 +++++++++++++---- src/components/features/Wishlist/index.tsx | 5 ++- src/contexts/ApiContext.tsx | 38 +++++++++++++++++++ 12 files changed, 107 insertions(+), 32 deletions(-) create mode 100644 src/contexts/ApiContext.tsx diff --git a/package-lock.json b/package-lock.json index 0609bb9c3..942797099 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@emotion/styled": "^11.11.0", "@tanstack/react-query": "^5.24.1", "axios": "^1.6.7", + "dotenv": "^16.4.5", "framer-motion": "^11.0.6", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -14204,7 +14205,7 @@ "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index 7e5bdc463..55578040b 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@emotion/styled": "^11.11.0", "@tanstack/react-query": "^5.24.1", "axios": "^1.6.7", + "dotenv": "^16.4.5", "framer-motion": "^11.0.6", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/App.tsx b/src/App.tsx index 24715e671..70f84fa85 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { ChakraProvider } from '@chakra-ui/react'; import { QueryClientProvider } from '@tanstack/react-query'; import { queryClient } from './api/instance'; +import { ApiProvider } from './contexts/ApiContext'; import { AuthProvider } from './provider/Auth'; import { Routes } from './routes'; @@ -9,9 +10,11 @@ const App = () => { return ( - - - + + + + + ); diff --git a/src/api/hooks/useGetCategorys.ts b/src/api/hooks/useGetCategorys.ts index d93e4fc95..acb2b6997 100644 --- a/src/api/hooks/useGetCategorys.ts +++ b/src/api/hooks/useGetCategorys.ts @@ -2,11 +2,11 @@ import { useQuery } from '@tanstack/react-query'; import type { CategoryData } from '@/types'; -import { BASE_URL, fetchInstance } from '../instance'; +import { fetchInstance } from '../instance'; export type CategoryResponseData = CategoryData[]; -export const getCategoriesPath = () => `${BASE_URL}/api/categories`; +export const getCategoriesPath = () => `/api/categories`; const categoriesQueryKey = [getCategoriesPath()]; export const getCategories = async () => { diff --git a/src/api/hooks/useGetProductDetail.ts b/src/api/hooks/useGetProductDetail.ts index 539de0196..5d0e92df9 100644 --- a/src/api/hooks/useGetProductDetail.ts +++ b/src/api/hooks/useGetProductDetail.ts @@ -2,7 +2,7 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import type { ProductData } from '@/types'; -import { BASE_URL, fetchInstance } from '../instance'; +import { fetchInstance } from '../instance'; export type ProductDetailRequestParams = { productId: string; @@ -12,7 +12,7 @@ type Props = ProductDetailRequestParams; export type GoodsDetailResponseData = ProductData; -export const getProductDetailPath = (productId: string) => `${BASE_URL}/api/products/${productId}`; +export const getProductDetailPath = (productId: string) => `/api/products/${productId}`; export const getProductDetail = async (params: ProductDetailRequestParams) => { const response = await fetchInstance.get( diff --git a/src/api/hooks/useGetProductOptions.ts b/src/api/hooks/useGetProductOptions.ts index a3bdc538f..02689b8a9 100644 --- a/src/api/hooks/useGetProductOptions.ts +++ b/src/api/hooks/useGetProductOptions.ts @@ -2,7 +2,7 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import type { ProductOptionsData } from '@/types'; -import { BASE_URL, fetchInstance } from '../instance'; +import { fetchInstance } from '../instance'; import type { ProductDetailRequestParams } from './useGetProductDetail'; type Props = ProductDetailRequestParams; @@ -10,7 +10,7 @@ type Props = ProductDetailRequestParams; export type ProductOptionsResponseData = ProductOptionsData[]; export const getProductOptionsPath = (productId: string) => - `${BASE_URL}/api/products/${productId}/options`; + `/api/products/${productId}/options`; export const getProductOptions = async (params: ProductDetailRequestParams) => { const response = await fetchInstance.get( diff --git a/src/api/hooks/useGetProducts.ts b/src/api/hooks/useGetProducts.ts index 2368fb023..d917d43fe 100644 --- a/src/api/hooks/useGetProducts.ts +++ b/src/api/hooks/useGetProducts.ts @@ -6,7 +6,6 @@ import { import type { ProductData } from '@/types'; -import { BASE_URL } from '../instance'; import { fetchInstance } from './../instance/index'; type RequestParams = { @@ -61,7 +60,7 @@ export const getProductsPath = ({ categoryId, pageToken, maxResults }: RequestPa if (pageToken) params.append('page', pageToken); if (maxResults) params.append('size', maxResults.toString()); - return `${BASE_URL}/api/products?${params.toString()}`; + return `/api/products?${params.toString()}`; }; export const getProducts = async (params: RequestParams): Promise => { diff --git a/src/api/instance/index.ts b/src/api/instance/index.ts index 46c2c6857..60fdbe03c 100644 --- a/src/api/instance/index.ts +++ b/src/api/instance/index.ts @@ -1,15 +1,12 @@ import { QueryClient } from '@tanstack/react-query'; -import type { AxiosInstance, AxiosRequestConfig } from 'axios'; import axios from 'axios'; -const initInstance = (config: AxiosRequestConfig): AxiosInstance => { +const initInstance = () => { const instance = axios.create({ timeout: 5000, - ...config, headers: { Accept: 'application/json', 'Content-Type': 'application/json', - ...config.headers, }, transformResponse: [(data, headers) => { if (headers['content-type']?.includes('text/plain')) { @@ -20,14 +17,27 @@ const initInstance = (config: AxiosRequestConfig): AxiosInstance => { }], }); + instance.interceptors.request.use((config) => { + const apiUrl = localStorage.getItem('apiUrl') || 'https://example.com'; + config.baseURL = apiUrl; + + return config; + }, (error) => { + return Promise.reject(error); + }); + + instance.defaults.transformResponse = [(data, headers) => { + if (headers['content-type']?.includes('text/plain')) { + return data; + } + + return JSON.parse(data); + }]; + return instance; }; -export const BASE_URL = 'https://api.example.com'; -// TODO: 추후 서버 API 주소 변경 필요 -export const fetchInstance = initInstance({ - baseURL: 'https://api.example.com', -}); +export const fetchInstance = initInstance(); export const queryClient = new QueryClient({ defaultOptions: { diff --git a/src/components/features/Goods/Detail/OptionSection.tsx b/src/components/features/Goods/Detail/OptionSection.tsx index a06a254db..1cf249acb 100644 --- a/src/components/features/Goods/Detail/OptionSection.tsx +++ b/src/components/features/Goods/Detail/OptionSection.tsx @@ -7,8 +7,8 @@ import { useGetProductDetail, } from '@/api/hooks/useGetProductDetail'; import { useGetProductOptions } from '@/api/hooks/useGetProductOptions'; -import { BASE_URL } from '@/api/instance'; import { Button } from '@/components/common/Button'; +import { useApi } from '@/contexts/ApiContext'; import { useAuth } from '@/provider/Auth'; import { getDynamicPath, RouterPath } from '@/routes/path'; import { orderHistorySessionStorage } from '@/utils/storage'; @@ -20,6 +20,7 @@ type Props = ProductDetailRequestParams; export const OptionSection = ({ productId }: Props) => { const { data: detail } = useGetProductDetail({ productId }); const { data: options } = useGetProductOptions({ productId }); + const { apiUrl } = useApi(); const [countAsString, setCountAsString] = useState('1'); const totalPrice = useMemo(() => { @@ -48,7 +49,7 @@ export const OptionSection = ({ productId }: Props) => { const handleAddToWishlist = async () => { try { - await fetch(`${BASE_URL}/api/wishes'`, { + await fetch(`${apiUrl}/api/wishes'`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/components/features/Layout/Header.tsx b/src/components/features/Layout/Header.tsx index da2dc3bd5..adc3a1581 100644 --- a/src/components/features/Layout/Header.tsx +++ b/src/components/features/Layout/Header.tsx @@ -2,17 +2,38 @@ import styled from '@emotion/styled'; import { Link, useNavigate } from 'react-router-dom'; import { Container } from '@/components/common/layouts/Container'; +import { useApi } from '@/contexts/ApiContext'; import { useAuth } from '@/provider/Auth'; import { getDynamicPath, RouterPath } from '@/routes/path'; +// 환경 변수 처리 필요! +const backend: { [key: string]: string } = { + backend1: 'https://example.com', + backend2: 'https://example.com', + backend3: 'https://example.com', + backend4: 'https://example.com', + backend5: 'https://example.com', + backend6: 'https://example.com', +}; + +export function getEnvVariable(key: string): string { + return process.env[key] || backend.backend1; +} + export const Header = () => { const navigate = useNavigate(); const authInfo = useAuth(); + const { apiUrl, setApiUrl } = useApi(); const handleLogin = () => { navigate(getDynamicPath.login()); }; + const handleApiChange = (event: React.ChangeEvent) => { + console.log('clicked: ', event.target.value); + setApiUrl(event.target.value); + }; + return ( @@ -23,13 +44,13 @@ export const Header = () => { /> - - - - - - - + + + + + + + {authInfo ? ( navigate(RouterPath.myAccount)}>내 계정 diff --git a/src/components/features/Wishlist/index.tsx b/src/components/features/Wishlist/index.tsx index 40b871bd4..01a335850 100644 --- a/src/components/features/Wishlist/index.tsx +++ b/src/components/features/Wishlist/index.tsx @@ -2,8 +2,8 @@ import { Box, Button, Heading, List, ListItem, Text } from '@chakra-ui/react'; import { useEffect, useState } from 'react'; import { useGetWishlist } from '@/api/hooks/useGetWishlist'; -import { BASE_URL } from '@/api/instance'; import { VisibilityLoader } from '@/components/common/VisibilityLoader'; +import { useApi } from '@/contexts/ApiContext'; import { useAuth } from '@/provider/Auth'; type WishlistItem = { @@ -16,6 +16,7 @@ type WishlistItem = { export const Wishlist = () => { const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useGetWishlist({ maxResults: 10 }); const authInfo = useAuth(); + const { apiUrl } = useApi(); const [wishlist, setWishlist] = useState([]); useEffect(() => { @@ -27,7 +28,7 @@ export const Wishlist = () => { const handleDelete = async (wishId: number) => { if (authInfo?.token) { - const response = await fetch(`${BASE_URL}/api/wishes/${wishId}`, { + const response = await fetch(`${apiUrl}/api/wishes/${wishId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${authInfo.token}`, diff --git a/src/contexts/ApiContext.tsx b/src/contexts/ApiContext.tsx new file mode 100644 index 000000000..f44263cb6 --- /dev/null +++ b/src/contexts/ApiContext.tsx @@ -0,0 +1,38 @@ +import { useQueryClient } from '@tanstack/react-query'; +import type { ReactNode} from 'react'; +import { createContext,useContext, useEffect, useState } from 'react'; + + +interface ApiContextType { + apiUrl: string; + setApiUrl: (url: string) => void; +} +// 변경 필요!!! +const defaultState: ApiContextType = { + apiUrl: 'https://example.com', + setApiUrl: () => {} +}; + +const ApiContext = createContext(defaultState); + +interface ApiProviderProps { + children: ReactNode; +} + +export const ApiProvider = ({ children }: ApiProviderProps) => { + const [apiUrl, setApiUrl] = useState('https://example.com'); + const queryClient = useQueryClient(); + + useEffect(() => { + localStorage.setItem('apiUrl', apiUrl); + queryClient.invalidateQueries(); + }, [apiUrl, queryClient]); + + return ( + + {children} + + ); +}; + +export const useApi = () => useContext(ApiContext); From e4c3d5b0a71ffba70e7168da5fd40300c7d805de Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Fri, 2 Aug 2024 16:29:39 +0900 Subject: [PATCH 11/26] =?UTF-8?q?refactor:=20API=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/instance/index.ts | 15 --------------- src/components/features/Layout/Header.tsx | 5 ----- src/contexts/ApiContext.tsx | 2 +- src/pages/Register/index.tsx | 4 ++-- 4 files changed, 3 insertions(+), 23 deletions(-) diff --git a/src/api/instance/index.ts b/src/api/instance/index.ts index 60fdbe03c..80c150d5b 100644 --- a/src/api/instance/index.ts +++ b/src/api/instance/index.ts @@ -8,13 +8,6 @@ const initInstance = () => { Accept: 'application/json', 'Content-Type': 'application/json', }, - transformResponse: [(data, headers) => { - if (headers['content-type']?.includes('text/plain')) { - return data; - } - - return JSON.parse(data); - }], }); instance.interceptors.request.use((config) => { @@ -26,14 +19,6 @@ const initInstance = () => { return Promise.reject(error); }); - instance.defaults.transformResponse = [(data, headers) => { - if (headers['content-type']?.includes('text/plain')) { - return data; - } - - return JSON.parse(data); - }]; - return instance; }; diff --git a/src/components/features/Layout/Header.tsx b/src/components/features/Layout/Header.tsx index adc3a1581..93fea2785 100644 --- a/src/components/features/Layout/Header.tsx +++ b/src/components/features/Layout/Header.tsx @@ -6,7 +6,6 @@ import { useApi } from '@/contexts/ApiContext'; import { useAuth } from '@/provider/Auth'; import { getDynamicPath, RouterPath } from '@/routes/path'; -// 환경 변수 처리 필요! const backend: { [key: string]: string } = { backend1: 'https://example.com', backend2: 'https://example.com', @@ -16,10 +15,6 @@ const backend: { [key: string]: string } = { backend6: 'https://example.com', }; -export function getEnvVariable(key: string): string { - return process.env[key] || backend.backend1; -} - export const Header = () => { const navigate = useNavigate(); const authInfo = useAuth(); diff --git a/src/contexts/ApiContext.tsx b/src/contexts/ApiContext.tsx index f44263cb6..753110dbb 100644 --- a/src/contexts/ApiContext.tsx +++ b/src/contexts/ApiContext.tsx @@ -7,7 +7,7 @@ interface ApiContextType { apiUrl: string; setApiUrl: (url: string) => void; } -// 변경 필요!!! + const defaultState: ApiContextType = { apiUrl: 'https://example.com', setApiUrl: () => {} diff --git a/src/pages/Register/index.tsx b/src/pages/Register/index.tsx index 5662daca7..2315dfbb7 100644 --- a/src/pages/Register/index.tsx +++ b/src/pages/Register/index.tsx @@ -21,9 +21,9 @@ export const RegisterPage = () => { } try { - const message = await register(email, password); + const response = await register(email, password); - if (message === 'User registered successfully') { + if (response.status === 201) { sessionStorage.setItem('authEmail', email); alert('회원가입이 완료되었습니다.'); } From 5df5fd2d161eb968e99bfa54426dc23e260126e7 Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Fri, 2 Aug 2024 20:39:17 +0900 Subject: [PATCH 12/26] =?UTF-8?q?refactor:=20MSW=EB=A1=9C=20=EB=8F=99?= =?UTF-8?q?=EC=9E=91=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++- src/App.tsx | 9 ++--- src/api/auth.ts | 6 +-- src/api/hooks/auth.mock.ts | 11 ++++-- src/api/hooks/products.mock.ts | 2 +- src/api/hooks/useGetCategorys.ts | 4 +- src/api/hooks/useGetProductDetail.ts | 4 +- src/api/hooks/useGetProductOptions.ts | 4 +- src/api/hooks/useGetProducts.ts | 4 +- src/api/hooks/useGetWishlist.ts | 4 +- src/api/hooks/wishlist.mock.ts | 35 ++++++++++++++++- src/api/instance/index.ts | 22 +++++------ .../features/Goods/Detail/OptionSection.tsx | 8 ++-- src/components/features/Layout/Header.tsx | 21 ++++------ src/components/features/Wishlist/index.tsx | 5 +-- src/contexts/ApiContext.tsx | 38 ------------------- src/provider/Auth/index.tsx | 21 +++++++--- 17 files changed, 99 insertions(+), 104 deletions(-) delete mode 100644 src/contexts/ApiContext.tsx diff --git a/README.md b/README.md index c5b2b508b..6863a5252 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ FE 카카오 선물하기 6주차 과제: 배포 & 협업 - 팀 내에서 일관된 기준을 정하여 API 명세를 결정 - 때로는 클라이언트의 편의를 위해 API 명세를 결정하는 것이 좋음

-- [ ] 팀 내에 배포될 API가 여러 개일 경우 상단 내비게이션 바에서 선택 가능 +- [X] 팀 내에 배포될 API가 여러 개일 경우 상단 내비게이션 바에서 선택 가능 - 프론트엔드의 경우 사용자가 팀 내 여러 서버 중 하나를 선택하여 서비스를 이용 - [ ] 팀 내 백엔드 엔지니어의 이름을 넣고, 이름을 선택하면 해당 엔지니어의 API로 API 통신을 하도록 구현 - - [ ] 기본 선택은 제일 첫 번째 이름 \ No newline at end of file + - [ ] 기본 선택은 제일 첫 번째 이름 +- [X] 백엔드에서 협의된 API를 배포하기 전까지는 MSW로 동작 가능하도록 구현 \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 70f84fa85..24715e671 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,6 @@ import { ChakraProvider } from '@chakra-ui/react'; import { QueryClientProvider } from '@tanstack/react-query'; import { queryClient } from './api/instance'; -import { ApiProvider } from './contexts/ApiContext'; import { AuthProvider } from './provider/Auth'; import { Routes } from './routes'; @@ -10,11 +9,9 @@ const App = () => { return ( - - - - - + + + ); diff --git a/src/api/auth.ts b/src/api/auth.ts index 42791565a..df1601dca 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,7 +1,7 @@ -import { fetchInstance } from './instance'; +import { BASE_URL, fetchInstance } from './instance'; export const register = async (email: string, password: string) => { - const response = await fetchInstance.post('/api/members/register', { + const response = await fetchInstance.post(`${BASE_URL}/api/members/register`, { email, password, }); @@ -9,7 +9,7 @@ export const register = async (email: string, password: string) => { }; export const login = async (email: string, password: string) => { - const response = await fetchInstance.post('/api/members/login', { + const response = await fetchInstance.post(`${BASE_URL}/api/members/login`, { email, password, }); diff --git a/src/api/hooks/auth.mock.ts b/src/api/hooks/auth.mock.ts index 3126b9c98..dd3baaa39 100644 --- a/src/api/hooks/auth.mock.ts +++ b/src/api/hooks/auth.mock.ts @@ -1,19 +1,22 @@ import { rest } from 'msw'; +import { BASE_URL } from '../instance'; + const VALID_EMAIL = 'test@example.com'; const VALID_PASSWORD = 'password1234'; export const authMockHandler = [ - rest.post('https://api.example.com/api/members/register', async (_, res, ctx) => { - return res(ctx.status(201), ctx.text('User registered successfully')); + rest.post(`${BASE_URL}/api/members/register`, async (_, res, ctx) => { + alert('회원가입이 완료되었습니다.'); + return res(ctx.status(201), ctx.json({ message: 'User registered successfully' })); }), rest.post('https://api.example.com/api/members/login', async (req, res, ctx) => { const { email, password } = await req.json<{ email: string; password: string }>(); if (email === VALID_EMAIL && password === VALID_PASSWORD) { - return res(ctx.status(200), ctx.text('token')); + return res(ctx.status(200), ctx.json({ token: 'fake-token' })); } - return res(ctx.status(403), ctx.text('Invalid email or password')); + return res(ctx.status(403), ctx.json({ message: 'Invalid email or password' })); }), ]; diff --git a/src/api/hooks/products.mock.ts b/src/api/hooks/products.mock.ts index 7c20ca314..a475bc5e7 100644 --- a/src/api/hooks/products.mock.ts +++ b/src/api/hooks/products.mock.ts @@ -111,4 +111,4 @@ const PRODUCTS_MOCK_DATA = { first: true, numberOfElements: 5, empty: false, -}; +}; \ No newline at end of file diff --git a/src/api/hooks/useGetCategorys.ts b/src/api/hooks/useGetCategorys.ts index acb2b6997..d93e4fc95 100644 --- a/src/api/hooks/useGetCategorys.ts +++ b/src/api/hooks/useGetCategorys.ts @@ -2,11 +2,11 @@ import { useQuery } from '@tanstack/react-query'; import type { CategoryData } from '@/types'; -import { fetchInstance } from '../instance'; +import { BASE_URL, fetchInstance } from '../instance'; export type CategoryResponseData = CategoryData[]; -export const getCategoriesPath = () => `/api/categories`; +export const getCategoriesPath = () => `${BASE_URL}/api/categories`; const categoriesQueryKey = [getCategoriesPath()]; export const getCategories = async () => { diff --git a/src/api/hooks/useGetProductDetail.ts b/src/api/hooks/useGetProductDetail.ts index 5d0e92df9..539de0196 100644 --- a/src/api/hooks/useGetProductDetail.ts +++ b/src/api/hooks/useGetProductDetail.ts @@ -2,7 +2,7 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import type { ProductData } from '@/types'; -import { fetchInstance } from '../instance'; +import { BASE_URL, fetchInstance } from '../instance'; export type ProductDetailRequestParams = { productId: string; @@ -12,7 +12,7 @@ type Props = ProductDetailRequestParams; export type GoodsDetailResponseData = ProductData; -export const getProductDetailPath = (productId: string) => `/api/products/${productId}`; +export const getProductDetailPath = (productId: string) => `${BASE_URL}/api/products/${productId}`; export const getProductDetail = async (params: ProductDetailRequestParams) => { const response = await fetchInstance.get( diff --git a/src/api/hooks/useGetProductOptions.ts b/src/api/hooks/useGetProductOptions.ts index 02689b8a9..a3bdc538f 100644 --- a/src/api/hooks/useGetProductOptions.ts +++ b/src/api/hooks/useGetProductOptions.ts @@ -2,7 +2,7 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import type { ProductOptionsData } from '@/types'; -import { fetchInstance } from '../instance'; +import { BASE_URL, fetchInstance } from '../instance'; import type { ProductDetailRequestParams } from './useGetProductDetail'; type Props = ProductDetailRequestParams; @@ -10,7 +10,7 @@ type Props = ProductDetailRequestParams; export type ProductOptionsResponseData = ProductOptionsData[]; export const getProductOptionsPath = (productId: string) => - `/api/products/${productId}/options`; + `${BASE_URL}/api/products/${productId}/options`; export const getProductOptions = async (params: ProductDetailRequestParams) => { const response = await fetchInstance.get( diff --git a/src/api/hooks/useGetProducts.ts b/src/api/hooks/useGetProducts.ts index d917d43fe..893e5fd14 100644 --- a/src/api/hooks/useGetProducts.ts +++ b/src/api/hooks/useGetProducts.ts @@ -6,7 +6,7 @@ import { import type { ProductData } from '@/types'; -import { fetchInstance } from './../instance/index'; +import { BASE_URL, fetchInstance } from './../instance/index'; type RequestParams = { categoryId: string; @@ -60,7 +60,7 @@ export const getProductsPath = ({ categoryId, pageToken, maxResults }: RequestPa if (pageToken) params.append('page', pageToken); if (maxResults) params.append('size', maxResults.toString()); - return `/api/products?${params.toString()}`; + return `${BASE_URL}/api/products?${params.toString()}`; }; export const getProducts = async (params: RequestParams): Promise => { diff --git a/src/api/hooks/useGetWishlist.ts b/src/api/hooks/useGetWishlist.ts index 425851624..4bdae84f2 100644 --- a/src/api/hooks/useGetWishlist.ts +++ b/src/api/hooks/useGetWishlist.ts @@ -6,6 +6,8 @@ import { import { useAuth } from '@/provider/Auth'; +import { BASE_URL } from '../instance'; + type WishlistItem = { id: number; name: string; @@ -47,7 +49,7 @@ const getWishlistPath = ({ pageToken, maxResults }: RequestParams) => { if (pageToken) params.append('page', pageToken); if (maxResults) params.append('size', maxResults.toString()); - return `/api/wishes?${params.toString()}`; + return `${BASE_URL}/api/wishes?${params.toString()}`; }; export const getWishlist = async (params: RequestParams, token: string): Promise => { diff --git a/src/api/hooks/wishlist.mock.ts b/src/api/hooks/wishlist.mock.ts index 5b98b9682..12d05b3e3 100644 --- a/src/api/hooks/wishlist.mock.ts +++ b/src/api/hooks/wishlist.mock.ts @@ -1,5 +1,7 @@ import { rest } from 'msw'; +import { BASE_URL } from '../instance'; + interface WishlistItem { id: number; product: { @@ -17,7 +19,7 @@ const getPagedData = (page: number, size: number) => { }; export const wishlistMockHandler = [ - rest.get('/api/wishes', async (req, res, ctx) => { + rest.get(`${BASE_URL}/api/wishes`, async (req, res, ctx) => { const token = req.headers.get('Authorization'); if (!token) { @@ -63,7 +65,7 @@ export const wishlistMockHandler = [ }) ); }), - rest.delete('/api/wishes/:wishId', (req, res, ctx) => { + rest.delete(`${BASE_URL}/api/wishes/:wishId`, (req, res, ctx) => { const { wishId } = req.params; const wishIndex = WISHLIST_MOCK_DATA.content.findIndex((item) => item.id === Number(wishId)); if (wishIndex !== -1) { @@ -72,6 +74,35 @@ export const wishlistMockHandler = [ } return res(ctx.status(404), ctx.json({ message: 'Wish not found' })); }), + rest.post(`${BASE_URL}/api/wishes/:productId`, (req, res, ctx) => { + const { productId } = req.params; + const token = req.headers.get('Authorization'); + + if (!token) { + return res(ctx.status(401), ctx.json({ message: 'Invalid or missing token' })); + } + + const existingWish = WISHLIST_MOCK_DATA.content.find((item) => item.product.id === Number(productId)); + + if (existingWish) { + return res(ctx.status(400), ctx.json({ message: 'Product already in wishlist' })); + } + + const newProduct = { + id: WISHLIST_MOCK_DATA.content.length + 1, + product: { + id: Number(productId), + name: `Product ${String.fromCharCode(65 + WISHLIST_MOCK_DATA.content.length)}`, + price: 150, + imageUrl: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292020231106_MXMUB.png', + }, + }; + + WISHLIST_MOCK_DATA.content.push(newProduct); + + return res(ctx.status(201), ctx.json(newProduct)); + }), ]; interface WishlistMockData { diff --git a/src/api/instance/index.ts b/src/api/instance/index.ts index 80c150d5b..73c0075d4 100644 --- a/src/api/instance/index.ts +++ b/src/api/instance/index.ts @@ -1,28 +1,26 @@ import { QueryClient } from '@tanstack/react-query'; +import type { AxiosInstance, AxiosRequestConfig } from 'axios'; import axios from 'axios'; -const initInstance = () => { +const initInstance = (config: AxiosRequestConfig): AxiosInstance => { const instance = axios.create({ timeout: 5000, + ...config, headers: { Accept: 'application/json', 'Content-Type': 'application/json', + ...config.headers, }, }); - instance.interceptors.request.use((config) => { - const apiUrl = localStorage.getItem('apiUrl') || 'https://example.com'; - config.baseURL = apiUrl; - - return config; - }, (error) => { - return Promise.reject(error); - }); - return instance; }; -export const fetchInstance = initInstance(); +export const BASE_URL = 'https://api.example.com'; +// TODO: 추후 서버 API 주소 변경 필요 +export const fetchInstance = initInstance({ + baseURL: 'https://api.example.com', +}); export const queryClient = new QueryClient({ defaultOptions: { @@ -33,4 +31,4 @@ export const queryClient = new QueryClient({ refetchOnWindowFocus: true, }, }, -}); +}); \ No newline at end of file diff --git a/src/components/features/Goods/Detail/OptionSection.tsx b/src/components/features/Goods/Detail/OptionSection.tsx index 1cf249acb..e228f1475 100644 --- a/src/components/features/Goods/Detail/OptionSection.tsx +++ b/src/components/features/Goods/Detail/OptionSection.tsx @@ -7,8 +7,8 @@ import { useGetProductDetail, } from '@/api/hooks/useGetProductDetail'; import { useGetProductOptions } from '@/api/hooks/useGetProductOptions'; +import { BASE_URL } from '@/api/instance'; import { Button } from '@/components/common/Button'; -import { useApi } from '@/contexts/ApiContext'; import { useAuth } from '@/provider/Auth'; import { getDynamicPath, RouterPath } from '@/routes/path'; import { orderHistorySessionStorage } from '@/utils/storage'; @@ -20,8 +20,6 @@ type Props = ProductDetailRequestParams; export const OptionSection = ({ productId }: Props) => { const { data: detail } = useGetProductDetail({ productId }); const { data: options } = useGetProductOptions({ productId }); - const { apiUrl } = useApi(); - const [countAsString, setCountAsString] = useState('1'); const totalPrice = useMemo(() => { return detail.price * Number(countAsString); @@ -48,11 +46,13 @@ export const OptionSection = ({ productId }: Props) => { }; const handleAddToWishlist = async () => { + const token = sessionStorage.getItem('token'); try { - await fetch(`${apiUrl}/api/wishes'`, { + await fetch(`${BASE_URL}/api/wishes/${productId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, }, body: JSON.stringify({ productId }), }); diff --git a/src/components/features/Layout/Header.tsx b/src/components/features/Layout/Header.tsx index 93fea2785..60366c69a 100644 --- a/src/components/features/Layout/Header.tsx +++ b/src/components/features/Layout/Header.tsx @@ -2,33 +2,26 @@ import styled from '@emotion/styled'; import { Link, useNavigate } from 'react-router-dom'; import { Container } from '@/components/common/layouts/Container'; -import { useApi } from '@/contexts/ApiContext'; import { useAuth } from '@/provider/Auth'; import { getDynamicPath, RouterPath } from '@/routes/path'; const backend: { [key: string]: string } = { - backend1: 'https://example.com', - backend2: 'https://example.com', - backend3: 'https://example.com', - backend4: 'https://example.com', - backend5: 'https://example.com', - backend6: 'https://example.com', + backend1: 'https://api.example.com', + backend2: 'https://api.example.com', + backend3: 'https://api.example.com', + backend4: 'https://api.example.com', + backend5: 'https://api.example.com', + backend6: 'https://api.example.com', }; export const Header = () => { const navigate = useNavigate(); const authInfo = useAuth(); - const { apiUrl, setApiUrl } = useApi(); const handleLogin = () => { navigate(getDynamicPath.login()); }; - const handleApiChange = (event: React.ChangeEvent) => { - console.log('clicked: ', event.target.value); - setApiUrl(event.target.value); - }; - return ( @@ -39,7 +32,7 @@ export const Header = () => { /> - + diff --git a/src/components/features/Wishlist/index.tsx b/src/components/features/Wishlist/index.tsx index 01a335850..40b871bd4 100644 --- a/src/components/features/Wishlist/index.tsx +++ b/src/components/features/Wishlist/index.tsx @@ -2,8 +2,8 @@ import { Box, Button, Heading, List, ListItem, Text } from '@chakra-ui/react'; import { useEffect, useState } from 'react'; import { useGetWishlist } from '@/api/hooks/useGetWishlist'; +import { BASE_URL } from '@/api/instance'; import { VisibilityLoader } from '@/components/common/VisibilityLoader'; -import { useApi } from '@/contexts/ApiContext'; import { useAuth } from '@/provider/Auth'; type WishlistItem = { @@ -16,7 +16,6 @@ type WishlistItem = { export const Wishlist = () => { const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useGetWishlist({ maxResults: 10 }); const authInfo = useAuth(); - const { apiUrl } = useApi(); const [wishlist, setWishlist] = useState([]); useEffect(() => { @@ -28,7 +27,7 @@ export const Wishlist = () => { const handleDelete = async (wishId: number) => { if (authInfo?.token) { - const response = await fetch(`${apiUrl}/api/wishes/${wishId}`, { + const response = await fetch(`${BASE_URL}/api/wishes/${wishId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${authInfo.token}`, diff --git a/src/contexts/ApiContext.tsx b/src/contexts/ApiContext.tsx deleted file mode 100644 index 753110dbb..000000000 --- a/src/contexts/ApiContext.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useQueryClient } from '@tanstack/react-query'; -import type { ReactNode} from 'react'; -import { createContext,useContext, useEffect, useState } from 'react'; - - -interface ApiContextType { - apiUrl: string; - setApiUrl: (url: string) => void; -} - -const defaultState: ApiContextType = { - apiUrl: 'https://example.com', - setApiUrl: () => {} -}; - -const ApiContext = createContext(defaultState); - -interface ApiProviderProps { - children: ReactNode; -} - -export const ApiProvider = ({ children }: ApiProviderProps) => { - const [apiUrl, setApiUrl] = useState('https://example.com'); - const queryClient = useQueryClient(); - - useEffect(() => { - localStorage.setItem('apiUrl', apiUrl); - queryClient.invalidateQueries(); - }, [apiUrl, queryClient]); - - return ( - - {children} - - ); -}; - -export const useApi = () => useContext(ApiContext); diff --git a/src/provider/Auth/index.tsx b/src/provider/Auth/index.tsx index 06c921c31..d96bdf4ee 100644 --- a/src/provider/Auth/index.tsx +++ b/src/provider/Auth/index.tsx @@ -15,12 +15,21 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const currentAuthEmail = sessionStorage.getItem('authEmail'); const currentAuthToken = authSessionStorage.get(); - const [isReady, setIsReady] = useState(!currentAuthToken); - - const [authInfo, setAuthInfo] = useState(undefined); + const [isReady, setIsReady] = useState(false); + const [authInfo, setAuthInfo] = useState(() => { + if (currentAuthEmail && currentAuthToken) { + const name = currentAuthEmail.split('@')[0]; + return { + email: currentAuthEmail, + name: name, + token: currentAuthToken, + }; + } + return undefined; + }); useEffect(() => { - if (currentAuthEmail && currentAuthToken) { + if (currentAuthEmail && currentAuthToken && !authInfo) { const name = currentAuthEmail.split('@')[0]; setAuthInfo({ @@ -28,9 +37,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { name: name, token: currentAuthToken, }); - setIsReady(true); } - }, [currentAuthEmail, currentAuthToken]); + setIsReady(true); + }, [currentAuthEmail, currentAuthToken, authInfo]); if (!isReady) return <>; return {children}; From 64d37dedccee72e57043cb017a65dc80c7c961b3 Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Fri, 2 Aug 2024 20:39:52 +0900 Subject: [PATCH 13/26] =?UTF-8?q?style:=20=EC=BD=94=EB=93=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=ED=8C=85=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/hooks/products.mock.ts | 2 +- src/api/hooks/useGetWishlist.ts | 10 ++++++++-- src/api/hooks/wishlist.mock.ts | 6 ++++-- src/api/instance/index.ts | 2 +- src/components/features/Goods/Detail/OptionSection.tsx | 2 +- src/components/features/Wishlist/index.tsx | 8 +++++--- 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/api/hooks/products.mock.ts b/src/api/hooks/products.mock.ts index a475bc5e7..7c20ca314 100644 --- a/src/api/hooks/products.mock.ts +++ b/src/api/hooks/products.mock.ts @@ -111,4 +111,4 @@ const PRODUCTS_MOCK_DATA = { first: true, numberOfElements: 5, empty: false, -}; \ No newline at end of file +}; diff --git a/src/api/hooks/useGetWishlist.ts b/src/api/hooks/useGetWishlist.ts index 4bdae84f2..32e18f48b 100644 --- a/src/api/hooks/useGetWishlist.ts +++ b/src/api/hooks/useGetWishlist.ts @@ -52,7 +52,10 @@ const getWishlistPath = ({ pageToken, maxResults }: RequestParams) => { return `${BASE_URL}/api/wishes?${params.toString()}`; }; -export const getWishlist = async (params: RequestParams, token: string): Promise => { +export const getWishlist = async ( + params: RequestParams, + token: string, +): Promise => { const url = getWishlistPath(params); const response = await fetch(url, { @@ -70,7 +73,10 @@ export const getWishlist = async (params: RequestParams, token: string): Promise export const useGetWishlist = ({ maxResults = 10, initPageToken, -}: { maxResults?: number; initPageToken?: string }): UseInfiniteQueryResult> => { +}: { + maxResults?: number; + initPageToken?: string; +}): UseInfiniteQueryResult> => { const authInfo = useAuth(); return useInfiniteQuery({ diff --git a/src/api/hooks/wishlist.mock.ts b/src/api/hooks/wishlist.mock.ts index 12d05b3e3..80f05f8d3 100644 --- a/src/api/hooks/wishlist.mock.ts +++ b/src/api/hooks/wishlist.mock.ts @@ -62,7 +62,7 @@ export const wishlistMockHandler = [ numberOfElements: pagedContent.length, first: page === 0, empty: pagedContent.length === 0, - }) + }), ); }), rest.delete(`${BASE_URL}/api/wishes/:wishId`, (req, res, ctx) => { @@ -82,7 +82,9 @@ export const wishlistMockHandler = [ return res(ctx.status(401), ctx.json({ message: 'Invalid or missing token' })); } - const existingWish = WISHLIST_MOCK_DATA.content.find((item) => item.product.id === Number(productId)); + const existingWish = WISHLIST_MOCK_DATA.content.find( + (item) => item.product.id === Number(productId), + ); if (existingWish) { return res(ctx.status(400), ctx.json({ message: 'Product already in wishlist' })); diff --git a/src/api/instance/index.ts b/src/api/instance/index.ts index 73c0075d4..b83ca1407 100644 --- a/src/api/instance/index.ts +++ b/src/api/instance/index.ts @@ -31,4 +31,4 @@ export const queryClient = new QueryClient({ refetchOnWindowFocus: true, }, }, -}); \ No newline at end of file +}); diff --git a/src/components/features/Goods/Detail/OptionSection.tsx b/src/components/features/Goods/Detail/OptionSection.tsx index e228f1475..7a58e5de7 100644 --- a/src/components/features/Goods/Detail/OptionSection.tsx +++ b/src/components/features/Goods/Detail/OptionSection.tsx @@ -46,7 +46,7 @@ export const OptionSection = ({ productId }: Props) => { }; const handleAddToWishlist = async () => { - const token = sessionStorage.getItem('token'); + const token = sessionStorage.getItem('token'); try { await fetch(`${BASE_URL}/api/wishes/${productId}`, { method: 'POST', diff --git a/src/components/features/Wishlist/index.tsx b/src/components/features/Wishlist/index.tsx index 40b871bd4..b429b3fce 100644 --- a/src/components/features/Wishlist/index.tsx +++ b/src/components/features/Wishlist/index.tsx @@ -14,7 +14,9 @@ type WishlistItem = { }; export const Wishlist = () => { - const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useGetWishlist({ maxResults: 10 }); + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useGetWishlist({ + maxResults: 10, + }); const authInfo = useAuth(); const [wishlist, setWishlist] = useState([]); @@ -51,7 +53,7 @@ export const Wishlist = () => { 관심 상품이 없습니다. ) : ( - {wishlist.map((item) => + {wishlist.map((item) => ( @@ -66,7 +68,7 @@ export const Wishlist = () => { - )} + ))} )} {hasNextPage && ( From bc4cfd852a918f01f033e90a46fe38314c4ba976 Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Sat, 3 Aug 2024 11:49:55 +0900 Subject: [PATCH 14/26] =?UTF-8?q?feat:=20=EB=B0=B1=EC=97=94=EB=93=9C=20API?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 9 ++-- src/api/auth.ts | 6 +-- src/api/hooks/auth.mock.ts | 4 +- src/api/hooks/useGetCategorys.ts | 4 +- src/api/hooks/useGetProductDetail.ts | 4 +- src/api/hooks/useGetProductOptions.ts | 4 +- src/api/hooks/useGetProducts.ts | 4 +- src/api/hooks/useGetWishlist.ts | 4 +- src/api/hooks/wishlist.mock.ts | 8 ++-- src/api/instance/index.ts | 23 +++++++---- .../features/Goods/Detail/OptionSection.tsx | 5 ++- src/components/features/Layout/Header.tsx | 25 ++++++----- src/components/features/Wishlist/index.tsx | 5 ++- src/config/backendConfig.ts | 8 ++++ src/provider/Api/index.tsx | 41 +++++++++++++++++++ 15 files changed, 103 insertions(+), 51 deletions(-) create mode 100644 src/config/backendConfig.ts create mode 100644 src/provider/Api/index.tsx diff --git a/src/App.tsx b/src/App.tsx index 24715e671..45491ffb1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { ChakraProvider } from '@chakra-ui/react'; import { QueryClientProvider } from '@tanstack/react-query'; import { queryClient } from './api/instance'; +import { ApiProvider } from './provider/Api'; import { AuthProvider } from './provider/Auth'; import { Routes } from './routes'; @@ -9,9 +10,11 @@ const App = () => { return ( - - - + + + + + ); diff --git a/src/api/auth.ts b/src/api/auth.ts index df1601dca..b945f1d2a 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,7 +1,7 @@ -import { BASE_URL, fetchInstance } from './instance'; +import { fetchInstance } from './instance'; export const register = async (email: string, password: string) => { - const response = await fetchInstance.post(`${BASE_URL}/api/members/register`, { + const response = await fetchInstance.post(`/api/members/register`, { email, password, }); @@ -9,7 +9,7 @@ export const register = async (email: string, password: string) => { }; export const login = async (email: string, password: string) => { - const response = await fetchInstance.post(`${BASE_URL}/api/members/login`, { + const response = await fetchInstance.post(`/api/members/login`, { email, password, }); diff --git a/src/api/hooks/auth.mock.ts b/src/api/hooks/auth.mock.ts index dd3baaa39..c6125173a 100644 --- a/src/api/hooks/auth.mock.ts +++ b/src/api/hooks/auth.mock.ts @@ -1,12 +1,10 @@ import { rest } from 'msw'; -import { BASE_URL } from '../instance'; - const VALID_EMAIL = 'test@example.com'; const VALID_PASSWORD = 'password1234'; export const authMockHandler = [ - rest.post(`${BASE_URL}/api/members/register`, async (_, res, ctx) => { + rest.post(`/api/members/register`, async (_, res, ctx) => { alert('회원가입이 완료되었습니다.'); return res(ctx.status(201), ctx.json({ message: 'User registered successfully' })); }), diff --git a/src/api/hooks/useGetCategorys.ts b/src/api/hooks/useGetCategorys.ts index d93e4fc95..acb2b6997 100644 --- a/src/api/hooks/useGetCategorys.ts +++ b/src/api/hooks/useGetCategorys.ts @@ -2,11 +2,11 @@ import { useQuery } from '@tanstack/react-query'; import type { CategoryData } from '@/types'; -import { BASE_URL, fetchInstance } from '../instance'; +import { fetchInstance } from '../instance'; export type CategoryResponseData = CategoryData[]; -export const getCategoriesPath = () => `${BASE_URL}/api/categories`; +export const getCategoriesPath = () => `/api/categories`; const categoriesQueryKey = [getCategoriesPath()]; export const getCategories = async () => { diff --git a/src/api/hooks/useGetProductDetail.ts b/src/api/hooks/useGetProductDetail.ts index 539de0196..5d0e92df9 100644 --- a/src/api/hooks/useGetProductDetail.ts +++ b/src/api/hooks/useGetProductDetail.ts @@ -2,7 +2,7 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import type { ProductData } from '@/types'; -import { BASE_URL, fetchInstance } from '../instance'; +import { fetchInstance } from '../instance'; export type ProductDetailRequestParams = { productId: string; @@ -12,7 +12,7 @@ type Props = ProductDetailRequestParams; export type GoodsDetailResponseData = ProductData; -export const getProductDetailPath = (productId: string) => `${BASE_URL}/api/products/${productId}`; +export const getProductDetailPath = (productId: string) => `/api/products/${productId}`; export const getProductDetail = async (params: ProductDetailRequestParams) => { const response = await fetchInstance.get( diff --git a/src/api/hooks/useGetProductOptions.ts b/src/api/hooks/useGetProductOptions.ts index a3bdc538f..02689b8a9 100644 --- a/src/api/hooks/useGetProductOptions.ts +++ b/src/api/hooks/useGetProductOptions.ts @@ -2,7 +2,7 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import type { ProductOptionsData } from '@/types'; -import { BASE_URL, fetchInstance } from '../instance'; +import { fetchInstance } from '../instance'; import type { ProductDetailRequestParams } from './useGetProductDetail'; type Props = ProductDetailRequestParams; @@ -10,7 +10,7 @@ type Props = ProductDetailRequestParams; export type ProductOptionsResponseData = ProductOptionsData[]; export const getProductOptionsPath = (productId: string) => - `${BASE_URL}/api/products/${productId}/options`; + `/api/products/${productId}/options`; export const getProductOptions = async (params: ProductDetailRequestParams) => { const response = await fetchInstance.get( diff --git a/src/api/hooks/useGetProducts.ts b/src/api/hooks/useGetProducts.ts index 893e5fd14..d917d43fe 100644 --- a/src/api/hooks/useGetProducts.ts +++ b/src/api/hooks/useGetProducts.ts @@ -6,7 +6,7 @@ import { import type { ProductData } from '@/types'; -import { BASE_URL, fetchInstance } from './../instance/index'; +import { fetchInstance } from './../instance/index'; type RequestParams = { categoryId: string; @@ -60,7 +60,7 @@ export const getProductsPath = ({ categoryId, pageToken, maxResults }: RequestPa if (pageToken) params.append('page', pageToken); if (maxResults) params.append('size', maxResults.toString()); - return `${BASE_URL}/api/products?${params.toString()}`; + return `/api/products?${params.toString()}`; }; export const getProducts = async (params: RequestParams): Promise => { diff --git a/src/api/hooks/useGetWishlist.ts b/src/api/hooks/useGetWishlist.ts index 32e18f48b..3876d0acf 100644 --- a/src/api/hooks/useGetWishlist.ts +++ b/src/api/hooks/useGetWishlist.ts @@ -6,8 +6,6 @@ import { import { useAuth } from '@/provider/Auth'; -import { BASE_URL } from '../instance'; - type WishlistItem = { id: number; name: string; @@ -49,7 +47,7 @@ const getWishlistPath = ({ pageToken, maxResults }: RequestParams) => { if (pageToken) params.append('page', pageToken); if (maxResults) params.append('size', maxResults.toString()); - return `${BASE_URL}/api/wishes?${params.toString()}`; + return `/api/wishes?${params.toString()}`; }; export const getWishlist = async ( diff --git a/src/api/hooks/wishlist.mock.ts b/src/api/hooks/wishlist.mock.ts index 80f05f8d3..dd9f5fcfe 100644 --- a/src/api/hooks/wishlist.mock.ts +++ b/src/api/hooks/wishlist.mock.ts @@ -1,7 +1,5 @@ import { rest } from 'msw'; -import { BASE_URL } from '../instance'; - interface WishlistItem { id: number; product: { @@ -19,7 +17,7 @@ const getPagedData = (page: number, size: number) => { }; export const wishlistMockHandler = [ - rest.get(`${BASE_URL}/api/wishes`, async (req, res, ctx) => { + rest.get(`/api/wishes`, async (req, res, ctx) => { const token = req.headers.get('Authorization'); if (!token) { @@ -65,7 +63,7 @@ export const wishlistMockHandler = [ }), ); }), - rest.delete(`${BASE_URL}/api/wishes/:wishId`, (req, res, ctx) => { + rest.delete(`/api/wishes/:wishId`, (req, res, ctx) => { const { wishId } = req.params; const wishIndex = WISHLIST_MOCK_DATA.content.findIndex((item) => item.id === Number(wishId)); if (wishIndex !== -1) { @@ -74,7 +72,7 @@ export const wishlistMockHandler = [ } return res(ctx.status(404), ctx.json({ message: 'Wish not found' })); }), - rest.post(`${BASE_URL}/api/wishes/:productId`, (req, res, ctx) => { + rest.post(`/api/wishes/:productId`, (req, res, ctx) => { const { productId } = req.params; const token = req.headers.get('Authorization'); diff --git a/src/api/instance/index.ts b/src/api/instance/index.ts index b83ca1407..80203f13d 100644 --- a/src/api/instance/index.ts +++ b/src/api/instance/index.ts @@ -1,26 +1,31 @@ import { QueryClient } from '@tanstack/react-query'; -import type { AxiosInstance, AxiosRequestConfig } from 'axios'; import axios from 'axios'; -const initInstance = (config: AxiosRequestConfig): AxiosInstance => { +import { backend } from '@/config/backendConfig'; + + +const initInstance = () => { const instance = axios.create({ timeout: 5000, - ...config, headers: { Accept: 'application/json', 'Content-Type': 'application/json', - ...config.headers, }, }); + instance.interceptors.request.use((config) => { + const apiUrl = localStorage.getItem('apiUrl') || backend.backend3; + config.baseURL = apiUrl; + + return config; + }, (error) => { + return Promise.reject(error); + }); + return instance; }; -export const BASE_URL = 'https://api.example.com'; -// TODO: 추후 서버 API 주소 변경 필요 -export const fetchInstance = initInstance({ - baseURL: 'https://api.example.com', -}); +export const fetchInstance = initInstance(); export const queryClient = new QueryClient({ defaultOptions: { diff --git a/src/components/features/Goods/Detail/OptionSection.tsx b/src/components/features/Goods/Detail/OptionSection.tsx index 7a58e5de7..670b7d68e 100644 --- a/src/components/features/Goods/Detail/OptionSection.tsx +++ b/src/components/features/Goods/Detail/OptionSection.tsx @@ -7,8 +7,8 @@ import { useGetProductDetail, } from '@/api/hooks/useGetProductDetail'; import { useGetProductOptions } from '@/api/hooks/useGetProductOptions'; -import { BASE_URL } from '@/api/instance'; import { Button } from '@/components/common/Button'; +import { useApi } from '@/provider/Api'; import { useAuth } from '@/provider/Auth'; import { getDynamicPath, RouterPath } from '@/routes/path'; import { orderHistorySessionStorage } from '@/utils/storage'; @@ -20,6 +20,7 @@ type Props = ProductDetailRequestParams; export const OptionSection = ({ productId }: Props) => { const { data: detail } = useGetProductDetail({ productId }); const { data: options } = useGetProductOptions({ productId }); + const { apiUrl } = useApi(); const [countAsString, setCountAsString] = useState('1'); const totalPrice = useMemo(() => { return detail.price * Number(countAsString); @@ -48,7 +49,7 @@ export const OptionSection = ({ productId }: Props) => { const handleAddToWishlist = async () => { const token = sessionStorage.getItem('token'); try { - await fetch(`${BASE_URL}/api/wishes/${productId}`, { + await fetch(`${apiUrl}/api/wishes/${productId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/components/features/Layout/Header.tsx b/src/components/features/Layout/Header.tsx index 60366c69a..c892d325a 100644 --- a/src/components/features/Layout/Header.tsx +++ b/src/components/features/Layout/Header.tsx @@ -2,26 +2,25 @@ import styled from '@emotion/styled'; import { Link, useNavigate } from 'react-router-dom'; import { Container } from '@/components/common/layouts/Container'; +import { backend } from '@/config/backendConfig'; +import { useApi } from '@/provider/Api'; import { useAuth } from '@/provider/Auth'; import { getDynamicPath, RouterPath } from '@/routes/path'; -const backend: { [key: string]: string } = { - backend1: 'https://api.example.com', - backend2: 'https://api.example.com', - backend3: 'https://api.example.com', - backend4: 'https://api.example.com', - backend5: 'https://api.example.com', - backend6: 'https://api.example.com', -}; - export const Header = () => { const navigate = useNavigate(); const authInfo = useAuth(); + const { apiUrl, setApiUrl } = useApi(); const handleLogin = () => { navigate(getDynamicPath.login()); }; + const handleApiChange = (event: React.ChangeEvent) => { + console.log('clicked: ', event.target.value); + setApiUrl(event.target.value); + }; + return ( @@ -32,13 +31,13 @@ export const Header = () => { /> - - - + + {/* */} + {/* */} - + {/* */} {authInfo ? ( navigate(RouterPath.myAccount)}>내 계정 diff --git a/src/components/features/Wishlist/index.tsx b/src/components/features/Wishlist/index.tsx index b429b3fce..5b1e46d55 100644 --- a/src/components/features/Wishlist/index.tsx +++ b/src/components/features/Wishlist/index.tsx @@ -2,8 +2,8 @@ import { Box, Button, Heading, List, ListItem, Text } from '@chakra-ui/react'; import { useEffect, useState } from 'react'; import { useGetWishlist } from '@/api/hooks/useGetWishlist'; -import { BASE_URL } from '@/api/instance'; import { VisibilityLoader } from '@/components/common/VisibilityLoader'; +import { useApi } from '@/provider/Api'; import { useAuth } from '@/provider/Auth'; type WishlistItem = { @@ -18,6 +18,7 @@ export const Wishlist = () => { maxResults: 10, }); const authInfo = useAuth(); + const { apiUrl } = useApi(); const [wishlist, setWishlist] = useState([]); useEffect(() => { @@ -29,7 +30,7 @@ export const Wishlist = () => { const handleDelete = async (wishId: number) => { if (authInfo?.token) { - const response = await fetch(`${BASE_URL}/api/wishes/${wishId}`, { + const response = await fetch(`${apiUrl}/api/wishes/${wishId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${authInfo.token}`, diff --git a/src/config/backendConfig.ts b/src/config/backendConfig.ts new file mode 100644 index 000000000..3dd989330 --- /dev/null +++ b/src/config/backendConfig.ts @@ -0,0 +1,8 @@ +export const backend: { [key: string]: string } = { + backend1: 'https://api.example.com', + backend2: 'https://api.example.com', + backend3: 'http://52.78.23.209:8080/', + backend4: 'http://54.180.228.132:8080/', + backend5: 'http://43.202.41.105:8080/', + backend6: 'https://api.example.com', +}; \ No newline at end of file diff --git a/src/provider/Api/index.tsx b/src/provider/Api/index.tsx new file mode 100644 index 000000000..078242d4a --- /dev/null +++ b/src/provider/Api/index.tsx @@ -0,0 +1,41 @@ +import { useQueryClient } from '@tanstack/react-query'; +import type { ReactNode} from 'react'; +import { createContext,useContext, useEffect, useState } from 'react'; + +import { backend } from '@/config/backendConfig'; + +interface ApiContextType { + apiUrl: string; + setApiUrl: (url: string) => void; +} + +const defaultState: ApiContextType = { + apiUrl: backend.backend3, + setApiUrl: () => {} +}; + +const ApiContext = createContext(defaultState); + +interface ApiProviderProps { + children: ReactNode; +} + +export const ApiProvider = ({ children }: ApiProviderProps) => { + const [apiUrl, setApiUrl] = useState( + () => localStorage.getItem('apiUrl') || backend.backend3 + ); + const queryClient = useQueryClient(); + + useEffect(() => { + localStorage.setItem('apiUrl', apiUrl); + queryClient.invalidateQueries(); + }, [apiUrl, queryClient]); + + return ( + + {children} + + ); +}; + +export const useApi = () => useContext(ApiContext); \ No newline at end of file From 2a3d42cdc47775fa3d7827280d55b797c5c59a45 Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Sat, 3 Aug 2024 15:16:09 +0900 Subject: [PATCH 15/26] =?UTF-8?q?fix:=20=ED=86=A0=ED=81=B0=EC=9D=B4=20obje?= =?UTF-8?q?ct=20=ED=83=80=EC=9E=85=EC=9C=BC=EB=A1=9C=20=EC=A0=84=EB=8B=AC?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/hooks/useGetWishlist.ts | 13 ++++++---- .../features/Goods/Detail/OptionSection.tsx | 26 ++++++++++++------- src/components/features/Wishlist/index.tsx | 5 ++-- src/config/backendConfig.ts | 2 +- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/api/hooks/useGetWishlist.ts b/src/api/hooks/useGetWishlist.ts index 3876d0acf..8d23f0132 100644 --- a/src/api/hooks/useGetWishlist.ts +++ b/src/api/hooks/useGetWishlist.ts @@ -4,6 +4,7 @@ import { type UseInfiniteQueryResult, } from '@tanstack/react-query'; +import { useApi } from '@/provider/Api'; import { useAuth } from '@/provider/Auth'; type WishlistItem = { @@ -41,26 +42,27 @@ type RequestParams = { maxResults?: number; }; -const getWishlistPath = ({ pageToken, maxResults }: RequestParams) => { +export const getWishlistPath = ({ pageToken, maxResults }: RequestParams, apiUrl: string) => { const params = new URLSearchParams(); params.append('sort', 'createdDate,desc'); if (pageToken) params.append('page', pageToken); if (maxResults) params.append('size', maxResults.toString()); - return `/api/wishes?${params.toString()}`; + return `${apiUrl}api/wishes?${params.toString()}`; }; export const getWishlist = async ( params: RequestParams, token: string, + apiUrl: string ): Promise => { - const url = getWishlistPath(params); + const url = getWishlistPath(params, apiUrl); const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, + 'Authorization': `Bearer ${JSON.parse(token).token}`, }, }); @@ -76,6 +78,7 @@ export const useGetWishlist = ({ initPageToken?: string; }): UseInfiniteQueryResult> => { const authInfo = useAuth(); + const { apiUrl } = useApi(); return useInfiniteQuery({ queryKey: ['wishlist', maxResults, initPageToken], @@ -83,7 +86,7 @@ export const useGetWishlist = ({ if (!authInfo?.token) { throw new Error('Authentication token is missing'); } - return getWishlist({ pageToken: pageParam, maxResults }, authInfo.token); + return getWishlist({ pageToken: pageParam, maxResults }, authInfo.token, apiUrl); }, initialPageParam: initPageToken, getNextPageParam: (lastPage) => (lastPage.last ? undefined : (lastPage.number + 1).toString()), diff --git a/src/components/features/Goods/Detail/OptionSection.tsx b/src/components/features/Goods/Detail/OptionSection.tsx index 670b7d68e..21deb82ad 100644 --- a/src/components/features/Goods/Detail/OptionSection.tsx +++ b/src/components/features/Goods/Detail/OptionSection.tsx @@ -47,17 +47,23 @@ export const OptionSection = ({ productId }: Props) => { }; const handleAddToWishlist = async () => { - const token = sessionStorage.getItem('token'); try { - await fetch(`${apiUrl}/api/wishes/${productId}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ productId }), - }); - alert('관심 등록 완료'); + const tokenString = sessionStorage.getItem('authToken'); + + if (tokenString) { + const token = JSON.parse(tokenString).token; + + await fetch(`${apiUrl}api/wishes/${productId}`, { + credentials: 'include', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ productId }), + }); + alert('관심 등록 완료'); + } } catch (error) { console.error(error); alert('관심 상품 등록에 실패했습니다.'); diff --git a/src/components/features/Wishlist/index.tsx b/src/components/features/Wishlist/index.tsx index 5b1e46d55..0d6179ef8 100644 --- a/src/components/features/Wishlist/index.tsx +++ b/src/components/features/Wishlist/index.tsx @@ -30,10 +30,11 @@ export const Wishlist = () => { const handleDelete = async (wishId: number) => { if (authInfo?.token) { - const response = await fetch(`${apiUrl}/api/wishes/${wishId}`, { + const token = JSON.parse(authInfo.token).token; + const response = await fetch(`${apiUrl}api/wishes/${wishId}`, { method: 'DELETE', headers: { - Authorization: `Bearer ${authInfo.token}`, + 'Authorization': `Bearer ${token}`, }, }); if (response.status === 204) { diff --git a/src/config/backendConfig.ts b/src/config/backendConfig.ts index 3dd989330..0a882bd8d 100644 --- a/src/config/backendConfig.ts +++ b/src/config/backendConfig.ts @@ -2,7 +2,7 @@ export const backend: { [key: string]: string } = { backend1: 'https://api.example.com', backend2: 'https://api.example.com', backend3: 'http://52.78.23.209:8080/', - backend4: 'http://54.180.228.132:8080/', + backend4: 'http://3.38.169.232:8080/', backend5: 'http://43.202.41.105:8080/', backend6: 'https://api.example.com', }; \ No newline at end of file From 1d7a35f9913b4ecc9feb4f6287b24d4111f0d6da Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Sat, 3 Aug 2024 19:37:39 +0900 Subject: [PATCH 16/26] =?UTF-8?q?docs:=202=EB=8B=A8=EA=B3=84=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6863a5252..ac45b8356 100644 --- a/README.md +++ b/README.md @@ -9,4 +9,7 @@ FE 카카오 선물하기 6주차 과제: 배포 & 협업 - 프론트엔드의 경우 사용자가 팀 내 여러 서버 중 하나를 선택하여 서비스를 이용 - [ ] 팀 내 백엔드 엔지니어의 이름을 넣고, 이름을 선택하면 해당 엔지니어의 API로 API 통신을 하도록 구현 - [ ] 기본 선택은 제일 첫 번째 이름 -- [X] 백엔드에서 협의된 API를 배포하기 전까지는 MSW로 동작 가능하도록 구현 \ No newline at end of file +- [X] 백엔드에서 협의된 API를 배포하기 전까지는 MSW로 동작 가능하도록 구현 + +### 🌿 2단계 - 배포하기 +- github pages를 사용하여 배포 \ No newline at end of file From 0f335946806eafb67a6577229c52e867717a6c83 Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Sat, 3 Aug 2024 19:38:35 +0900 Subject: [PATCH 17/26] =?UTF-8?q?chore:=20gh-pages=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 248 ++++++++++++++++++++++++++++++++++++++++------ package.json | 8 +- 2 files changed, 221 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index 942797099..48a09e167 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "axios": "^1.6.7", "dotenv": "^16.4.5", "framer-motion": "^11.0.6", + "gh-pages": "^6.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.12", @@ -10950,6 +10951,15 @@ "node": ">=8" } }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array.prototype.filter": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.3.tgz", @@ -11127,8 +11137,7 @@ "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" }, "node_modules/async-limiter": { "version": "1.0.1", @@ -11835,8 +11844,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -12744,8 +12752,7 @@ "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" }, "node_modules/compressible": { "version": "2.0.18", @@ -12815,8 +12822,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/concat-stream": { "version": "1.6.2", @@ -14310,6 +14316,12 @@ "integrity": "sha512-1PpuqJUFWoXZ1E54m8bsLPVYwIVCRzvaL+n5cjigGga4z854abDnFRc+cTa2th4S79kyGqya/1xoR7h+Y5G5lg==", "dev": true }, + "node_modules/email-addresses": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz", + "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==", + "license": "MIT" + }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", @@ -16733,6 +16745,32 @@ "node": ">=10" } }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/filesize": { "version": "8.0.7", "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", @@ -16791,7 +16829,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", @@ -16808,7 +16845,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -16821,7 +16857,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -16833,7 +16868,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "dependencies": { "p-try": "^2.0.0" }, @@ -16848,7 +16882,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -16860,7 +16893,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, "dependencies": { "find-up": "^4.0.0" }, @@ -17248,7 +17280,6 @@ "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -17297,8 +17328,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -17488,6 +17518,108 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gh-pages": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.1.1.tgz", + "integrity": "sha512-upnohfjBwN5hBP9w2dPE7HO5JJTHzSGMV1JrLrHvNuqmjoYHg6TBrCcnEoorjG/e0ejbuvnwyKMdTyM40PEByw==", + "license": "MIT", + "dependencies": { + "async": "^3.2.4", + "commander": "^11.0.0", + "email-addresses": "^5.0.0", + "filenamify": "^4.3.0", + "find-cache-dir": "^3.3.1", + "fs-extra": "^11.1.1", + "globby": "^6.1.0" + }, + "bin": { + "gh-pages": "bin/gh-pages.js", + "gh-pages-clean": "bin/gh-pages-clean.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gh-pages/node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "license": "MIT", + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gh-pages/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/gh-pages/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/gh-pages/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gh-pages/node_modules/globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "license": "MIT", + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gh-pages/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/giget": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.1.tgz", @@ -17659,8 +17791,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -18368,7 +18499,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -18377,8 +18507,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "1.3.8", @@ -23204,7 +23333,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -23607,7 +23735,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, "dependencies": { "semver": "^6.0.0" }, @@ -24693,7 +24820,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -24899,7 +25025,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "engines": { "node": ">=6" } @@ -25003,7 +25128,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "engines": { "node": ">=8" } @@ -25012,7 +25136,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -25142,7 +25265,27 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "license": "MIT", + "dependencies": { + "pinkie": "^2.0.0" + }, "engines": { "node": ">=0.10.0" } @@ -30071,7 +30214,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -31063,6 +31205,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/style-inject": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz", @@ -31948,6 +32111,27 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/tryer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", @@ -32569,7 +32753,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "engines": { "node": ">= 10.0.0" } @@ -34045,8 +34228,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "4.0.2", diff --git a/package.json b/package.json index 55578040b..4998ab257 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "build": "craco build", "test": "craco test --transformIgnorePatterns \"node_modules/(?!axios)/\"", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "build-storybook": "storybook build", + "predeploy": "npm run build", + "deploy": "gh-pages -d build" }, "browserslist": { "production": [ @@ -31,6 +33,7 @@ "axios": "^1.6.7", "dotenv": "^16.4.5", "framer-motion": "^11.0.6", + "gh-pages": "^6.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.12", @@ -90,5 +93,6 @@ }, "overrides": { "react-refresh": "0.11.0" - } + }, + "homepage": "https://KimJi-An.github.io/react-deploy" } From d5c4692a909382303ac13a4adfa582113a646d1f Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Sat, 3 Aug 2024 19:57:50 +0900 Subject: [PATCH 18/26] =?UTF-8?q?docs:=203=EB=8B=A8=EA=B3=84=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ac45b8356..421f2034e 100644 --- a/README.md +++ b/README.md @@ -12,4 +12,15 @@ FE 카카오 선물하기 6주차 과제: 배포 & 협업 - [X] 백엔드에서 협의된 API를 배포하기 전까지는 MSW로 동작 가능하도록 구현 ### 🌿 2단계 - 배포하기 -- github pages를 사용하여 배포 \ No newline at end of file +- github pages를 사용하여 배포 + +### 🪴 3단계 - 포인트 +- 포인트 기능 구현 +- API 명세는 팀과 협의하여 결정하고 구현

+ + - **포인트 조회**
+ 해당하는 멤버의 포인트를 리턴
+ **URL** : /api/member/point
+ **Request** : Header: Authorization: Bearer {token}
+ **Response** : 200 OK { "point": "number" }
+ From 4ca6ececa9fc5168a710365be0b10c9adebdd778 Mon Sep 17 00:00:00 2001 From: KimJi-An Date: Sat, 3 Aug 2024 21:34:40 +0900 Subject: [PATCH 19/26] =?UTF-8?q?feat:=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- src/api/hooks/points.mock.ts | 15 +++++++++++++++ src/api/hooks/useGetPoints.ts | 27 +++++++++++++++++++++++++++ src/mocks/browser.ts | 2 ++ src/mocks/server.ts | 2 ++ src/pages/Login/index.tsx | 21 +++++++++++++++++++++ src/pages/MyAccount/index.tsx | 11 ++++++++++- 7 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 src/api/hooks/points.mock.ts create mode 100644 src/api/hooks/useGetPoints.ts diff --git a/README.md b/README.md index 421f2034e..398702ffb 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ FE 카카오 선물하기 6주차 과제: 배포 & 협업

- [X] 팀 내에 배포될 API가 여러 개일 경우 상단 내비게이션 바에서 선택 가능 - 프론트엔드의 경우 사용자가 팀 내 여러 서버 중 하나를 선택하여 서비스를 이용 - - [ ] 팀 내 백엔드 엔지니어의 이름을 넣고, 이름을 선택하면 해당 엔지니어의 API로 API 통신을 하도록 구현 - - [ ] 기본 선택은 제일 첫 번째 이름 + - [X] 팀 내 백엔드 엔지니어의 이름을 넣고, 이름을 선택하면 해당 엔지니어의 API로 API 통신을 하도록 구현 + - [X] 기본 선택은 제일 첫 번째 이름 - [X] 백엔드에서 협의된 API를 배포하기 전까지는 MSW로 동작 가능하도록 구현 ### 🌿 2단계 - 배포하기 diff --git a/src/api/hooks/points.mock.ts b/src/api/hooks/points.mock.ts new file mode 100644 index 000000000..244b181d9 --- /dev/null +++ b/src/api/hooks/points.mock.ts @@ -0,0 +1,15 @@ +import { rest } from 'msw'; + +export const pointMockHandlers = [ + rest.get('https://api.example.com/api/member/point', (req, res, ctx) => { + const authHeader = req.headers.get('Authorization'); + if (!authHeader) { + return res(ctx.status(401)); + } + + return res( + ctx.status(200), + ctx.json({ point: 1000 }) + ); + }), +]; \ No newline at end of file diff --git a/src/api/hooks/useGetPoints.ts b/src/api/hooks/useGetPoints.ts new file mode 100644 index 000000000..f48c4119c --- /dev/null +++ b/src/api/hooks/useGetPoints.ts @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react'; + +import { fetchInstance } from '../instance'; + +export const useGetPoints = () => { + const [points, setPoints] = useState(() => { + const savedPoints = sessionStorage.getItem('points'); + return savedPoints ? parseInt(savedPoints, 10) : 0; + }); + + useEffect(() => { + if (!sessionStorage.getItem('points')) { + const fetchPoints = async () => { + try { + const response = await fetchInstance.get('/api/member/point'); + setPoints(response.data.point); + sessionStorage.setItem('points', response.data.point); + } catch (error) { + console.error('Failed to fetch points', error); + } + }; + fetchPoints(); + } + }, []); + + return points; +}; \ No newline at end of file diff --git a/src/mocks/browser.ts b/src/mocks/browser.ts index d3454c161..8d06b1dd1 100644 --- a/src/mocks/browser.ts +++ b/src/mocks/browser.ts @@ -2,6 +2,7 @@ import { setupWorker } from 'msw'; import { authMockHandler } from '@/api/hooks/auth.mock'; import { categoriesMockHandler } from '@/api/hooks/categories.mock'; +import { pointMockHandlers } from '@/api/hooks/points.mock'; import { productsMockHandler } from '@/api/hooks/products.mock'; import { wishlistMockHandler } from '@/api/hooks/wishlist.mock'; @@ -10,4 +11,5 @@ export const worker = setupWorker( ...productsMockHandler, ...authMockHandler, ...wishlistMockHandler, + ...pointMockHandlers, ); diff --git a/src/mocks/server.ts b/src/mocks/server.ts index e4c6ea86a..3d22e3712 100644 --- a/src/mocks/server.ts +++ b/src/mocks/server.ts @@ -2,6 +2,7 @@ import { setupServer } from 'msw/node'; import { authMockHandler } from '@/api/hooks/auth.mock'; import { categoriesMockHandler } from '@/api/hooks/categories.mock'; +import { pointMockHandlers } from '@/api/hooks/points.mock'; import { productsMockHandler } from '@/api/hooks/products.mock'; import { wishlistMockHandler } from '@/api/hooks/wishlist.mock'; @@ -10,4 +11,5 @@ export const server = setupServer( ...productsMockHandler, ...authMockHandler, ...wishlistMockHandler, + ...pointMockHandlers, ); diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index b3108b394..75bfe0706 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -1,4 +1,5 @@ import styled from '@emotion/styled'; +import axios from 'axios'; import { useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; @@ -7,6 +8,7 @@ import KAKAO_LOGO from '@/assets/kakao_logo.svg'; import { Button } from '@/components/common/Button'; import { UnderlineTextField } from '@/components/common/Form/Input/UnderlineTextField'; import { Spacing } from '@/components/common/layouts/Spacing'; +import { useApi } from '@/provider/Api'; import { RouterPath } from '@/routes/path'; import { breakpoints } from '@/styles/variants'; import { authSessionStorage } from '@/utils/storage'; @@ -16,6 +18,7 @@ export const LoginPage = () => { const [password, setPassword] = useState(''); const [queryParams] = useSearchParams(); const navigate = useNavigate(); + const { apiUrl } = useApi(); const handleConfirm = async () => { if (!email || !password) { @@ -30,6 +33,9 @@ export const LoginPage = () => { sessionStorage.setItem('authToken', token); authSessionStorage.set(token); + const points = await fetchPoints(token); + sessionStorage.setItem('points', points.toString()); + const redirectUrl = queryParams.get('redirect') ?? `${window.location.origin}/`; window.location.replace(redirectUrl); } catch (error) { @@ -38,6 +44,21 @@ export const LoginPage = () => { } }; + const fetchPoints = async (token: string) => { + try { + const response = await axios.get(`${apiUrl}api/member/point`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + return response.data.point; + } catch (error) { + console.error('Failed to fetch points', error); + return 0; + } + }; + return ( diff --git a/src/pages/MyAccount/index.tsx b/src/pages/MyAccount/index.tsx index 7d201a73b..2a8ee8c45 100644 --- a/src/pages/MyAccount/index.tsx +++ b/src/pages/MyAccount/index.tsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled'; +import { useGetPoints } from '@/api/hooks/useGetPoints'; import { Button } from '@/components/common/Button'; import { Spacing } from '@/components/common/layouts/Spacing'; import { Wishlist } from '@/components/features/Wishlist'; @@ -9,6 +10,7 @@ import { authSessionStorage } from '@/utils/storage'; export const MyAccountPage = () => { const authInfo = useAuth(); + const points = useGetPoints(); const handleLogout = () => { authSessionStorage.set(undefined); @@ -20,7 +22,8 @@ export const MyAccountPage = () => { return ( - {authInfo?.name}님 안녕하세요! + {authInfo?.name}님 안녕하세요! + 보유 포인트 : {points}p