diff --git a/.eslintrc b/.eslintrc index b8ad6cf49..da35e08b8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -46,7 +46,10 @@ "react/button-has-type": "off", "react-hooks/rules-of-hooks": "off", "no-const-assign": "off", - "consistent-return": "off" + "consistent-return": "off", + "no-unused-expressions": "off", + "no-unsafe-optional-chaining": "off", + "no-restricted-globals": "off" }, "settings": { "import/resolver": { @@ -62,7 +65,8 @@ ["@context", "./src/context"], ["@internalTypes", "./src/types"], ["@apis", "./src/apis"], - ["@mocks", "./src/mocks"] + ["@mocks", "./src/mocks"], + ["@features", "./src/features"] ], "extensions": [".js", ".jsx", ".ts", ".tsx"] } diff --git a/README.md b/README.md index 3eaeec280..061b92fff 100644 --- a/README.md +++ b/README.md @@ -1 +1,40 @@ -# react-deploy \ No newline at end of file +# 6️⃣ 6주차 로그인 및 관심목록 (with. 테스트코드) +## 📄 1단계 - API 명세 협의 & 반영 +### ✅ 기능 목록 +- [x] auth관련 API 구현 + - [x] 회원가입 API 기능 구현 + - [x] 로그인 API 기능 구현 +- [x] 카테고리 전체 조회 API 기능 구현 +- [x] 상품관련 API 기능 구현 + - [x] 상품 개별 조회 API 기능 구현 + - [x] 상품 목록 조회(페이지네이션 조회) API 기능 구현 + - [x] 상품 옵션 API 기능 구현 +- [x] 위시리스트 API 기능 구현 + - [x] 위시 리스트 상품 추가 API 기능 구현 + - [x] 위시 리스트 상품 삭제 API 기능 구현 + - [x] 위시 리스트 상품 조회(페이지네이션 적용) API 기능 구현 +- [x] 주문하기 API 기능 구현 + +## 🚀 2단계 - 배포하기 +### ✅ 기능 목록 +- [x] vercel로 배포 진행 + +## 💰 3단계 - 포인트 +### ✅ 기능 목록 +- [x] 포인트 조회 API 기능 구현 +- [x] 마이페이지 포인트 조회 렌더링 구현 +- [x] 상품 주문시 포인트 사용 + - [x] 사용 가능한 포인트 렌더링 + - [x] 상품 가격에서 포인트 차감한 가격 렌더링 + - [x] 상품 주문시 포인트 사용 후 요청 + +## 🤔 4단계 - 질문의 답변을 README에 작성 +### 질문 1. SPA 페이지를 정적 배포를 하려고 할 때 Vercel을 사용하지 않고 한다면 어떻게 할 수 있을까요? +AWS S3와 CloudFront를 사용하여 배포할 수 있습니다. S3 버킷에 정적 파일을 업로드하고, CloudFront를 통해 캐싱 및 배포를 설정할 수 있습니다. + +### 질문 2. CSRF나 XSS 공격을 막는 방법은 무엇일까요? +- CSRF: CSRF 토큰을 사용하여 요청 검증, SameSite 쿠키 속성을 설정해 쿠키를 특정 사이트에서만 사용하도록 제한. +- XSS: 사용자 입력값을 철저히 검증하고 인코딩 처리, Content Security Policy(CSP) 설정, 신뢰할 수 없는 데이터를 HTML에 직접 삽입하지 않음. + +### 질문 3. 브라우저 렌더링 원리에대해 설명해주세요. +브라우저는 HTML을 파싱하여 DOM을 생성하고, CSS를 파싱하여 CSSOM을 생성합니다. DOM과 CSSOM을 결합하여 렌더 트리를 형성합니다. 이 렌더 트리를 기반으로 레이아웃을 계산하고, 페인트 과정을 통해 픽셀을 화면에 그립니다. 최종적으로 합성하여 사용자에게 화면을 표시합니다. \ No newline at end of file diff --git a/craco.config.js b/craco.config.js index 55e95420f..7dbca7465 100644 --- a/craco.config.js +++ b/craco.config.js @@ -14,6 +14,7 @@ module.exports = { '@mocks': path.resolve(__dirname, 'src/mocks'), '@internalTypes': path.resolve(__dirname, 'src/types'), '@apis': path.resolve(__dirname, 'src/apis'), + '@features': path.resolve(__dirname, 'src/features'), }, }, jest: { @@ -31,6 +32,7 @@ module.exports = { '^@types/(.*)$': '/src/types/$1', '^@utils/(.*)$': '/src/utils/$1', '^@hooks/(.*)$': '/src/hooks/$1', + '^@features/(.*)$': '/src/features/$1', }, }, }, diff --git a/public/index.html b/public/index.html index bf8ac870c..9f3f59147 100644 --- a/public/index.html +++ b/public/index.html @@ -4,6 +4,7 @@ + Kakao Tech diff --git a/src/App.tsx b/src/App.tsx index b0264cca7..e68ca7101 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,20 @@ import React from 'react'; import { Outlet } from 'react-router-dom'; +import APIProvider from '@context/api/APiProvider'; import AuthProvider from '@context/auth/AuthProvider'; import FilterProvider from '@context/filter/FilterProvider'; import GlobalStyles from '@assets/styles'; function App() { return ( - - - - - - + + + + + + + + ); } diff --git a/src/apis/categories/hooks/useGetCategories.ts b/src/apis/categories/hooks/useGetCategories.ts new file mode 100644 index 000000000..1beeefbb6 --- /dev/null +++ b/src/apis/categories/hooks/useGetCategories.ts @@ -0,0 +1,21 @@ +import { initInstance } from '@apis/instance'; +import { AxiosError } from 'axios'; +import { CategoriesResponse } from '@internalTypes/responseTypes'; +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import { CATEGORIES_PATHS } from '@apis/path'; +import { useAPI } from '@context/api/useAPI'; + +const getCategories = async (baseURL: string): Promise => { + const instance = initInstance(baseURL); + const res = await instance.get(CATEGORIES_PATHS.CATEGORIES); + return res.data; +}; + +export const useGetCategories = (): UseQueryResult => { + const { baseURL } = useAPI(); + + return useQuery({ + queryKey: ['categories'], + queryFn: () => getCategories(baseURL), + }); +}; diff --git a/src/apis/hooks/categories.mock.ts b/src/apis/categories/index.mock.ts similarity index 76% rename from src/apis/hooks/categories.mock.ts rename to src/apis/categories/index.mock.ts index a98c651d7..f5c31c689 100644 --- a/src/apis/hooks/categories.mock.ts +++ b/src/apis/categories/index.mock.ts @@ -1,9 +1,10 @@ import { rest } from 'msw'; +import { CATEGORIES_PATHS } from '@apis/path'; -import { getCategoriesPath } from './useGetCategorys'; +const BASE_URL = process.env.REACT_APP_BASE_URL; export const categoriesMockHandler = [ - rest.get(getCategoriesPath(), (_, res, ctx) => res(ctx.json(CATEGORIES_RESPONSE_DATA))), + rest.get(`${BASE_URL}${CATEGORIES_PATHS.CATEGORIES}`, (_, res, ctx) => res(ctx.json(CATEGORIES_RESPONSE_DATA))), ]; const CATEGORIES_RESPONSE_DATA = [ diff --git a/src/apis/hooks/products.mock.ts b/src/apis/hooks/products.mock.ts deleted file mode 100644 index 442641395..000000000 --- a/src/apis/hooks/products.mock.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { rest } from 'msw'; -import { getProductDetailPath } from './useGetProductDetail'; -import { getProductOptionsPath } from './useGetProductOptions'; - -export const productsMockHandler = [ - rest.get('/api/products', (req, res, ctx) => { - const categoryId = req.url.searchParams.get('categoryId'); - const sort = req.url.searchParams.get('sort'); - const page = req.url.searchParams.get('page'); - const size = req.url.searchParams.get('size'); - - if (categoryId === '2920' || categoryId === '2930') { - return res(ctx.json(PRODUCTS_MOCK_DATA)); - } - - return res(ctx.status(404)); - }), - rest.get(getProductDetailPath(':productId'), (_, res, ctx) => res(ctx.json(PRODUCTS_MOCK_DATA.content[0]))), - rest.get(getProductOptionsPath(':productId'), (_, res, ctx) => - res( - ctx.json([ - { - id: 1, - name: 'Option A', - quantity: 10, - productId: 1, - }, - { - id: 2, - name: 'Option B', - quantity: 20, - productId: 1, - }, - ]), - ), - ), -]; - -const PRODUCTS_MOCK_DATA = { - content: [ - { - id: 3245119, - name: '[단독각인] 피렌체 1221 에디션 오드코롱 50ml (13종 택1)', - imageUrl: 'https://st.kakaocdn.net/product/gift/product/20240215083306_8e1db057580145829542463a84971ae3.png', - price: 145000, - }, - { - id: 2263833, - name: '외식 통합권 10만원권', - imageUrl: 'https://st.kakaocdn.net/product/gift/product/20200513102805_4867c1e4a7ae43b5825e9ae14e2830e3.png', - price: 100000, - }, - { - id: 6502823, - name: '[선물포장/미니퍼퓸증정] 디켄터 리드 디퓨저 300ml + 메세지카드', - imageUrl: 'https://st.kakaocdn.net/product/gift/product/20240215112140_11f857e972bc4de6ac1d2f1af47ce182.jpg', - price: 108000, - }, - { - id: 1181831, - name: '[선물포장] 소바쥬 오 드 뚜왈렛 60ML', - imageUrl: 'https://st.kakaocdn.net/product/gift/product/20240214150740_ad25267defa64912a7c030a7b57dc090.jpg', - price: 122000, - }, - { - id: 1379982, - name: '[정관장] 홍삼정 에브리타임 리미티드 (10ml x 30포)', - imageUrl: 'https://st.kakaocdn.net/product/gift/product/20240118135914_a6e1a7442ea04aa49add5e02ed62b4c3.jpg', - price: 133000, - }, - ], - number: 0, - totalElements: 5, - size: 10, - last: true, -}; diff --git a/src/apis/hooks/useGetCategorys.ts b/src/apis/hooks/useGetCategorys.ts deleted file mode 100644 index ef79adabf..000000000 --- a/src/apis/hooks/useGetCategorys.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -import type { CategoryData } from '@internalTypes/dataTypes'; - -import axiosInstance from '../instance'; - -export type CategoryResponseData = CategoryData[]; - -export const getCategoriesPath = () => `${process.env.REACT_APP_BASE_URL}/api/categories`; -const categoriesQueryKey = [getCategoriesPath()]; - -export const getCategories = async () => { - const response = await axiosInstance.get(getCategoriesPath()); - return response.data; -}; - -export const useGetCategories = () => - useQuery({ - queryKey: categoriesQueryKey, - queryFn: getCategories, - }); diff --git a/src/apis/hooks/useGetProductDetail.ts b/src/apis/hooks/useGetProductDetail.ts deleted file mode 100644 index 43fc75036..000000000 --- a/src/apis/hooks/useGetProductDetail.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import type { ProductData } from '@internalTypes/dataTypes'; -import axiosInstance from '../instance'; - -export type ProductDetailRequestParams = { - productId: string; -}; - -type Props = ProductDetailRequestParams; - -export type GoodsDetailResponseData = ProductData; - -export const getProductDetailPath = (productId: string) => - `${process.env.REACT_APP_BASE_URL}/api/products/${productId}`; - -export const getProductDetail = async (params: ProductDetailRequestParams) => { - const response = await axiosInstance.get(getProductDetailPath(params.productId)); - - return response.data; -}; - -export const useGetProductDetail = ({ productId }: Props) => - useSuspenseQuery({ - queryKey: [getProductDetailPath(productId)], - queryFn: () => getProductDetail({ productId }), - }); diff --git a/src/apis/hooks/useGetProductOptions.ts b/src/apis/hooks/useGetProductOptions.ts deleted file mode 100644 index 9d9111794..000000000 --- a/src/apis/hooks/useGetProductOptions.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query'; -import type { ProductOptionsData } from '@internalTypes/dataTypes'; -import type { ProductDetailRequestParams } from './useGetProductDetail'; -import axiosInstance from '../instance'; - -type Props = ProductDetailRequestParams; - -export type ProductOptionsResponseData = ProductOptionsData[]; - -export const getProductOptionsPath = (productId: string) => - `${process.env.REACT_APP_BASE_URL}/api/products/${productId}/options`; - -export const getProductOptions = async (params: ProductDetailRequestParams) => { - const response = await axiosInstance.get(getProductOptionsPath(params.productId)); - return response.data; -}; - -export const useGetProductOptions = ({ productId }: Props) => - useSuspenseQuery({ - queryKey: [getProductOptionsPath(productId)], - queryFn: () => getProductOptions({ productId }), - }); diff --git a/src/apis/hooks/useGetProducts.ts b/src/apis/hooks/useGetProducts.ts deleted file mode 100644 index 9c58e5f47..000000000 --- a/src/apis/hooks/useGetProducts.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { type InfiniteData, useInfiniteQuery, type UseInfiniteQueryResult } from '@tanstack/react-query'; -import type { ProductData } from '@internalTypes/dataTypes'; -import axiosInstance from '../instance'; - -type RequestParams = { - categoryId: string; - pageToken?: string; - maxResults?: number; -}; - -type ProductsResponseData = { - products: ProductData[]; - nextPageToken?: string; - pageInfo: { - totalResults: number; - resultsPerPage: number; - }; -}; - -type ProductsResponseRawData = { - content: ProductData[]; - number: number; - totalElements: number; - size: number; - last: boolean; -}; - -export const getProductsPath = ({ categoryId, pageToken, maxResults }: RequestParams) => { - const params = new URLSearchParams(); - - params.append('categoryId', categoryId); - params.append('sort', 'name,asc'); - if (pageToken) params.append('page', pageToken); - if (maxResults) params.append('size', maxResults.toString()); - - return `${process.env.REACT_APP_BASE_URL}/api/products?${params.toString()}`; -}; - -export const getProducts = async (params: RequestParams): Promise => { - const response = await axiosInstance.get(getProductsPath(params)); - const { data } = response; - - return { - products: data.content, - nextPageToken: data.last === false ? (data.number + 1).toString() : undefined, - pageInfo: { - totalResults: data.totalElements, - resultsPerPage: data.size, - }, - }; -}; - -type Params = Pick & { initPageToken?: string }; -export const useGetProducts = ({ - categoryId, - maxResults = 20, - initPageToken, -}: Params): UseInfiniteQueryResult> => - useInfiniteQuery({ - queryKey: ['products', categoryId, maxResults, initPageToken], - queryFn: async ({ pageParam = initPageToken }) => getProducts({ categoryId, pageToken: pageParam, maxResults }), - initialPageParam: initPageToken, - getNextPageParam: (lastPage) => lastPage.nextPageToken, - }); diff --git a/src/apis/instance.ts b/src/apis/instance.ts index 330269313..9ed894727 100644 --- a/src/apis/instance.ts +++ b/src/apis/instance.ts @@ -1,4 +1,4 @@ -import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios'; +import axios, { AxiosError, AxiosInstance } from 'axios'; import { ERROR } from '@utils/constants/message'; import { QueryClient } from '@tanstack/react-query'; @@ -12,15 +12,13 @@ const statusMessages: { [key: number]: string } = { 500: ERROR.SERVER_ERROR, }; -export const initInstance = (config: AxiosRequestConfig): AxiosInstance => { +export const initInstance = (baseURL: string): AxiosInstance => { const instance = axios.create({ timeout: 5000, - baseURL: process.env.REACT_APP_BASE_URL, - ...config, + baseURL, headers: { Accept: 'application/json', 'Content-Type': 'application/json', - ...config.headers, }, }); @@ -38,8 +36,4 @@ export const initInstance = (config: AxiosRequestConfig): AxiosInstance => { return instance; }; -const axiosInstance = initInstance({}); - export const queryClient = new QueryClient(); - -export default axiosInstance; diff --git a/src/apis/instance/index.ts b/src/apis/instance/index.ts deleted file mode 100644 index b83ca1407..000000000 --- a/src/apis/instance/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { QueryClient } from '@tanstack/react-query'; -import type { AxiosInstance, AxiosRequestConfig } from 'axios'; -import axios from 'axios'; - -const initInstance = (config: AxiosRequestConfig): AxiosInstance => { - const instance = axios.create({ - timeout: 5000, - ...config, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - ...config.headers, - }, - }); - - return instance; -}; - -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: { - queries: { - retry: 1, - refetchOnMount: true, - refetchOnReconnect: true, - refetchOnWindowFocus: true, - }, - }, -}); diff --git a/src/apis/members/hooks/useMemberRegister.ts b/src/apis/members/hooks/useMemberRegister.ts new file mode 100644 index 000000000..9c783b244 --- /dev/null +++ b/src/apis/members/hooks/useMemberRegister.ts @@ -0,0 +1,23 @@ +import { AxiosError } from 'axios'; +import { useMutation, UseMutationResult } from '@tanstack/react-query'; +import { UserInfoData } from '@internalTypes/dataTypes'; +import { initInstance } from '@apis/instance'; +import { MemberResponse } from '@internalTypes/responseTypes'; +import { MEMBERS_PATHS } from '@apis/path'; +import { useAPI } from '@context/api/useAPI'; + +const postMemberRegister = async ({ email, password }: UserInfoData, baseURL: string): Promise => { + const instance = initInstance(baseURL); + const res = await instance.post(MEMBERS_PATHS.REGISTER, { + email, + password, + }); + return res.data; +}; + +export const useMemberRegister = (): UseMutationResult => { + const { baseURL } = useAPI(); + return useMutation({ + mutationFn: ({ email, password }: UserInfoData) => postMemberRegister({ email, password }, baseURL), + }); +}; diff --git a/src/apis/members/hooks/useMemerLogin.ts b/src/apis/members/hooks/useMemerLogin.ts new file mode 100644 index 000000000..a4f902155 --- /dev/null +++ b/src/apis/members/hooks/useMemerLogin.ts @@ -0,0 +1,23 @@ +import { AxiosError } from 'axios'; +import { initInstance } from '@apis/instance'; +import { useMutation, UseMutationResult } from '@tanstack/react-query'; +import { UserInfoData } from '@internalTypes/dataTypes'; +import { MemberResponse } from '@internalTypes/responseTypes'; +import { MEMBERS_PATHS } from '@apis/path'; +import { useAPI } from '@/context/api/useAPI'; + +const postMemberLogin = async ({ email, password }: UserInfoData, baseURL: string): Promise => { + const instance = initInstance(baseURL); + const res = await instance.post(MEMBERS_PATHS.LOGIN, { + email, + password, + }); + return res.data; +}; + +export const useMemberLogin = (): UseMutationResult => { + const { baseURL } = useAPI(); + return useMutation({ + mutationFn: ({ email, password }: UserInfoData) => postMemberLogin({ email, password }, baseURL), + }); +}; diff --git a/src/apis/members/index.mock.ts b/src/apis/members/index.mock.ts new file mode 100644 index 000000000..d5a98deac --- /dev/null +++ b/src/apis/members/index.mock.ts @@ -0,0 +1,13 @@ +import { rest } from 'msw'; +import { MEMBERS_PATHS } from '@apis/path'; + +const BASE_URL = process.env.REACT_APP_BASE_URL; + +export const memberMockHandler = [ + rest.post(`${BASE_URL}${MEMBERS_PATHS.REGISTER}`, async (_, res, ctx) => + res(ctx.status(200), ctx.json({ access_token: '1234' })), + ), + rest.post(`${BASE_URL}${MEMBERS_PATHS.LOGIN}`, (_, res, ctx) => + res(ctx.status(200), ctx.json({ access_token: '1234' })), + ), +]; diff --git a/src/apis/orders/index.mock.ts b/src/apis/orders/index.mock.ts new file mode 100644 index 000000000..c1791c1c7 --- /dev/null +++ b/src/apis/orders/index.mock.ts @@ -0,0 +1,20 @@ +import { rest } from 'msw'; +import { ORDER_PATHS } from '@apis/path'; + +const BASE_URL = process.env.REACT_APP_BASE_URL; + +export const orderMockHandler = [ + rest.post(`${BASE_URL}${ORDER_PATHS.ORDERS}`, async (req, res, ctx) => { + const { optionId, quantity, message } = await req.json(); + return res( + ctx.status(201), + ctx.json({ + id: 1, + optionId, + quantity, + orderDateTime: new Date().toISOString(), + message, + }), + ); + }), +]; diff --git a/src/apis/orders/useOrders.ts b/src/apis/orders/useOrders.ts new file mode 100644 index 000000000..e7f53f932 --- /dev/null +++ b/src/apis/orders/useOrders.ts @@ -0,0 +1,38 @@ +import { OrderResponse } from '@internalTypes/responseTypes'; +import { OrderRequest } from '@internalTypes/requestTypes'; +import { ORDER_PATHS } from '@apis/path'; +import { initInstance } from '@apis/instance'; +import { useMutation, UseMutationResult } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { useAPI } from '@context/api/useAPI'; + +const postOrders = async ( + { optionId, quantity, message, usedPoint }: OrderRequest, + baseURL: string, +): Promise => { + const instance = initInstance(baseURL); + const res = await instance.post( + ORDER_PATHS.ORDERS, + { + optionId, + quantity, + message, + usedPoint, + }, + { + headers: { + Authorization: `Bearer ${sessionStorage.getItem('authToken')}`, + }, + }, + ); + return res.data; +}; + +export const useOrders = (): UseMutationResult => { + const { baseURL } = useAPI(); + + return useMutation({ + mutationFn: ({ optionId, quantity, message, usedPoint }: OrderRequest) => + postOrders({ optionId, quantity, message, usedPoint }, baseURL), + }); +}; diff --git a/src/apis/path.ts b/src/apis/path.ts new file mode 100644 index 000000000..986fafd60 --- /dev/null +++ b/src/apis/path.ts @@ -0,0 +1,39 @@ +import { GetProductsRequest, GetWishesRequest } from '@internalTypes/requestTypes'; + +const API_BASE_KAKAO = '/api/v1'; +const API_BASE = '/api'; + +export const CATEGORIES_PATHS = { + CATEGORIES: `${API_BASE}/categories`, +}; + +export const MEMBERS_PATHS = { + REGISTER: `${API_BASE}/members/register`, + LOGIN: `${API_BASE}/members/login`, +}; + +export const PRODUCTS_PATHS = { + PRODUCTS: (params: GetProductsRequest) => + `${API_BASE}/products?page=${params.page}&size=${params.size}&sort=${params.sort}&categoryId=${params.categoryId}`, + PRODUCTS_DETAIL: (productId?: string) => `${API_BASE}/products/${productId}`, + PRODUCTS_OPTIONS: (productId?: string) => `${API_BASE}/products/${productId}/options`, +}; + +export const RANKING_PATHS = { + PRODUCTS: `${API_BASE_KAKAO}/ranking/products`, +}; + +export const WISH_PATHS = { + GET_WISH: (params: GetWishesRequest) => + `${API_BASE}/wishes?page=${params.page}&size=${params.size}&sort=${params.sort}`, + ADD_WISH: `${API_BASE}/wishes`, + DELETE_WISH: `${API_BASE}/wishes`, +}; + +export const ORDER_PATHS = { + ORDERS: `${API_BASE}/orders`, +}; + +export const POINT_PATHS = { + GET_POINT: `${API_BASE}/points`, +}; diff --git a/src/apis/point/index.mock.ts b/src/apis/point/index.mock.ts new file mode 100644 index 000000000..62ed7c90d --- /dev/null +++ b/src/apis/point/index.mock.ts @@ -0,0 +1,8 @@ +import { rest } from 'msw'; +import { POINT_PATHS } from '../path'; + +const BASE_URL = process.env.REACT_APP_BASE_URL; + +export const pointMockHandler = [ + rest.get(`${BASE_URL}${POINT_PATHS.GET_POINT}`, (_, res, ctx) => res(ctx.json({ point: 1000 }))), +]; diff --git a/src/apis/point/useGetPoint.ts b/src/apis/point/useGetPoint.ts new file mode 100644 index 000000000..36291100f --- /dev/null +++ b/src/apis/point/useGetPoint.ts @@ -0,0 +1,24 @@ +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import { useAPI } from '@context/api/useAPI'; +import { GetPointResponse } from '@internalTypes/responseTypes'; +import { AxiosError } from 'axios'; +import { initInstance } from '@apis/instance'; +import { POINT_PATHS } from '@apis/path'; + +const getPoint = async (baseURL: string): Promise => { + const instance = initInstance(baseURL); + const res = await instance(POINT_PATHS.GET_POINT, { + headers: { + Authorization: `Bearer ${sessionStorage.getItem('authToken')}`, + }, + }); + return res.data; +}; + +export const useGetPoint = (): UseQueryResult => { + const { baseURL } = useAPI(); + return useQuery({ + queryKey: ['point'], + queryFn: () => getPoint(baseURL), + }); +}; diff --git a/src/apis/products/hooks/useGetProducts.ts b/src/apis/products/hooks/useGetProducts.ts new file mode 100644 index 000000000..bfaa2090f --- /dev/null +++ b/src/apis/products/hooks/useGetProducts.ts @@ -0,0 +1,31 @@ +import { GetProductsRequest } from '@internalTypes/requestTypes'; +import { GetProductsResponse } from '@internalTypes/responseTypes'; +import { initInstance } from '@apis/instance'; +import { PRODUCTS_PATHS } from '@apis/path'; +import { InfiniteData, useInfiniteQuery, UseInfiniteQueryResult } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { useAPI } from '@/context/api/useAPI'; + +const getProducts = async (params: GetProductsRequest, baseURL: string): Promise => { + const instance = initInstance(baseURL); + const res = await instance.get(PRODUCTS_PATHS.PRODUCTS(params)); + return res.data; +}; + +export const useGetProducts = ({ + categoryId, + size, + sort, +}: Omit): UseInfiniteQueryResult, AxiosError> => { + const { baseURL } = useAPI(); + + return useInfiniteQuery({ + queryKey: ['products', categoryId], + queryFn: ({ pageParam = 0 }) => getProducts({ categoryId, page: pageParam, size, sort }, baseURL), + initialPageParam: 0, + getNextPageParam: (lastPage) => { + const nextPage = lastPage.pageable.pageNumber + 1; + return nextPage < lastPage.totalPages ? nextPage : undefined; + }, + }); +}; diff --git a/src/apis/products/hooks/useGetProductsDetail.ts b/src/apis/products/hooks/useGetProductsDetail.ts index e6ea680f0..68a74cb1e 100644 --- a/src/apis/products/hooks/useGetProductsDetail.ts +++ b/src/apis/products/hooks/useGetProductsDetail.ts @@ -1,20 +1,24 @@ -import { useQuery } from '@tanstack/react-query'; +import { useQuery, UseQueryResult } from '@tanstack/react-query'; import { ProductDetailRequest } from '@internalTypes/requestTypes'; import { ProductDetailResponse } from '@internalTypes/responseTypes'; +import { initInstance } from '@apis/instance'; import { AxiosError } from 'axios'; -import axiosInstance from '@apis/instance'; -import { PRODUCTS_PATHS } from '../path'; - -const getProductsDetail = async (params?: ProductDetailRequest): Promise => { - if (!params) throw new Error('params is required'); - const { productId } = params; - const res = await axiosInstance.get(PRODUCTS_PATHS.PRODUCTS_DETAIL(productId)); +import { PRODUCTS_PATHS } from '@apis/path'; +import { useAPI } from '@/context/api/useAPI'; +const getProductsDetail = async (params: ProductDetailRequest, baseURL: string): Promise => { + const instance = initInstance(baseURL); + const res = await instance.get(PRODUCTS_PATHS.PRODUCTS_DETAIL(params.productId)); return res.data; }; -export const useGetProductsDetail = ({ productId }: ProductDetailRequest) => - useQuery({ +export const useGetProductsDetail = ({ + productId, +}: ProductDetailRequest): UseQueryResult => { + const { baseURL } = useAPI(); + + return useQuery({ queryKey: ['productDetail', productId], - queryFn: () => getProductsDetail({ productId }), + queryFn: () => getProductsDetail({ productId }, baseURL), }); +}; diff --git a/src/apis/products/hooks/useGetProductsOption.ts b/src/apis/products/hooks/useGetProductsOption.ts index 72754ade1..93f0b88cc 100644 --- a/src/apis/products/hooks/useGetProductsOption.ts +++ b/src/apis/products/hooks/useGetProductsOption.ts @@ -1,20 +1,28 @@ -import { useQuery } from '@tanstack/react-query'; -import { ProductOptionsRequest } from '@internalTypes/requestTypes'; +import { useQuery, UseQueryResult } from '@tanstack/react-query'; import { ProductOptionResponse } from '@internalTypes/responseTypes'; +import { ProductOptionsRequest } from '@internalTypes/requestTypes'; +import { initInstance } from '@apis/instance'; import { AxiosError } from 'axios'; -import axiosInstance from '@apis/instance'; -import { PRODUCTS_PATHS } from '../path'; +import { PRODUCTS_PATHS } from '@apis/path'; +import { useAPI } from '@/context/api/useAPI'; -export const getProductsOptions = async (params?: ProductOptionsRequest): Promise => { - if (!params) throw new Error('params is required'); +export const getProductsOptions = async ( + params: ProductOptionsRequest, + baseURL: string, +): Promise => { + const instance = initInstance(baseURL); const { productId } = params; - const res = await axiosInstance.get(PRODUCTS_PATHS.PRODUCTS_OPTIONS(productId)); - + const res = await instance.get(PRODUCTS_PATHS.PRODUCTS_OPTIONS(productId)); return res.data; }; -export const useGetProductsOption = ({ productId }: ProductOptionsRequest) => - useQuery({ +export const useGetProductsOption = ({ + productId, +}: ProductOptionsRequest): UseQueryResult => { + const { baseURL } = useAPI(); + + return useQuery({ queryKey: ['productOption', productId], - queryFn: () => getProductsOptions({ productId }), + queryFn: () => getProductsOptions({ productId }, baseURL), }); +}; diff --git a/src/apis/products/index.mock.ts b/src/apis/products/index.mock.ts index 04bd78eec..fb25fc78d 100644 --- a/src/apis/products/index.mock.ts +++ b/src/apis/products/index.mock.ts @@ -1,38 +1,121 @@ import { rest } from 'msw'; -import { PRODUCTS_PATHS } from './path'; +import { PRODUCTS_PATHS } from '@apis/path'; const BASE_URL = process.env.REACT_APP_BASE_URL; export const productsMockHandler = [ + rest.get( + `${BASE_URL}${PRODUCTS_PATHS.PRODUCTS({ page: 0, sort: 'name,asc', size: 10, categoryId: 2 })}`, + (_, res, ctx) => res(ctx.json(PRODUCTS_MOCK_DATA)), + ), rest.get(`${BASE_URL}${PRODUCTS_PATHS.PRODUCTS_DETAIL(':productId')}`, (req, res, ctx) => { const { productId } = req.params; - if (productId === '3245119') { - return res(ctx.json(PRODUCTS_MOCK_DATA)); - } - return res(ctx.status(404), ctx.json({ message: 'Product not found' })); - }), - rest.get(`${BASE_URL}${PRODUCTS_PATHS.PRODUCTS_OPTIONS(':productId')}`, (req, res, ctx) => { - const { productId } = req.params; - if (productId === '3245119') { - return res( - ctx.json({ - options: { - giftOrderLimit: 100, - }, - }), - ); - } - return res(ctx.status(404), ctx.json({ message: 'Product not found' })); + return res(ctx.json(PRODUCTS_MOCK_DATA.content.find((data) => productId === String(data.id)))); }), + rest.get(`${BASE_URL}${PRODUCTS_PATHS.PRODUCTS_OPTIONS(':productId')}`, (_, res, ctx) => + res( + ctx.json([ + { + id: 1, + name: '옵션이름1', + quantity: 100, + }, + { + id: 2, + name: '옵션이름2', + quantity: 100, + }, + ]), + ), + ), ]; const PRODUCTS_MOCK_DATA = { - detail: { - id: 3245119, - name: '[단독각인] 피렌체 1221 에디션 오드코롱 50ml (13종 택1)', - imageURL: 'https://st.kakaocdn.net/product/gift/product/20240215083306_8e1db057580145829542463a84971ae3.png', - price: { - basicPrice: 145000, + totalPages: 0, + totalElements: 0, + size: 0, + content: [ + { + id: 3245119, + name: '[단독각인] 피렌체 1221 에디션 오드코롱 50ml (13종 택1)', + imageUrl: 'https://st.kakaocdn.net/product/gift/product/20240215083306_8e1db057580145829542463a84971ae3.png', + price: 145000, + category: { + id: 3245119, + name: '카테고리 이름', + imageUrl: 'https://st.kakaocdn.net/product/gift/product/20240215083306_8e1db057580145829542463a84971ae3.png', + description: '설명', + }, + }, + { + id: 2263833, + name: '외식 통합권 10만원권', + imageUrl: 'https://st.kakaocdn.net/product/gift/product/20200513102805_4867c1e4a7ae43b5825e9ae14e2830e3.png', + price: 100000, + category: { + id: 2263833, + name: '카테고리 이름', + imageUrl: 'https://st.kakaocdn.net/product/gift/product/20200513102805_4867c1e4a7ae43b5825e9ae14e2830e3.png', + description: '설명', + }, + }, + { + id: 6502823, + name: '[선물포장/미니퍼퓸증정] 디켄터 리드 디퓨저 300ml + 메세지카드', + imageUrl: 'https://st.kakaocdn.net/product/gift/product/20240215112140_11f857e972bc4de6ac1d2f1af47ce182.jpg', + price: 108000, + category: { + id: 6502823, + name: '카테고리 이름', + imageUrl: 'https://st.kakaocdn.net/product/gift/product/20240215112140_11f857e972bc4de6ac1d2f1af47ce182.jpg', + description: '설명', + }, + }, + { + id: 1181831, + name: '[선물포장] 소바쥬 오 드 뚜왈렛 60ML', + imageUrl: 'https://st.kakaocdn.net/product/gift/product/20240214150740_ad25267defa64912a7c030a7b57dc090.jpg', + price: 122000, + category: { + id: 1181831, + name: '카테고리 이름', + imageUrl: 'https://st.kakaocdn.net/product/gift/product/20240214150740_ad25267defa64912a7c030a7b57dc090.jpg', + description: '설명', + }, + }, + { + id: 1379982, + name: '[정관장] 홍삼정 에브리타임 리미티드 (10ml x 30포)', + imageUrl: 'https://st.kakaocdn.net/product/gift/product/20240118135914_a6e1a7442ea04aa49add5e02ed62b4c3.jpg', + price: 133000, + category: { + id: 1379982, + name: '카테고리 이름', + imageUrl: 'https://st.kakaocdn.net/product/gift/product/20240118135914_a6e1a7442ea04aa49add5e02ed62b4c3.jpg', + description: '설명', + }, + }, + ], + number: 0, + sort: { + empty: true, + sorted: true, + unsorted: true, + }, + first: true, + last: true, + numberOfElements: 0, + pageable: { + offset: 0, + sort: { + empty: true, + sorted: true, + unsorted: true, }, + paged: true, + pageSize: 0, + pageNumber: 0, + unpaged: true, }, + empty: true, }; diff --git a/src/apis/products/path.ts b/src/apis/products/path.ts deleted file mode 100644 index f593ec49b..000000000 --- a/src/apis/products/path.ts +++ /dev/null @@ -1,6 +0,0 @@ -const API_BASE = '/api/v1'; - -export const PRODUCTS_PATHS = { - PRODUCTS_DETAIL: (productId?: string) => `${API_BASE}/products/${productId}/detail`, - PRODUCTS_OPTIONS: (productId?: string) => `${API_BASE}/products/${productId}/options`, -}; diff --git a/src/apis/ranking/index.ts b/src/apis/ranking/index.ts deleted file mode 100644 index 322ff9aaa..000000000 --- a/src/apis/ranking/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { RankingProductsRequest } from '@internalTypes/requestTypes'; -import { RankingProductsResponse } from '@internalTypes/responseTypes'; -import axiosInstance from '../instance'; -import { RANKING_PATHS } from './path'; - -export const getRankingProducts = async (params?: RankingProductsRequest): Promise => { - const res = await axiosInstance.get(RANKING_PATHS.PRODUCTS, { params }); - return res.data; -}; diff --git a/src/apis/ranking/path.ts b/src/apis/ranking/path.ts deleted file mode 100644 index 08d19c3f8..000000000 --- a/src/apis/ranking/path.ts +++ /dev/null @@ -1,5 +0,0 @@ -const API_BASE = '/api/v1'; - -export const RANKING_PATHS = { - PRODUCTS: `${API_BASE}/ranking/products`, -}; diff --git a/src/apis/ranking/useGetRankingProducts.ts b/src/apis/ranking/useGetRankingProducts.ts new file mode 100644 index 000000000..d0999b0f9 --- /dev/null +++ b/src/apis/ranking/useGetRankingProducts.ts @@ -0,0 +1,27 @@ +import { RankingProductsRequest } from '@internalTypes/requestTypes'; +import { RankingProductsResponse } from '@internalTypes/responseTypes'; +import { RANKING_PATHS } from '@apis/path'; +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { initInstance } from '@apis/instance'; +import { useAPI } from '@/context/api/useAPI'; + +const getRankingProducts = async ( + baseURL: string, + params?: RankingProductsRequest, +): Promise => { + const instance = initInstance(baseURL); + const res = await instance.get(RANKING_PATHS.PRODUCTS, { params }); + return res.data; +}; + +export const useGetRankingProducts = ( + params: RankingProductsRequest, +): UseQueryResult => { + const { baseURL } = useAPI(); + + return useQuery({ + queryKey: ['rankingProducts', params], + queryFn: () => getRankingProducts(baseURL, params), + }); +}; diff --git a/src/apis/themes/index.ts b/src/apis/themes/index.ts deleted file mode 100644 index 4c9b41fb8..000000000 --- a/src/apis/themes/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ThemeProductsRequest } from '@internalTypes/requestTypes'; -import { ThemeProductsResponse } from '@internalTypes/responseTypes'; -import axiosInstance from '../instance'; -import { THEME_PATHS } from './path'; - -export const getThemes = async (): Promise => { - const res = await axiosInstance.get(THEME_PATHS.THEMES); - return res.data; -}; - -export const getThemesProducts = async (params?: ThemeProductsRequest): Promise => { - if (!params) throw new Error('params is required'); - const { themeKey, pageToken, maxResults } = params; - const queryParams = { pageToken, maxResults }; - - const res = await axiosInstance.get(THEME_PATHS.THEME_PRODUCTS(themeKey), { - params: queryParams, - }); - return res.data; -}; diff --git a/src/apis/themes/path.ts b/src/apis/themes/path.ts deleted file mode 100644 index 8724b6fd9..000000000 --- a/src/apis/themes/path.ts +++ /dev/null @@ -1,6 +0,0 @@ -const API_BASE = '/api/v1'; - -export const THEME_PATHS = { - THEMES: `${API_BASE}/themes`, - THEME_PRODUCTS: (themeKey?: string) => `${API_BASE}/themes/${themeKey}/products`, -}; diff --git a/src/apis/wish/hooks/useAddWish.ts b/src/apis/wish/hooks/useAddWish.ts index 956330ff8..7261b5f98 100644 --- a/src/apis/wish/hooks/useAddWish.ts +++ b/src/apis/wish/hooks/useAddWish.ts @@ -1,19 +1,22 @@ -import { AxiosError, AxiosResponse } from 'axios'; -import axiosInstance from '@apis/instance'; +import { AxiosError } from 'axios'; +import { initInstance } from '@apis/instance'; import { AddWishRequest } from '@internalTypes/requestTypes'; import { AddWishResponse } from '@internalTypes/responseTypes'; -import { WISH_PATHS } from '@apis/wish/path'; +import { WISH_PATHS } from '@apis/path'; import { useMutation, UseMutationResult } from '@tanstack/react-query'; +import { useAPI } from '@/context/api/useAPI'; -const addWish = async (request: AddWishRequest): Promise => { - const res = await axiosInstance.post(WISH_PATHS.ADD_WISH, request, { +const addWish = async (request: AddWishRequest, baseURL: string): Promise => { + const instance = initInstance(baseURL); + const res = await instance.post(WISH_PATHS.ADD_WISH, request, { headers: { - Authorization: `Bearer ${localStorage.getItem('authToken')}`, + Authorization: `Bearer ${sessionStorage.getItem('authToken')}`, }, }); return res.data; }; -export default function useAddWishMutation(): UseMutationResult { - return useMutation({ mutationFn: (productId: AddWishRequest) => addWish(productId) }); -} +export const useAddWishMutation = (): UseMutationResult => { + const { baseURL } = useAPI(); + return useMutation({ mutationFn: (request: AddWishRequest) => addWish(request, baseURL) }); +}; diff --git a/src/apis/wish/hooks/useDeleteWish.ts b/src/apis/wish/hooks/useDeleteWish.ts index 97e83cf0f..857800f96 100644 --- a/src/apis/wish/hooks/useDeleteWish.ts +++ b/src/apis/wish/hooks/useDeleteWish.ts @@ -1,22 +1,25 @@ -import axios, { AxiosResponse, AxiosError } from 'axios'; +import { AxiosError } from 'axios'; import { DeleteWishRequest } from '@internalTypes/requestTypes'; -import { useMutation, UseMutationResult, UseMutationOptions } from '@tanstack/react-query'; -import { WISH_PATHS } from '../path'; +import { initInstance } from '@apis/instance'; +import { WISH_PATHS } from '@apis/path'; +import { useMutation, UseMutationResult } from '@tanstack/react-query'; +import { useAPI } from '@/context/api/useAPI'; -const deleteWish = async ({ wishId }: DeleteWishRequest): Promise => { - await axios.delete(`${WISH_PATHS.DELETE_WISH}/${wishId}`, { +const deleteWish = async (request: DeleteWishRequest, baseURL: string): Promise => { + const instance = initInstance(baseURL); + await instance.delete(`${WISH_PATHS.DELETE_WISH}/${request.wishId}`, { headers: { - Authorization: `Bearer ${localStorage.getItem('authToken')}`, + Authorization: `Bearer ${sessionStorage.getItem('authToken')}`, }, }); }; -const useDeleteWish = ( - options?: UseMutationOptions, -): UseMutationResult => - useMutation({ - mutationFn: deleteWish, - ...options, +const useDeleteWish = (): UseMutationResult => { + const { baseURL } = useAPI(); + + return useMutation({ + mutationFn: (request: DeleteWishRequest) => deleteWish(request, baseURL), }); +}; export default useDeleteWish; diff --git a/src/apis/wish/hooks/useGetWishes.ts b/src/apis/wish/hooks/useGetWishes.ts index 546e96001..4a0b95cc8 100644 --- a/src/apis/wish/hooks/useGetWishes.ts +++ b/src/apis/wish/hooks/useGetWishes.ts @@ -1,15 +1,16 @@ -import axios, { AxiosResponse, AxiosError } from 'axios'; +import { AxiosResponse, AxiosError } from 'axios'; import { GetWishesRequest } from '@internalTypes/requestTypes'; import { GetWishesResponse } from '@internalTypes/responseTypes'; +import { WISH_PATHS } from '@apis/path'; +import { initInstance } from '@apis/instance'; import { useQuery, UseQueryResult } from '@tanstack/react-query'; -import { WISH_PATHS } from '../path'; +import { useAPI } from '@/context/api/useAPI'; -const getWishes = async (params: GetWishesRequest): Promise => { - const { page, size, sort } = params; - const res: AxiosResponse = await axios.get(WISH_PATHS.GET_WISH, { - params: { page, size, sort }, +const getWishes = async (params: GetWishesRequest, baseURL: string): Promise => { + const instance = initInstance(baseURL); + const res: AxiosResponse = await instance.get(WISH_PATHS.GET_WISH(params), { headers: { - Authorization: `Bearer ${localStorage.getItem('authToken')}`, + Authorization: `Bearer ${sessionStorage.getItem('authToken')}`, }, }); return res.data; @@ -17,8 +18,10 @@ const getWishes = async (params: GetWishesRequest): Promise = export default getWishes; -export const useGetWishes = (params: GetWishesRequest): UseQueryResult => - useQuery({ +export const useGetWishes = (params: GetWishesRequest): UseQueryResult => { + const { baseURL } = useAPI(); + return useQuery({ queryKey: ['wishes', params], - queryFn: () => getWishes(params), + queryFn: () => getWishes(params, baseURL), }); +}; diff --git a/src/apis/wish/index.mock.ts b/src/apis/wish/index.mock.ts index 4c40f25e7..6ba11598b 100644 --- a/src/apis/wish/index.mock.ts +++ b/src/apis/wish/index.mock.ts @@ -1,64 +1,106 @@ import { rest } from 'msw'; +import { WISH_PATHS } from '@apis/path'; + +const BASE_URL = process.env.REACT_APP_BASE_URL; export const wishMockHandler = [ - rest.get('/api/wishes', (req, res, ctx) => { - const page = req.url.searchParams.get('page'); - const size = req.url.searchParams.get('size'); - const sort = req.url.searchParams.get('sort'); + rest.get(`${BASE_URL}${WISH_PATHS.GET_WISH({ page: 0, size: 10, sort: 'createdDate,desc' })}`, (_, res, ctx) => + res(ctx.json(WISHES_MOCK_DATA)), + ), + rest.post(`${BASE_URL}${WISH_PATHS.ADD_WISH}`, (req, res, ctx) => { + const maxWishId = Math.max(...WISHES_MOCK_DATA.content.map((item) => item.wishId), 0); + const maxProductId = Math.max(...WISHES_MOCK_DATA.content.map((item) => item.productId), 0); + const newWishId = maxWishId + 1; + const newProductId = maxProductId + 1; - return res(ctx.status(200), ctx.json(WISHES_MOCK_DATA)); + WISHES_MOCK_DATA = { + ...WISHES_MOCK_DATA, + content: [ + ...WISHES_MOCK_DATA.content, + { + wishId: newWishId, + productId: newProductId, + productName: '새 상품 이름', + productPrice: 3000, + productImageUrl: 'https://example.com/image3.jpg', + category: { + id: 3, + name: '새 카테고리 이름', + imageUrl: 'https://example.com/category3.jpg', + description: '새 카테고리 설명', + }, + }, + ], + totalElements: WISHES_MOCK_DATA.totalElements + 1, + }; + return res(ctx.status(200), ctx.json({ wishId: newWishId })); }), - rest.delete(`/api/wishes/:wishId`, (req, res, ctx) => { - const wishId = req.params.wishId as string; - const wishIdNumber = Array.isArray(wishId) ? parseInt(wishId[0], 10) : parseInt(wishId, 10); + rest.delete(`${BASE_URL}${WISH_PATHS.DELETE_WISH}/:wishId`, (req, res, ctx) => { + const wishIdParam = req.params.wishId; + const wishId = Array.isArray(wishIdParam) ? wishIdParam[0] : wishIdParam; + const wishIdNumber = parseInt(wishId, 10); WISHES_MOCK_DATA = { ...WISHES_MOCK_DATA, - content: WISHES_MOCK_DATA.content.filter((item) => item.id !== wishIdNumber), + content: WISHES_MOCK_DATA.content.filter((item) => item.wishId !== wishIdNumber), + totalElements: WISHES_MOCK_DATA.totalElements - 1, }; return res(ctx.status(200)); }), ]; let WISHES_MOCK_DATA = { + totalPages: 1, + totalElements: 2, + size: 2, content: [ { - id: 1, - product: { + wishId: 1, + productId: 1, + productName: '상품 이름 1', + productPrice: 1000, + productImageUrl: 'https://example.com/image1.jpg', + category: { id: 1, - name: 'Product A', - price: 100, - imageUrl: 'http://example.com/product-a.jpg', + name: '카테고리 이름 1', + imageUrl: 'https://example.com/category1.jpg', + description: '카테고리 설명 1', }, }, { - id: 2, - product: { + wishId: 2, + productId: 2, + productName: '상품 이름 2', + productPrice: 2000, + productImageUrl: 'https://example.com/image2.jpg', + category: { id: 2, - name: 'Product B', - price: 150, - imageUrl: 'http://example.com/product-b.jpg', + name: '카테고리 이름 2', + imageUrl: 'https://example.com/category2.jpg', + description: '카테고리 설명 2', }, }, ], + number: 0, + sort: { + empty: false, + sorted: true, + unsorted: false, + }, + first: true, + last: true, + numberOfElements: 2, pageable: { + offset: 0, sort: { + empty: false, sorted: true, unsorted: false, - empty: false, }, + paged: true, + pageSize: 2, pageNumber: 0, - pageSize: 10, - offset: 0, unpaged: false, - paged: true, }, - totalPages: 5, - totalElements: 50, - last: false, - number: 0, - size: 10, - numberOfElements: 2, - first: true, empty: false, }; diff --git a/src/apis/wish/path.ts b/src/apis/wish/path.ts deleted file mode 100644 index 1b93a4dfd..000000000 --- a/src/apis/wish/path.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const WISH_PATHS = { - ADD_WISH: '/api/wishes', - GET_WISH: '/api/wishes', - DELETE_WISH: '/api/wishes', -}; diff --git a/src/components/common/index.ts b/src/components/common/index.ts index c10fbdea9..2a958e683 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -1,11 +1,9 @@ export { default as AsyncBoundary } from './AsyncBoundary'; export { default as Button } from './Button'; -export { default as Footer } from '../features/Layout/Footer'; export { default as InputField } from './Form/InputField'; export { default as Image } from './Image'; export { default as GoodsItem } from './GoodsItem'; export { default as Ranking } from './GoodsItem/Ranking'; -export { default as Header } from '../features/Layout/Header'; export { default as Container } from './Layout/Container'; export { default as Grid } from './Layout/Grid'; export { default as CenteredContainer } from './Layout/CenteredContainer'; diff --git a/src/components/features/Auth/useAuthForm.ts b/src/components/features/Auth/useAuthForm.ts deleted file mode 100644 index 1b66564c9..000000000 --- a/src/components/features/Auth/useAuthForm.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useAuth } from '@context/auth/useAuth'; -import { ROUTE_PATH } from '@routes/path'; -import { ChangeEvent, FormEvent, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; - -interface UserInfoState { - email: string; - password: string; -} - -export default function useAuthForm() { - const navigate = useNavigate(); - const { login } = useAuth(); - const [userInfo, setUserInfo] = useState({ - email: '', - password: '', - }); - - const handleChange = (e: ChangeEvent) => { - const { name, value } = e.target; - setUserInfo({ ...userInfo, [name]: value }); - }; - - const handleLogin = () => { - if (userInfo.email !== '' && userInfo.password !== '') { - login(userInfo.email); - navigate(ROUTE_PATH.HOME); - } - }; - - const handleSubmit = (e: FormEvent) => { - e.preventDefault(); - handleLogin(); - }; - - return { - userInfo, - handleChange, - handleSubmit, - }; -} diff --git a/src/components/features/Home/ThemeCategory/ThemeItem/index.stories.ts b/src/components/features/Home/ThemeCategory/ThemeItem/index.stories.ts deleted file mode 100644 index 7a38884f4..000000000 --- a/src/components/features/Home/ThemeCategory/ThemeItem/index.stories.ts +++ /dev/null @@ -1,24 +0,0 @@ -import image from '@assets/images/theme.jpeg'; -import { Meta, StoryObj } from '@storybook/react'; -import ThemeItem, { ThemeItemProps } from '.'; - -const meta: Meta = { - title: 'features/Home/ThemeCategory/ThemeItem', - component: ThemeItem, - tags: ['autodocs'], - argTypes: { - label: { control: { type: 'text' } }, - image: { control: { type: 'text' } }, - }, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - label: '럭셔리', - image, - }, -}; diff --git a/src/components/features/Home/ThemeCategory/index.tsx b/src/components/features/Home/ThemeCategory/index.tsx deleted file mode 100644 index f366a6e80..000000000 --- a/src/components/features/Home/ThemeCategory/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import styled from '@emotion/styled'; -import { getThemes } from '@apis/themes'; -import { Grid, CenteredContainer, StatusHandler } from '@components/common'; -import { useQuery } from '@tanstack/react-query'; -import { Link } from 'react-router-dom'; -import { ROUTE_PATH } from '@routes/path'; -import { getDynamicPath } from '@utils/getDynamicPath'; -import { ThemesResponse } from '@internalTypes/responseTypes'; -import { AxiosError } from 'axios'; -import ThemeItem from './ThemeItem'; - -const GRID_GAP = 0; -const GRID_COLUMNS = 6; - -interface LocationState { - backgroundColor: string; - label: string; - title: string; - description: string; -} - -export default function ThemeCategory() { - const { data, error, isError, isLoading } = useQuery({ - queryKey: ['theme'], - queryFn: getThemes, - }); - - const isEmpty = !data || data?.themes.length === 0; - - return ( - - - - - {data?.themes.map((theme) => ( - - - - ))} - - - - - ); -} - -const ThemeCategoryContainer = styled.section` - padding-top: 45px; - padding-bottom: 23px; -`; diff --git a/src/components/features/Order/Payment/ReceiptForm/index.tsx b/src/components/features/Order/Payment/ReceiptForm/index.tsx deleted file mode 100644 index d9809fd8d..000000000 --- a/src/components/features/Order/Payment/ReceiptForm/index.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import styled from '@emotion/styled'; -import { Checkbox, Select, Input } from '@chakra-ui/react'; -import { Button } from '@components/common'; -import { useFormContext } from 'react-hook-form'; -import { OrderDataFormValues } from '@/pages/Order'; -import { validatePayment } from './validation'; - -export default function ReceiptForm() { - const { register, watch, handleSubmit } = useFormContext(); - const { hasCashReceipt } = watch(); - - const onSubmit = (data: OrderDataFormValues) => { - const errorMessage = validatePayment(data.message, data.hasCashReceipt, data.cashReceiptNumber); - if (errorMessage) return alert(errorMessage); - return alert('주문이 완료되었습니다.'); - }; - - return ( -
- - 현금영수증 신청 - - {hasCashReceipt && ( - <> - - - - )} - -
-
최종 결제금액
-
49900원
-
-
- -
- ); -} - -const TotalAmount = styled.div` - padding: 18px 20px; - border-radius: 4px; - background-color: rgb(245, 245, 245); - margin-bottom: 20px; - - dl { - display: flex; - justify-content: space-between; - align-items: center; - font-weight: 700; - } - - dt { - font-size: 14px; - } - - dd { - font-size: 20px; - } -`; diff --git a/src/context/api/APIContext.tsx b/src/context/api/APIContext.tsx new file mode 100644 index 000000000..1ab4631c0 --- /dev/null +++ b/src/context/api/APIContext.tsx @@ -0,0 +1,10 @@ +import { createContext } from 'react'; + +export interface APIContextProps { + baseURL: string; + setBaseURL: (url: string) => void; +} + +const APIContext = createContext(undefined); + +export default APIContext; diff --git a/src/context/api/APiProvider.tsx b/src/context/api/APiProvider.tsx new file mode 100644 index 000000000..dd43e17ca --- /dev/null +++ b/src/context/api/APiProvider.tsx @@ -0,0 +1,21 @@ +// apis/APIProvider.tsx +import React, { useState, ReactNode, useMemo } from 'react'; +import APIContext from './APIContext'; + +interface APIProviderProps { + children: ReactNode; +} + +export default function APIProvider({ children }: APIProviderProps) { + const [baseURL, setBaseURL] = useState(process.env.REACT_APP_BASE_URL || ''); + + const value = useMemo( + () => ({ + baseURL, + setBaseURL, + }), + [baseURL, setBaseURL], + ); + + return {children}; +} diff --git a/src/context/api/useAPI.ts b/src/context/api/useAPI.ts new file mode 100644 index 000000000..3d2811f3c --- /dev/null +++ b/src/context/api/useAPI.ts @@ -0,0 +1,11 @@ +import { useContext } from 'react'; +import APIContext, { APIContextProps } from './APIContext'; + +const ERROR_MESSAGE = 'useAPI must be used within an APIProvider'; + +export const useAPI = (): APIContextProps => { + const context = useContext(APIContext); + + if (!context) throw new Error(ERROR_MESSAGE); + return context; +}; diff --git a/src/context/auth/AuthContext.tsx b/src/context/auth/AuthContext.tsx index 0368e52ae..0913c9b83 100644 --- a/src/context/auth/AuthContext.tsx +++ b/src/context/auth/AuthContext.tsx @@ -2,8 +2,9 @@ import { createContext } from 'react'; interface AuthContextProps { isAuthenticated: boolean; - login: (userName: string) => void; + login: (email: string, accessToken: string) => void; logout: () => void; + userEmail: string; } const AuthContext = createContext(undefined); diff --git a/src/context/auth/AuthProvider.tsx b/src/context/auth/AuthProvider.tsx index b5aa53608..3a65dac89 100644 --- a/src/context/auth/AuthProvider.tsx +++ b/src/context/auth/AuthProvider.tsx @@ -11,6 +11,7 @@ interface AuthProviderProps { export default function AuthProvider({ children }: AuthProviderProps) { const navigate = useNavigate(); const [authToken, setAuthToken] = useSessionStorage('authToken', ''); + const [userEmail, setUserEmail] = useSessionStorage('email', ''); const [isAuthenticated, setIsAuthenticated] = useState(!!authToken); useEffect(() => { @@ -18,8 +19,9 @@ export default function AuthProvider({ children }: AuthProviderProps) { }, [authToken]); const login = useCallback( - (userName: string) => { - setAuthToken(userName); + (email: string, accessToken: string) => { + setAuthToken(accessToken); + setUserEmail(email); }, [setAuthToken], ); @@ -34,8 +36,9 @@ export default function AuthProvider({ children }: AuthProviderProps) { isAuthenticated, login, logout, + userEmail, }), - [isAuthenticated, login, logout], + [isAuthenticated, login, logout, userEmail], ); return {children}; diff --git a/src/components/features/Account/AccountOverview/index.stories.tsx b/src/features/Account/AccountOverview/index.stories.tsx similarity index 100% rename from src/components/features/Account/AccountOverview/index.stories.tsx rename to src/features/Account/AccountOverview/index.stories.tsx diff --git a/src/components/features/Account/AccountOverview/index.tsx b/src/features/Account/AccountOverview/index.tsx similarity index 63% rename from src/components/features/Account/AccountOverview/index.tsx rename to src/features/Account/AccountOverview/index.tsx index aefbade12..ce4df8838 100644 --- a/src/components/features/Account/AccountOverview/index.tsx +++ b/src/features/Account/AccountOverview/index.tsx @@ -2,15 +2,16 @@ import React from 'react'; import styled from '@emotion/styled'; import { Button } from '@components/common'; import { useAuth } from '@context/auth/useAuth'; -import { useSessionStorage } from '@hooks/useSessionStorage'; +import { useGetPoint } from '@apis/point/useGetPoint'; export default function AccountOverview() { - const { logout } = useAuth(); - const [userName] = useSessionStorage('authToken', ''); + const { userEmail, logout } = useAuth(); + const { data } = useGetPoint(); return ( - {userName}님 안녕하세요! + {userEmail}님 안녕하세요! + {`사용 가능한 포인트: ${data?.point}`} diff --git a/src/components/features/Account/WishList/index.tsx b/src/features/Account/WishList/index.tsx similarity index 69% rename from src/components/features/Account/WishList/index.tsx rename to src/features/Account/WishList/index.tsx index 3fa4b8058..87f2b04d0 100644 --- a/src/components/features/Account/WishList/index.tsx +++ b/src/features/Account/WishList/index.tsx @@ -5,19 +5,19 @@ import { CenteredContainer } from '@components/common'; import WishListItem from './WishLIstItem'; export default function WishList() { - const { data: wishesData, refetch } = useGetWishes({ page: 0, size: 10, sort: 'createdDate,desc' }); + const { data, refetch } = useGetWishes({ page: 0, size: 10, sort: 'asc' }); return ( 관심목록 - {wishesData?.content.map((wishItem) => ( + {data?.content.map((wishItem) => ( refetch()} /> ))} diff --git a/src/components/features/Auth/AuthField/index.tsx b/src/features/Auth/AuthField/index.tsx similarity index 81% rename from src/components/features/Auth/AuthField/index.tsx rename to src/features/Auth/AuthField/index.tsx index 1ebb0c81e..c6e2017c2 100644 --- a/src/components/features/Auth/AuthField/index.tsx +++ b/src/features/Auth/AuthField/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import styled from '@emotion/styled'; +import { ROUTE_PATH } from '@routes/path'; import { Button } from '@components/common'; import { Link } from 'react-router-dom'; import AuthForm from '../AuthForm'; @@ -18,14 +19,14 @@ export default function AuthField({ isSignUp }: AuthFieldProps) { userInfo={userInfo} onChange={handleChange} submitButton={ - } isSignUp={ !isSignUp && ( - 회원가입 + 회원가입 ) } diff --git a/src/components/features/Auth/AuthForm/index.stories.tsx b/src/features/Auth/AuthForm/index.stories.tsx similarity index 100% rename from src/components/features/Auth/AuthForm/index.stories.tsx rename to src/features/Auth/AuthForm/index.stories.tsx diff --git a/src/components/features/Auth/AuthForm/index.tsx b/src/features/Auth/AuthForm/index.tsx similarity index 100% rename from src/components/features/Auth/AuthForm/index.tsx rename to src/features/Auth/AuthForm/index.tsx diff --git a/src/features/Auth/useAuthForm.ts b/src/features/Auth/useAuthForm.ts new file mode 100644 index 000000000..b4dc3ebac --- /dev/null +++ b/src/features/Auth/useAuthForm.ts @@ -0,0 +1,63 @@ +import { UserInfoData } from '@internalTypes/dataTypes'; +import { useAuth } from '@context/auth/useAuth'; +import { ROUTE_PATH } from '@routes/path'; +import { ChangeEvent, FormEvent, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useMemberRegister } from '@apis/members/hooks/useMemberRegister'; +import { useMemberLogin } from '@/apis/members/hooks/useMemerLogin'; + +export default function useAuthForm() { + const navigate = useNavigate(); + const { login } = useAuth(); + const { mutate: registerMutate } = useMemberRegister(); + const { mutate: loginMutate } = useMemberLogin(); + + const [userInfo, setUserInfo] = useState({ + email: '', + password: '', + }); + + const handleChange = (e: ChangeEvent) => { + const { name, value } = e.target; + setUserInfo({ ...userInfo, [name]: value }); + }; + + const handleSignUp = () => { + registerMutate(userInfo, { + onSuccess: (data) => { + console.info(data); + login(userInfo.email, data.token); + navigate(ROUTE_PATH.HOME); + }, + onError: (error) => { + console.error('Error:', error); + }, + }); + }; + + const handleLogin = () => { + loginMutate(userInfo, { + onSuccess: (data) => { + console.info(data); + login(userInfo.email, data.token); + navigate(ROUTE_PATH.HOME); + }, + onError: (error) => { + console.error('Error:', error); + }, + }); + }; + + const isUserInfoData = () => userInfo.email !== '' && userInfo.password !== ''; + + const handleSubmit = (e: FormEvent, isSignUp: boolean) => { + e.preventDefault(); + if (isUserInfoData()) isSignUp ? handleSignUp() : handleLogin(); + }; + + return { + userInfo, + handleChange, + handleSubmit, + }; +} diff --git a/src/components/features/Theme/ThemeHeader/index.stories.tsx b/src/features/Category/CategoryHeader/index.stories.tsx similarity index 69% rename from src/components/features/Theme/ThemeHeader/index.stories.tsx rename to src/features/Category/CategoryHeader/index.stories.tsx index c93b2e198..0ef253ba8 100644 --- a/src/components/features/Theme/ThemeHeader/index.stories.tsx +++ b/src/features/Category/CategoryHeader/index.stories.tsx @@ -1,16 +1,12 @@ import React, { ReactNode } from 'react'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { Meta, StoryObj } from '@storybook/react'; +import { CategoryLocationState } from '@internalTypes/dataTypes'; import { ROUTE_PATH } from '@routes/path'; -import ThemeHeader from '.'; +import CategoryHeader from '.'; interface MockUseLocationDecoratorProps { - state: { - title: string; - label: string; - description?: string; - backgroundColor: string; - }; + state: CategoryLocationState; children: ReactNode; } @@ -24,18 +20,17 @@ function MockUseLocationDecorator({ state, children }: MockUseLocationDecoratorP ); } -const meta: Meta = { - title: 'features/Theme/ThemeHeader', - component: ThemeHeader, +const meta: Meta = { + title: 'features/Category/CategoryHeader', + component: CategoryHeader, tags: ['autodocs'], decorators: [ (Story) => ( @@ -46,6 +41,6 @@ const meta: Meta = { export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = {}; diff --git a/src/components/features/Theme/ThemeHeader/index.tsx b/src/features/Category/CategoryHeader/index.tsx similarity index 66% rename from src/components/features/Theme/ThemeHeader/index.tsx rename to src/features/Category/CategoryHeader/index.tsx index 42f729fc6..e15987580 100644 --- a/src/components/features/Theme/ThemeHeader/index.tsx +++ b/src/features/Category/CategoryHeader/index.tsx @@ -3,39 +3,32 @@ import styled from '@emotion/styled'; import { CenteredContainer } from '@components/common'; import { useLocation, useNavigate } from 'react-router-dom'; import { ROUTE_PATH } from '@routes/path'; +import { CategoryLocationState } from '@internalTypes/dataTypes'; -interface LocationState { - title: string; - label: string; - description?: string; - backgroundColor: string; -} - -export default function ThemeHeader() { +export default function CategoryHeader() { const location = useLocation(); const navigate = useNavigate(); - const state = location.state as LocationState | null; + const state = location.state as CategoryLocationState | null; useEffect(() => { if (!state) navigate(ROUTE_PATH.HOME); }, [state, navigate]); if (!state) return null; - - const { title, label, description, backgroundColor } = state; + const { name, color, description } = state; return ( - + - - {title} + + {name} {description} - + ); } -const ThemeHeaderContainer = styled.section<{ color?: string }>` +const CategoryHeaderContainer = styled.section<{ color?: string }>` margin-top: 60px; background-color: ${({ color }) => color}; padding: 50px 0; diff --git a/src/components/features/Theme/GoodsItemList/index.stories.tsx b/src/features/Category/GoodsItemList/index.stories.tsx similarity index 94% rename from src/components/features/Theme/GoodsItemList/index.stories.tsx rename to src/features/Category/GoodsItemList/index.stories.tsx index be39b1233..615b2546f 100644 --- a/src/components/features/Theme/GoodsItemList/index.stories.tsx +++ b/src/features/Category/GoodsItemList/index.stories.tsx @@ -8,7 +8,7 @@ import GoodsItemList from '.'; const queryClient = new QueryClient(); const meta: Meta = { - title: 'features/Theme/GoodsItemList', + title: 'features/Category/GoodsItemList', component: GoodsItemList, tags: ['autodocs'], decorators: [ diff --git a/src/components/features/Theme/GoodsItemList/index.tsx b/src/features/Category/GoodsItemList/index.tsx similarity index 66% rename from src/components/features/Theme/GoodsItemList/index.tsx rename to src/features/Category/GoodsItemList/index.tsx index 1e079ee0b..f715a0f48 100644 --- a/src/components/features/Theme/GoodsItemList/index.tsx +++ b/src/features/Category/GoodsItemList/index.tsx @@ -1,25 +1,34 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import styled from '@emotion/styled'; import { GoodsItem, Grid, CenteredContainer, StatusHandler } from '@components/common'; import { Link, useParams } from 'react-router-dom'; -import useGoodsItemListQuery from '@hooks/useGoodsItemListQuery'; import useInfiniteScroll from '@hooks/useInfiniteScroll'; -import { ThemeProductsRequest } from '@internalTypes/requestTypes'; import { getDynamicPath } from '@utils/getDynamicPath'; import { ROUTE_PATH } from '@routes/path'; +import { useGetProducts } from '@apis/products/hooks/useGetProducts'; const GRID_GAP = 14; const GRID_COLUMNS = 4; -const MAX_RESULTS = 20; +const MAX_SIZE = 10; export default function GoodsItemList() { - const { themeKey } = useParams>(); - const { products, isLoading, isError, error, fetchNextPage, isFetchingNextPage, hasNextPage } = useGoodsItemListQuery( - { themeKey, rowsPerPage: MAX_RESULTS }, - ); + const { categoryId } = useParams<{ categoryId: string }>(); + const { + data: products, + isLoading, + isError, + error, + fetchNextPage, + isFetchingNextPage, + hasNextPage, + } = useGetProducts({ + categoryId: Number(categoryId), + size: MAX_SIZE, + sort: 'name,asc', + }); const ref = useInfiniteScroll({ condition: hasNextPage && !isFetchingNextPage, fetchNextPage }); - const isEmpty = products.length === 0; + const isEmpty = products?.pages[0].content.length === 0; return ( @@ -32,12 +41,12 @@ export default function GoodsItemList() { isFetchingNextPage={isFetchingNextPage} > - {products.map((product) => ( + {products?.pages[0].content.map((product) => ( diff --git a/src/features/Home/Categories/CategoryItem/index.stories.ts b/src/features/Home/Categories/CategoryItem/index.stories.ts new file mode 100644 index 000000000..3f6420e67 --- /dev/null +++ b/src/features/Home/Categories/CategoryItem/index.stories.ts @@ -0,0 +1,24 @@ +import image from '@assets/images/theme.jpeg'; +import { Meta, StoryObj } from '@storybook/react'; +import CategoryItem, { CategoryItemProps } from '.'; + +const meta: Meta = { + title: 'features/Home/Categories/CategoryItem', + component: CategoryItem, + tags: ['autodocs'], + argTypes: { + name: { control: { type: 'text' } }, + imageUrl: { control: { type: 'text' } }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + label: '럭셔리', + image, + }, +}; diff --git a/src/components/features/Home/ThemeCategory/ThemeItem/index.tsx b/src/features/Home/Categories/CategoryItem/index.tsx similarity index 60% rename from src/components/features/Home/ThemeCategory/ThemeItem/index.tsx rename to src/features/Home/Categories/CategoryItem/index.tsx index 844ecaa00..6705dfd6f 100644 --- a/src/components/features/Home/ThemeCategory/ThemeItem/index.tsx +++ b/src/features/Home/Categories/CategoryItem/index.tsx @@ -5,17 +5,17 @@ import { Container, Image } from '@components/common'; const IMAGE_SIZE = 90; const IMAGE_RADIUS = 32; -export interface ThemeItemProps { - image: string; - label: string; +export interface CategoryItemProps { + imageUrl: string; + name: string; } -export default function ThemeItem({ image, label }: ThemeItemProps) { +export default function CategoryItem({ imageUrl, name }: CategoryItemProps) { return ( - {label} - {label} + {name} + {name} ); diff --git a/src/components/features/Home/ThemeCategory/index.stories.tsx b/src/features/Home/Categories/index.stories.tsx similarity index 75% rename from src/components/features/Home/ThemeCategory/index.stories.tsx rename to src/features/Home/Categories/index.stories.tsx index 9ef8477fe..e523f2a3a 100644 --- a/src/components/features/Home/ThemeCategory/index.stories.tsx +++ b/src/features/Home/Categories/index.stories.tsx @@ -3,13 +3,13 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { MemoryRouter } from 'react-router-dom'; import { Meta, StoryObj } from '@storybook/react'; import GlobalStyles from '@assets/styles'; -import ThemeCategory from '.'; +import Categories from '.'; const queryClient = new QueryClient(); -const meta: Meta = { - title: 'features/Home/ThemeCategory', - component: ThemeCategory, +const meta: Meta = { + title: 'features/Home/Categories', + component: Categories, tags: ['autodocs'], decorators: [ (Story) => ( @@ -25,6 +25,6 @@ const meta: Meta = { export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = {}; diff --git a/src/features/Home/Categories/index.tsx b/src/features/Home/Categories/index.tsx new file mode 100644 index 000000000..41f9a6cb1 --- /dev/null +++ b/src/features/Home/Categories/index.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { Grid, CenteredContainer, StatusHandler } from '@components/common'; +import { useGetCategories } from '@apis/categories/hooks/useGetCategories'; +import { CategoryLocationState } from '@internalTypes/dataTypes'; +import { Link } from 'react-router-dom'; +import { ROUTE_PATH } from '@routes/path'; +import { getDynamicPath } from '@utils/getDynamicPath'; +import ThemeItem from './CategoryItem'; + +const GRID_GAP = 0; +const GRID_COLUMNS = 6; + +export default function Categories() { + const { data, isError, isLoading, error } = useGetCategories(); + const isEmpty = !data || data?.length === 0; + + return ( + + + + + {data?.map((category) => ( + + + + ))} + + + + + ); +} + +const CategoriesContainer = styled.section` + padding-top: 45px; + padding-bottom: 23px; +`; diff --git a/src/components/features/Home/Filter/Target/TargetItem/index.stories.ts b/src/features/Home/Filter/Target/TargetItem/index.stories.ts similarity index 100% rename from src/components/features/Home/Filter/Target/TargetItem/index.stories.ts rename to src/features/Home/Filter/Target/TargetItem/index.stories.ts diff --git a/src/components/features/Home/Filter/Target/TargetItem/index.tsx b/src/features/Home/Filter/Target/TargetItem/index.tsx similarity index 100% rename from src/components/features/Home/Filter/Target/TargetItem/index.tsx rename to src/features/Home/Filter/Target/TargetItem/index.tsx diff --git a/src/components/features/Home/Filter/Target/index.stories.tsx b/src/features/Home/Filter/Target/index.stories.tsx similarity index 100% rename from src/components/features/Home/Filter/Target/index.stories.tsx rename to src/features/Home/Filter/Target/index.stories.tsx diff --git a/src/components/features/Home/Filter/Target/index.tsx b/src/features/Home/Filter/Target/index.tsx similarity index 100% rename from src/components/features/Home/Filter/Target/index.tsx rename to src/features/Home/Filter/Target/index.tsx diff --git a/src/components/features/Home/Filter/Wish/WishItem/index.stories.ts b/src/features/Home/Filter/Wish/WishItem/index.stories.ts similarity index 100% rename from src/components/features/Home/Filter/Wish/WishItem/index.stories.ts rename to src/features/Home/Filter/Wish/WishItem/index.stories.ts diff --git a/src/components/features/Home/Filter/Wish/WishItem/index.tsx b/src/features/Home/Filter/Wish/WishItem/index.tsx similarity index 100% rename from src/components/features/Home/Filter/Wish/WishItem/index.tsx rename to src/features/Home/Filter/Wish/WishItem/index.tsx diff --git a/src/components/features/Home/Filter/Wish/index.stories.tsx b/src/features/Home/Filter/Wish/index.stories.tsx similarity index 100% rename from src/components/features/Home/Filter/Wish/index.stories.tsx rename to src/features/Home/Filter/Wish/index.stories.tsx diff --git a/src/components/features/Home/Filter/Wish/index.tsx b/src/features/Home/Filter/Wish/index.tsx similarity index 100% rename from src/components/features/Home/Filter/Wish/index.tsx rename to src/features/Home/Filter/Wish/index.tsx diff --git a/src/components/features/Home/Filter/constants.ts b/src/features/Home/Filter/constants.ts similarity index 100% rename from src/components/features/Home/Filter/constants.ts rename to src/features/Home/Filter/constants.ts diff --git a/src/components/features/Home/Filter/index.stories.tsx b/src/features/Home/Filter/index.stories.tsx similarity index 100% rename from src/components/features/Home/Filter/index.stories.tsx rename to src/features/Home/Filter/index.stories.tsx diff --git a/src/components/features/Home/Filter/index.tsx b/src/features/Home/Filter/index.tsx similarity index 100% rename from src/components/features/Home/Filter/index.tsx rename to src/features/Home/Filter/index.tsx diff --git a/src/components/features/Home/FriendGiftRecommendation/index.stories.ts b/src/features/Home/FriendGiftRecommendation/index.stories.ts similarity index 100% rename from src/components/features/Home/FriendGiftRecommendation/index.stories.ts rename to src/features/Home/FriendGiftRecommendation/index.stories.ts diff --git a/src/components/features/Home/FriendGiftRecommendation/index.tsx b/src/features/Home/FriendGiftRecommendation/index.tsx similarity index 100% rename from src/components/features/Home/FriendGiftRecommendation/index.tsx rename to src/features/Home/FriendGiftRecommendation/index.tsx diff --git a/src/components/features/Home/FriendSelector/index.stories.ts b/src/features/Home/FriendSelector/index.stories.ts similarity index 100% rename from src/components/features/Home/FriendSelector/index.stories.ts rename to src/features/Home/FriendSelector/index.stories.ts diff --git a/src/components/features/Home/FriendSelector/index.tsx b/src/features/Home/FriendSelector/index.tsx similarity index 100% rename from src/components/features/Home/FriendSelector/index.tsx rename to src/features/Home/FriendSelector/index.tsx diff --git a/src/components/features/Home/RankingList/index.stories.tsx b/src/features/Home/RankingList/index.stories.tsx similarity index 100% rename from src/components/features/Home/RankingList/index.stories.tsx rename to src/features/Home/RankingList/index.stories.tsx diff --git a/src/components/features/Home/RankingList/index.tsx b/src/features/Home/RankingList/index.tsx similarity index 77% rename from src/components/features/Home/RankingList/index.tsx rename to src/features/Home/RankingList/index.tsx index b3a19e108..3668d0051 100644 --- a/src/components/features/Home/RankingList/index.tsx +++ b/src/features/Home/RankingList/index.tsx @@ -3,10 +3,7 @@ import styled from '@emotion/styled'; import { Grid, Button, GoodsItem, StatusHandler } from '@components/common'; import useToggle from '@hooks/useToggle'; import { useFilter } from '@context/filter/useFilter'; -import { useQuery } from '@tanstack/react-query'; -import { RankingProductsResponse } from '@internalTypes/responseTypes'; -import { getRankingProducts } from '@apis/ranking'; -import { AxiosError } from 'axios'; +import { useGetRankingProducts } from '@apis/ranking/useGetRankingProducts'; const INITIAL_DISPLAY_COUNT = 6; const GRID_GAP = 14; @@ -15,10 +12,9 @@ const GRID_COLUMNS = 6; export default function RankingList() { const [showAll, toggleShowAll] = useToggle(false); const { selectedTarget, selectedWish } = useFilter(); - - const { isLoading, isError, error, data } = useQuery({ - queryKey: ['rankingProducts', selectedTarget, selectedWish], - queryFn: () => getRankingProducts({ targetType: selectedTarget, rankType: selectedWish }), + const { isLoading, isError, error, data } = useGetRankingProducts({ + targetType: selectedTarget, + rankType: selectedWish, }); const displayedProducts = showAll ? data?.products : data?.products.slice(0, INITIAL_DISPLAY_COUNT); diff --git a/src/components/features/Home/TrendingGifts/index.stories.tsx b/src/features/Home/TrendingGifts/index.stories.tsx similarity index 100% rename from src/components/features/Home/TrendingGifts/index.stories.tsx rename to src/features/Home/TrendingGifts/index.stories.tsx diff --git a/src/components/features/Home/TrendingGifts/index.tsx b/src/features/Home/TrendingGifts/index.tsx similarity index 80% rename from src/components/features/Home/TrendingGifts/index.tsx rename to src/features/Home/TrendingGifts/index.tsx index 4d5a2efdd..8fa0bf5ef 100644 --- a/src/components/features/Home/TrendingGifts/index.tsx +++ b/src/features/Home/TrendingGifts/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; import styled from '@emotion/styled'; -import { CenteredContainer } from '@components/common/'; -import { RankingList, Filter } from '@components/features/Home'; +import { CenteredContainer } from '@components/common'; +import { RankingList, Filter } from '@features/Home'; export default function TrendingGifts() { return ( diff --git a/src/components/features/Home/index.ts b/src/features/Home/index.ts similarity index 83% rename from src/components/features/Home/index.ts rename to src/features/Home/index.ts index 3b3e9acd3..17a5cd93d 100644 --- a/src/components/features/Home/index.ts +++ b/src/features/Home/index.ts @@ -2,5 +2,5 @@ export { default as Filter } from './Filter'; export { default as FriendSelector } from './FriendSelector'; export { default as FriendGiftRecommendation } from './FriendGiftRecommendation'; export { default as RankingList } from './RankingList'; -export { default as ThemeCategory } from './ThemeCategory'; +export { default as Categories } from './Categories'; export { default as TrendingGifts } from './TrendingGifts'; diff --git a/src/components/features/Layout/Footer/index.stories.ts b/src/features/Layout/Footer/index.stories.ts similarity index 100% rename from src/components/features/Layout/Footer/index.stories.ts rename to src/features/Layout/Footer/index.stories.ts diff --git a/src/components/features/Layout/Footer/index.tsx b/src/features/Layout/Footer/index.tsx similarity index 100% rename from src/components/features/Layout/Footer/index.tsx rename to src/features/Layout/Footer/index.tsx diff --git a/src/features/Layout/Header/APISelector/index.tsx b/src/features/Layout/Header/APISelector/index.tsx new file mode 100644 index 000000000..00df480e6 --- /dev/null +++ b/src/features/Layout/Header/APISelector/index.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { useAPI } from '@context/api/useAPI'; + +export default function APISelector() { + const { setBaseURL } = useAPI(); + + const handleChange = (e: React.ChangeEvent) => { + setBaseURL(e.target.value); + }; + + return ( + + + + + + + ); +} + +const SelectorContainer = styled.select` + font-weight: 500; + font-size: 14px; + margin-right: 24px; +`; diff --git a/src/components/features/Layout/Header/AuthLinks/index.stories.tsx b/src/features/Layout/Header/AuthLinks/index.stories.tsx similarity index 100% rename from src/components/features/Layout/Header/AuthLinks/index.stories.tsx rename to src/features/Layout/Header/AuthLinks/index.stories.tsx diff --git a/src/components/features/Layout/Header/AuthLinks/index.tsx b/src/features/Layout/Header/AuthLinks/index.tsx similarity index 100% rename from src/components/features/Layout/Header/AuthLinks/index.tsx rename to src/features/Layout/Header/AuthLinks/index.tsx diff --git a/src/components/features/Layout/Header/index.stories.tsx b/src/features/Layout/Header/index.stories.tsx similarity index 100% rename from src/components/features/Layout/Header/index.stories.tsx rename to src/features/Layout/Header/index.stories.tsx diff --git a/src/components/features/Layout/Header/index.tsx b/src/features/Layout/Header/index.tsx similarity index 83% rename from src/components/features/Layout/Header/index.tsx rename to src/features/Layout/Header/index.tsx index c3dc7e732..3d3561c38 100644 --- a/src/components/features/Layout/Header/index.tsx +++ b/src/features/Layout/Header/index.tsx @@ -5,6 +5,7 @@ import { CenteredContainer, Container } from '@components/common'; import { Link } from 'react-router-dom'; import { ROUTE_PATH } from '@routes/path'; import AuthLinks from './AuthLinks'; +import APISelector from './APISelector'; const LOGO_ALT = 'home page logo'; @@ -16,7 +17,10 @@ export default function Header() { - + @@ -35,3 +39,7 @@ const HeaderContainer = styled.header` const Logo = styled.img` width: 60px; `; + +const Nav = styled.div` + display: flex; +`; diff --git a/src/components/features/Layout/index.tsx b/src/features/Layout/index.tsx similarity index 81% rename from src/components/features/Layout/index.tsx rename to src/features/Layout/index.tsx index c7bb35c07..7163730fd 100644 --- a/src/components/features/Layout/index.tsx +++ b/src/features/Layout/index.tsx @@ -1,5 +1,6 @@ import React, { ReactNode } from 'react'; -import { Header, Footer } from '@components/common'; +import Header from './Header'; +import Footer from './Footer'; export interface PageWrapperProps { children: ReactNode; diff --git a/src/components/features/Order/GiftDetail/Gift/index.stories.tsx b/src/features/Order/GiftDetail/Gift/index.stories.tsx similarity index 100% rename from src/components/features/Order/GiftDetail/Gift/index.stories.tsx rename to src/features/Order/GiftDetail/Gift/index.stories.tsx diff --git a/src/components/features/Order/GiftDetail/Gift/index.tsx b/src/features/Order/GiftDetail/Gift/index.tsx similarity index 77% rename from src/components/features/Order/GiftDetail/Gift/index.tsx rename to src/features/Order/GiftDetail/Gift/index.tsx index a7dd9009c..c6f94585d 100644 --- a/src/components/features/Order/GiftDetail/Gift/index.tsx +++ b/src/features/Order/GiftDetail/Gift/index.tsx @@ -8,18 +8,18 @@ const IMAGE_SIZE = 86; export default function Gift() { const data = sessionStorage.getItem('orderHistory'); const parsedData = data ? JSON.parse(data) : null; - const productId = parsedData?.id; - const count = parsedData?.count; + const productId = parsedData?.productId; + const quantity = parsedData?.quantity; const { data: productDetailData } = useGetProductsDetail({ productId }); return ( - +
- {productDetailData?.detail.brandInfo.name} + {productDetailData?.category.name} - {productDetailData?.detail.name} X {count}개 + {productDetailData?.name} X {quantity}개
diff --git a/src/components/features/Order/GiftDetail/index.stories.tsx b/src/features/Order/GiftDetail/index.stories.tsx similarity index 100% rename from src/components/features/Order/GiftDetail/index.stories.tsx rename to src/features/Order/GiftDetail/index.stories.tsx diff --git a/src/components/features/Order/GiftDetail/index.tsx b/src/features/Order/GiftDetail/index.tsx similarity index 100% rename from src/components/features/Order/GiftDetail/index.tsx rename to src/features/Order/GiftDetail/index.tsx diff --git a/src/components/features/Order/OrderMessage/index.stories.tsx b/src/features/Order/OrderMessage/index.stories.tsx similarity index 100% rename from src/components/features/Order/OrderMessage/index.stories.tsx rename to src/features/Order/OrderMessage/index.stories.tsx diff --git a/src/components/features/Order/OrderMessage/index.tsx b/src/features/Order/OrderMessage/index.tsx similarity index 100% rename from src/components/features/Order/OrderMessage/index.tsx rename to src/features/Order/OrderMessage/index.tsx diff --git a/src/components/features/Order/Payment/ReceiptForm/index.stories.tsx b/src/features/Order/Payment/ReceiptForm/index.stories.tsx similarity index 100% rename from src/components/features/Order/Payment/ReceiptForm/index.stories.tsx rename to src/features/Order/Payment/ReceiptForm/index.stories.tsx diff --git a/src/components/features/Order/Payment/ReceiptForm/index.test.tsx b/src/features/Order/Payment/ReceiptForm/index.test.tsx similarity index 100% rename from src/components/features/Order/Payment/ReceiptForm/index.test.tsx rename to src/features/Order/Payment/ReceiptForm/index.test.tsx diff --git a/src/features/Order/Payment/ReceiptForm/index.tsx b/src/features/Order/Payment/ReceiptForm/index.tsx new file mode 100644 index 000000000..c513f376b --- /dev/null +++ b/src/features/Order/Payment/ReceiptForm/index.tsx @@ -0,0 +1,122 @@ +import React, { useEffect, useState } from 'react'; +import styled from '@emotion/styled'; +import { Checkbox, Select, Input } from '@chakra-ui/react'; +import { Button } from '@components/common'; +import { useFormContext } from 'react-hook-form'; +import { OrderDataFormValues } from '@pages/Order'; +import { useNavigate } from 'react-router-dom'; +import { useSessionStorage } from '@hooks/useSessionStorage'; +import { useGetPoint } from '@apis/point/useGetPoint'; +import { ROUTE_PATH } from '@routes/path'; +import { useOrders } from '@apis/orders/useOrders'; +import { validatePayment } from './validation'; + +const SUCCESS_ORDER = '주문이 완료되었습니다.'; +const FAIL_ORDER = '주문이 실패하였습니다.'; + +export default function ReceiptForm() { + const [storedValue, setValue] = useSessionStorage('orderHistory', ''); + const { optionId, quantity, price } = JSON.parse(storedValue); + const [point, setPoint] = useState(0); + const [totalPrice, setTotalPrice] = useState(price); + const navigate = useNavigate(); + const { register, watch, handleSubmit } = useFormContext(); + const { hasCashReceipt } = watch(); + const { mutate } = useOrders(); + const { data: pointData } = useGetPoint(); + if (pointData) setPoint(pointData.point); + + useEffect(() => { + if (pointData) { + setPoint(pointData.point); + } + }, [pointData]); + + const handleOrders = (message: string) => { + mutate( + { message, optionId, quantity, usedPoint: point }, + { + onSuccess: () => { + alert(SUCCESS_ORDER); + navigate(ROUTE_PATH.HOME); + }, + onError: () => alert(FAIL_ORDER), + }, + ); + }; + + const onSubmit = (data: OrderDataFormValues) => { + const errorMessage = validatePayment(data.message, data.hasCashReceipt, data.cashReceiptNumber); + if (errorMessage) return alert(errorMessage); + handleOrders(data.message); + }; + + return ( +
+ + 현금영수증 신청 + + {hasCashReceipt && ( + <> + + + + )} + +
+
사용 가능한 포인트
+
{`${point} 포인트`}
+
최종 결제금액
+
{`${price}원`}
+
+
+ + + + +
+ ); +} + +const TotalAmount = styled.div` + padding: 18px 20px; + border-radius: 4px; + background-color: rgb(245, 245, 245); + margin-bottom: 20px; + + dl { + display: flex; + flex-direction: column; + font-weight: 700; + } + + dt { + font-size: 14px; + } + + dd { + font-size: 20px; + margin-bottom: 24px; + } + + dd:last-of-type { + margin-bottom: 0; + } +`; + +const ButtonContainer = styled.div` + button { + margin-bottom: 24px; + } + + button:last-of-type { + margin-bottom: 0; + } +`; diff --git a/src/components/features/Order/Payment/ReceiptForm/validation.ts b/src/features/Order/Payment/ReceiptForm/validation.ts similarity index 100% rename from src/components/features/Order/Payment/ReceiptForm/validation.ts rename to src/features/Order/Payment/ReceiptForm/validation.ts diff --git a/src/components/features/Order/Payment/index.stories.tsx b/src/features/Order/Payment/index.stories.tsx similarity index 100% rename from src/components/features/Order/Payment/index.stories.tsx rename to src/features/Order/Payment/index.stories.tsx diff --git a/src/components/features/Order/Payment/index.tsx b/src/features/Order/Payment/index.tsx similarity index 100% rename from src/components/features/Order/Payment/index.tsx rename to src/features/Order/Payment/index.tsx diff --git a/src/components/features/Product/ProductInfo/index.stories.tsx b/src/features/Product/ProductInfo/index.stories.tsx similarity index 100% rename from src/components/features/Product/ProductInfo/index.stories.tsx rename to src/features/Product/ProductInfo/index.stories.tsx diff --git a/src/components/features/Product/ProductInfo/index.tsx b/src/features/Product/ProductInfo/index.tsx similarity index 85% rename from src/components/features/Product/ProductInfo/index.tsx rename to src/features/Product/ProductInfo/index.tsx index aa28bc2a4..b2b0e1ae2 100644 --- a/src/components/features/Product/ProductInfo/index.tsx +++ b/src/features/Product/ProductInfo/index.tsx @@ -7,17 +7,17 @@ import WishButton from '../WishButton'; const IMAGE_SIZE = 450; interface ProductInfoProps { + imageUrl?: string; name?: string; - image?: string; price?: number; } -export default function ProductInfo({ name, image, price }: ProductInfoProps) { +export default function ProductInfo({ imageUrl, name, price }: ProductInfoProps) { return (
- +
{name} {price}원 diff --git a/src/components/features/Product/ProductOrder/QuantitySelector/index.stories.tsx b/src/features/Product/ProductOrder/OptionItem/QuantitySelector/index.stories.tsx similarity index 88% rename from src/components/features/Product/ProductOrder/QuantitySelector/index.stories.tsx rename to src/features/Product/ProductOrder/OptionItem/QuantitySelector/index.stories.tsx index cae9f492a..86d172eda 100644 --- a/src/components/features/Product/ProductOrder/QuantitySelector/index.stories.tsx +++ b/src/features/Product/ProductOrder/OptionItem/QuantitySelector/index.stories.tsx @@ -2,12 +2,12 @@ import React from 'react'; import { Meta, StoryObj } from '@storybook/react'; import { ChakraProvider } from '@chakra-ui/react'; import { useForm, FormProvider } from 'react-hook-form'; +import { QuantityValues } from '@features/Product/ProductOrder'; import GlobalStyles from '@assets/styles'; import QuantitySelector from '.'; -import { QuantityValues } from '..'; const meta: Meta = { - title: 'features/Product/ProductOrder/QuantitySelector', + title: 'features/Product/ProductOrder/OptionItem/QuantitySelector', component: QuantitySelector, tags: ['autodocs'], decorators: [ diff --git a/src/components/features/Product/ProductOrder/QuantitySelector/index.tsx b/src/features/Product/ProductOrder/OptionItem/QuantitySelector/index.tsx similarity index 84% rename from src/components/features/Product/ProductOrder/QuantitySelector/index.tsx rename to src/features/Product/ProductOrder/OptionItem/QuantitySelector/index.tsx index 3cccd9e40..62d2f2a3c 100644 --- a/src/components/features/Product/ProductOrder/QuantitySelector/index.tsx +++ b/src/features/Product/ProductOrder/OptionItem/QuantitySelector/index.tsx @@ -1,19 +1,19 @@ import React, { useEffect, useState } from 'react'; import { Input, Button, useNumberInput, HStack } from '@chakra-ui/react'; import { UseFormSetValue } from 'react-hook-form'; -import { QuantityValues } from '..'; +import { QuantityValues } from '../../index'; interface QuantitySelectorProps { - giftOrderLimit?: number; + quantity?: number; setValue: UseFormSetValue; } -export default function QuantitySelector({ giftOrderLimit, setValue }: QuantitySelectorProps) { +export default function QuantitySelector({ quantity, setValue }: QuantitySelectorProps) { const { getInputProps, getIncrementButtonProps, getDecrementButtonProps, valueAsNumber } = useNumberInput({ step: 1, defaultValue: 1, min: 1, - max: giftOrderLimit, + max: quantity, }); const [inputValue, setInputValue] = useState('1'); @@ -23,7 +23,7 @@ export default function QuantitySelector({ giftOrderLimit, setValue }: QuantityS const input = getInputProps({ 'data-testid': 'option-input' }); useEffect(() => { - setValue('count', valueAsNumber); + setValue('quantity', valueAsNumber); }, [valueAsNumber, setValue]); const handleIncrement = () => { diff --git a/src/features/Product/ProductOrder/OptionItem/index.tsx b/src/features/Product/ProductOrder/OptionItem/index.tsx new file mode 100644 index 000000000..3114ba642 --- /dev/null +++ b/src/features/Product/ProductOrder/OptionItem/index.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { UseFormSetValue } from 'react-hook-form'; +import { QuantityValues } from '..'; +import QuantitySelector from './QuantitySelector'; + +interface OptionItemProps { + name: string; + quantity: number; + setValue: UseFormSetValue; +} + +export default function OptionItem({ name, quantity, setValue }: OptionItemProps) { + return ( + + {name} + + + ); +} + +const OptionItemContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: 12px 14px 16px; + border: 1px solid rgb(237, 237, 237); + border-radius: 2px; + margin-bottom: 16px; + + &:last-of-type { + margin-bottom: 0; + } +`; + +const Title = styled.p` + font-weight: 700; + margin-bottom: 12px; +`; diff --git a/src/components/features/Product/ProductOrder/index.stories.tsx b/src/features/Product/ProductOrder/index.stories.tsx similarity index 100% rename from src/components/features/Product/ProductOrder/index.stories.tsx rename to src/features/Product/ProductOrder/index.stories.tsx diff --git a/src/components/features/Product/ProductOrder/index.tsx b/src/features/Product/ProductOrder/index.tsx similarity index 66% rename from src/components/features/Product/ProductOrder/index.tsx rename to src/features/Product/ProductOrder/index.tsx index 2dbf3738c..dfd3cb19d 100644 --- a/src/components/features/Product/ProductOrder/index.tsx +++ b/src/features/Product/ProductOrder/index.tsx @@ -5,31 +5,37 @@ import { ROUTE_PATH } from '@routes/path'; import { useNavigate, useParams } from 'react-router-dom'; import { useAuth } from '@context/auth/useAuth'; import { Button } from '@components/common'; -import QuantitySelector from './QuantitySelector'; +import { useGetProductsOption } from '@apis/products/hooks/useGetProductsOption'; +import OptionItem from './OptionItem'; -interface ProductOrderProps { - name?: string; - giftOrderLimit?: number; +export interface QuantityValues { + quantity: number; } -export interface QuantityValues { - count: number; +interface ProductOrderProps { + price: number; } -export default function ProductOrder({ name, giftOrderLimit }: ProductOrderProps) { +export default function ProductOrder({ price }: ProductOrderProps) { + const { productId } = useParams<{ productId: string }>(); + const { data: productOption } = useGetProductsOption({ productId }); + const { isAuthenticated } = useAuth(); + const navigate = useNavigate(); const { watch, setValue } = useForm({ defaultValues: { - count: 1, + quantity: 1, }, }); - const { isAuthenticated } = useAuth(); - const navigate = useNavigate(); - const { productId } = useParams<{ productId: string }>(); const handleOrderClick = () => { - const data = { count: watch('count') }; - if (productId) { - const orderHistory = { id: Number(productId), count: data.count }; + const data = { quantity: watch('quantity') }; + if (productOption) { + const orderHistory = { + productId: Number(productId), + optionId: productOption[0].id, + quantity: data.quantity, + price, + }; sessionStorage.setItem('orderHistory', JSON.stringify(orderHistory)); const targetPath = isAuthenticated ? ROUTE_PATH.ORDER : ROUTE_PATH.LOGIN; navigate(targetPath); @@ -38,15 +44,16 @@ export default function ProductOrder({ name, giftOrderLimit }: ProductOrderProps return ( - - {name} - - + + {productOption?.map((option) => ( + + ))} +
총 결제 금액
-
145000원
+
{`${price}원`}