From aaffa82c81050b1baab04d8a4cd7887f169d63ed Mon Sep 17 00:00:00 2001 From: hookor Date: Sun, 2 Jul 2023 23:20:36 +0900 Subject: [PATCH 1/3] Submission --- .eslintrc.cjs | 16 + .gitignore | 28 + .prettierrc | 9 + README.md | 1396 +---------------- index.html | 46 + netlify.toml | 4 + package.json | 52 + public/ico_coupon.png | Bin 0 -> 605 bytes public/ico_orders.png | Bin 0 -> 723 bytes public/ico_won.png | Bin 0 -> 1035 bytes public/instagramIcon.svg | 1 + public/no-photo.png | Bin 0 -> 2537 bytes src/api/adminRequests.ts | 130 ++ src/api/axios.ts | 16 + src/api/index.ts | 6 + src/api/mypageRequests.ts | 20 + src/api/paymentRequests.ts | 47 + src/api/productRequests.ts | 14 + src/api/signApi.ts | 22 + src/components/App.tsx | 126 ++ src/components/admin/AdminCustomerItem.tsx | 69 + .../admin/AdminCustomerSkeleton.tsx | 18 + src/components/admin/AdminDashboardCard.tsx | 22 + src/components/admin/AdminLoading.tsx | 19 + src/components/admin/AdminMoreButton.tsx | 48 + src/components/admin/AdminNav.tsx | 34 + src/components/admin/AdminPrivateRoute.tsx | 84 + src/components/admin/AdminProductItem.tsx | 84 + .../admin/AdminProductItemHeader.tsx | 14 + .../admin/AdminProductsSkeleton.tsx | 18 + src/components/admin/AdminSalesItem.tsx | 74 + src/components/admin/AdminSalesSkeleton.tsx | 18 + src/components/admin/ProductAddForm.tsx | 246 +++ src/components/admin/index.ts | 13 + src/components/cart/CartFooter.tsx | 57 + src/components/cart/CartHeader.tsx | 32 + src/components/cart/CartItem.tsx | 97 ++ src/components/cart/CartProducts.tsx | 96 ++ src/components/cart/CartSummary.tsx | 97 ++ src/components/cart/index.ts | 5 + src/components/common/ErrorComponent.tsx | 11 + src/components/common/Footer.tsx | 93 ++ src/components/common/Header.tsx | 267 ++++ src/components/common/Modal.tsx | 34 + src/components/common/NotFound.tsx | 3 + src/components/common/index.ts | 5 + src/components/index.ts | 6 + src/components/main/Badge.tsx | 34 + src/components/main/BestSeller.tsx | 110 ++ src/components/main/ColleyNews.tsx | 68 + src/components/main/ImageSlider.tsx | 52 + src/components/main/NewArrival.tsx | 104 ++ src/components/main/PromotionSlider.tsx | 52 + src/components/main/RecentlyList.tsx | 64 + src/components/main/index.ts | 7 + src/components/mypage/MyOrderItem.tsx | 67 + src/components/mypage/MyOrderItemSkeleton.tsx | 17 + src/components/mypage/MyOrderList.tsx | 150 ++ src/components/mypage/MyOrderStatus.tsx | 57 + src/components/mypage/MyOrderSummary.tsx | 85 + src/components/mypage/MyPageNav.tsx | 71 + src/components/mypage/MyWishItem.tsx | 54 + src/components/mypage/MyWishList.tsx | 95 ++ src/components/mypage/index.ts | 8 + src/components/payment/BankSelection.tsx | 93 ++ src/components/payment/Confirmation.tsx | 133 ++ src/components/payment/DaumPostCode.tsx | 74 + src/components/payment/PaymentMethods.tsx | 199 +++ src/components/payment/PriceInfo.tsx | 50 + src/components/payment/ProductInfo.tsx | 35 + src/components/payment/UserAddress.tsx | 90 ++ src/components/payment/index.ts | 8 + src/components/product/BestProduct.tsx | 56 + .../product/ProductDetailInfoTab.tsx | 56 + src/components/product/Products.tsx | 170 ++ src/components/product/index.ts | 3 + src/constants/detailTabItems.ts | 65 + src/constants/errors.ts | 12 + src/constants/imageLinks.ts | 11 + src/constants/index.ts | 4 + src/constants/tag.ts | 17 + src/contexts/AccountContext.tsx | 17 + src/contexts/BankContext.tsx | 10 + src/contexts/CartContext.tsx | 16 + src/contexts/ClickedContext.tsx | 8 + src/contexts/LoginContext.tsx | 8 + src/contexts/LoginedUserContext.tsx | 10 + src/contexts/PhoneNumberContext.tsx | 10 + src/contexts/RecentlyContext.tsx | 9 + src/contexts/UserAddressContext.tsx | 18 + src/contexts/UseremailContext.tsx | 10 + src/contexts/UsernameContext.tsx | 8 + src/contexts/WishListContext.tsx | 9 + src/contexts/index.ts | 11 + src/hooks/index.ts | 6 + src/hooks/useAxiosInterceptor.ts | 128 ++ src/hooks/useCartLocalStorage.ts | 48 + src/hooks/useLocalStorage.ts | 35 + src/hooks/useOnClickOutside.ts | 30 + src/hooks/useSessionStorage.ts | 14 + src/main.tsx | 7 + src/pages/Home.tsx | 32 + src/pages/Router.tsx | 128 ++ src/pages/admin/AdminCustomers.tsx | 137 ++ src/pages/admin/AdminDashboard.tsx | 112 ++ src/pages/admin/AdminProductAdd.tsx | 72 + src/pages/admin/AdminProducts.tsx | 264 ++++ src/pages/admin/AdminSales.tsx | 138 ++ src/pages/admin/index.ts | 5 + src/pages/index.ts | 6 + src/pages/login/SignInPage.tsx | 151 ++ src/pages/login/SignUpPage.tsx | 205 +++ src/pages/login/index.ts | 2 + src/pages/mypage/Cart.tsx | 14 + src/pages/mypage/ModifyPassword.tsx | 126 ++ src/pages/mypage/MyOrders.tsx | 57 + src/pages/mypage/Order.tsx | 58 + src/pages/mypage/WishList.tsx | 30 + src/pages/mypage/index.ts | 5 + src/pages/payment/Payment.tsx | 52 + src/pages/payment/Success.tsx | 53 + src/pages/payment/index.ts | 2 + src/pages/product/ProductDetail.tsx | 293 ++++ src/pages/product/ProductList.tsx | 99 ++ src/pages/product/index.ts | 2 + src/styles/abstracts/mixins.scss | 67 + src/styles/abstracts/variables.scss | 63 + src/styles/common.scss | 172 ++ src/styles/components/Modal.module.scss | 68 + .../components/admin/adminCard.module.scss | 45 + .../admin/adminSalesItem.module.scss | 93 ++ .../admin/adminSkeleton.module.scss | 104 ++ .../components/admin/customerItem.module.scss | 71 + .../admin/productAddForm.module.scss | 74 + .../components/admin/productItem.module.scss | 121 ++ .../admin/productItemHeader.module.scss | 45 + src/styles/components/badge.module.scss | 63 + src/styles/components/cart/cart.module.scss | 12 + .../components/cart/cartFooter.module.scss | 37 + .../components/cart/cartHeader.module.scss | 86 + .../components/cart/cartItem.module.scss | 86 + .../components/cart/cartProducts.module.scss | 51 + .../components/cart/cartSummary.module.scss | 73 + .../components/errorComponent.module.scss | 29 + .../components/main/recentlyList.module.scss | 49 + .../components/mypage/myOrderItem.module.scss | 105 ++ .../components/mypage/myOrderList.module.scss | 23 + .../mypage/myOrderStatus.module.scss | 65 + .../mypage/myOrderSummary.module.scss | 63 + .../components/mypage/myWishItem.module.scss | 60 + .../components/mypage/myWishList.module.scss | 34 + .../components/mypage/mypage.module.scss | 7 + .../components/mypage/mypageNav.module.scss | 45 + .../components/mypage/mypageroute.module.scss | 4 + .../payment/BankSelection.module.scss | 12 + .../payment/Confirmation.module.scss | 36 + .../payment/DaumPostCode.module.scss | 11 + .../components/payment/Payment.module.scss | 14 + .../payment/PaymentMethods.module.scss | 76 + .../components/payment/PriceInfo.module.scss | 53 + .../payment/ProductInfo.module.scss | 48 + .../payment/UserAddress.module.scss | 42 + .../product/productDetailInfoTab.module.scss | 46 + src/styles/layout/BestSeller.scss | 107 ++ src/styles/layout/ColleyNews.scss | 55 + src/styles/layout/ImageSlider.scss | 45 + src/styles/layout/NewArrival.scss | 91 ++ src/styles/layout/ProductDetail.scss | 239 +++ src/styles/layout/ProductList.scss | 110 ++ src/styles/layout/PromotionSlider.scss | 45 + src/styles/layout/footer.module.scss | 111 ++ src/styles/layout/header.module.scss | 147 ++ src/styles/pages/admin.module.scss | 189 +++ src/styles/pages/adminCustomers.module.scss | 50 + src/styles/pages/adminDashboard.module.scss | 17 + src/styles/pages/adminProductAdd.module.scss | 1 + src/styles/pages/adminProducts.module.scss | 19 + src/styles/pages/adminSales.module.scss | 48 + src/styles/pages/modifyPassword.module.scss | 63 + src/styles/pages/myOrders.module.scss | 60 + src/styles/pages/myWishList.module.scss | 61 + src/styles/pages/signin.module.scss | 47 + src/styles/pages/signup.module.scss | 60 + src/styles/pages/success.module.scss | 90 ++ src/types/Address.interface.ts | 41 + src/types/AdminDashboardCardProps.type.ts | 6 + src/types/AdminMoreButtonProps.type.ts | 10 + src/types/AdminSalesItemProps.type.ts | 7 + src/types/BankAccounts.interface.ts | 34 + src/types/BestProductProps.type.ts | 6 + src/types/BodyInfo.interface.ts | 5 + src/types/Cart.interface.ts | 13 + src/types/CommonError.interface.ts | 5 + src/types/Customer.interface.ts | 11 + src/types/IdInfo.interface.ts | 11 + src/types/ModalProps.type.ts | 15 + src/types/MyOrderItemProps.type.ts | 8 + src/types/MyWishItemProps.type.ts | 8 + src/types/Product.interface.ts | 23 + src/types/ProductAddFormProps.type.ts | 6 + src/types/ProductItemProps.type.ts | 10 + src/types/Products.interface.ts | 24 + src/types/TransactionDetail.interface.ts | 37 + src/types/index.ts | 19 + src/types/swiper/index.d.ts | 5 + src/utils/CalculateDiscountedPrice.ts | 10 + src/utils/CheckIsAdmin.ts | 4 + src/utils/convertTagColor.ts | 38 + src/utils/index.ts | 3 + src/vite-env.d.ts | 1 + tsconfig.json | 60 + tsconfig.node.json | 10 + vite.config.ts | 9 + 213 files changed, 11051 insertions(+), 1319 deletions(-) create mode 100644 .eslintrc.cjs create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 index.html create mode 100644 netlify.toml create mode 100644 package.json create mode 100644 public/ico_coupon.png create mode 100644 public/ico_orders.png create mode 100644 public/ico_won.png create mode 100644 public/instagramIcon.svg create mode 100644 public/no-photo.png create mode 100644 src/api/adminRequests.ts create mode 100644 src/api/axios.ts create mode 100644 src/api/index.ts create mode 100644 src/api/mypageRequests.ts create mode 100644 src/api/paymentRequests.ts create mode 100644 src/api/productRequests.ts create mode 100644 src/api/signApi.ts create mode 100644 src/components/App.tsx create mode 100644 src/components/admin/AdminCustomerItem.tsx create mode 100644 src/components/admin/AdminCustomerSkeleton.tsx create mode 100644 src/components/admin/AdminDashboardCard.tsx create mode 100644 src/components/admin/AdminLoading.tsx create mode 100644 src/components/admin/AdminMoreButton.tsx create mode 100644 src/components/admin/AdminNav.tsx create mode 100644 src/components/admin/AdminPrivateRoute.tsx create mode 100644 src/components/admin/AdminProductItem.tsx create mode 100644 src/components/admin/AdminProductItemHeader.tsx create mode 100644 src/components/admin/AdminProductsSkeleton.tsx create mode 100644 src/components/admin/AdminSalesItem.tsx create mode 100644 src/components/admin/AdminSalesSkeleton.tsx create mode 100644 src/components/admin/ProductAddForm.tsx create mode 100644 src/components/admin/index.ts create mode 100644 src/components/cart/CartFooter.tsx create mode 100644 src/components/cart/CartHeader.tsx create mode 100644 src/components/cart/CartItem.tsx create mode 100644 src/components/cart/CartProducts.tsx create mode 100644 src/components/cart/CartSummary.tsx create mode 100644 src/components/cart/index.ts create mode 100644 src/components/common/ErrorComponent.tsx create mode 100644 src/components/common/Footer.tsx create mode 100644 src/components/common/Header.tsx create mode 100644 src/components/common/Modal.tsx create mode 100644 src/components/common/NotFound.tsx create mode 100644 src/components/common/index.ts create mode 100644 src/components/index.ts create mode 100644 src/components/main/Badge.tsx create mode 100644 src/components/main/BestSeller.tsx create mode 100644 src/components/main/ColleyNews.tsx create mode 100644 src/components/main/ImageSlider.tsx create mode 100644 src/components/main/NewArrival.tsx create mode 100644 src/components/main/PromotionSlider.tsx create mode 100644 src/components/main/RecentlyList.tsx create mode 100644 src/components/main/index.ts create mode 100644 src/components/mypage/MyOrderItem.tsx create mode 100644 src/components/mypage/MyOrderItemSkeleton.tsx create mode 100644 src/components/mypage/MyOrderList.tsx create mode 100644 src/components/mypage/MyOrderStatus.tsx create mode 100644 src/components/mypage/MyOrderSummary.tsx create mode 100644 src/components/mypage/MyPageNav.tsx create mode 100644 src/components/mypage/MyWishItem.tsx create mode 100644 src/components/mypage/MyWishList.tsx create mode 100644 src/components/mypage/index.ts create mode 100644 src/components/payment/BankSelection.tsx create mode 100644 src/components/payment/Confirmation.tsx create mode 100644 src/components/payment/DaumPostCode.tsx create mode 100644 src/components/payment/PaymentMethods.tsx create mode 100644 src/components/payment/PriceInfo.tsx create mode 100644 src/components/payment/ProductInfo.tsx create mode 100644 src/components/payment/UserAddress.tsx create mode 100644 src/components/payment/index.ts create mode 100644 src/components/product/BestProduct.tsx create mode 100644 src/components/product/ProductDetailInfoTab.tsx create mode 100644 src/components/product/Products.tsx create mode 100644 src/components/product/index.ts create mode 100644 src/constants/detailTabItems.ts create mode 100644 src/constants/errors.ts create mode 100644 src/constants/imageLinks.ts create mode 100644 src/constants/index.ts create mode 100644 src/constants/tag.ts create mode 100644 src/contexts/AccountContext.tsx create mode 100644 src/contexts/BankContext.tsx create mode 100644 src/contexts/CartContext.tsx create mode 100644 src/contexts/ClickedContext.tsx create mode 100644 src/contexts/LoginContext.tsx create mode 100644 src/contexts/LoginedUserContext.tsx create mode 100644 src/contexts/PhoneNumberContext.tsx create mode 100644 src/contexts/RecentlyContext.tsx create mode 100644 src/contexts/UserAddressContext.tsx create mode 100644 src/contexts/UseremailContext.tsx create mode 100644 src/contexts/UsernameContext.tsx create mode 100644 src/contexts/WishListContext.tsx create mode 100644 src/contexts/index.ts create mode 100644 src/hooks/index.ts create mode 100644 src/hooks/useAxiosInterceptor.ts create mode 100644 src/hooks/useCartLocalStorage.ts create mode 100644 src/hooks/useLocalStorage.ts create mode 100644 src/hooks/useOnClickOutside.ts create mode 100644 src/hooks/useSessionStorage.ts create mode 100644 src/main.tsx create mode 100644 src/pages/Home.tsx create mode 100644 src/pages/Router.tsx create mode 100644 src/pages/admin/AdminCustomers.tsx create mode 100644 src/pages/admin/AdminDashboard.tsx create mode 100644 src/pages/admin/AdminProductAdd.tsx create mode 100644 src/pages/admin/AdminProducts.tsx create mode 100644 src/pages/admin/AdminSales.tsx create mode 100644 src/pages/admin/index.ts create mode 100644 src/pages/index.ts create mode 100644 src/pages/login/SignInPage.tsx create mode 100644 src/pages/login/SignUpPage.tsx create mode 100644 src/pages/login/index.ts create mode 100644 src/pages/mypage/Cart.tsx create mode 100644 src/pages/mypage/ModifyPassword.tsx create mode 100644 src/pages/mypage/MyOrders.tsx create mode 100644 src/pages/mypage/Order.tsx create mode 100644 src/pages/mypage/WishList.tsx create mode 100644 src/pages/mypage/index.ts create mode 100644 src/pages/payment/Payment.tsx create mode 100644 src/pages/payment/Success.tsx create mode 100644 src/pages/payment/index.ts create mode 100644 src/pages/product/ProductDetail.tsx create mode 100644 src/pages/product/ProductList.tsx create mode 100644 src/pages/product/index.ts create mode 100644 src/styles/abstracts/mixins.scss create mode 100644 src/styles/abstracts/variables.scss create mode 100644 src/styles/common.scss create mode 100644 src/styles/components/Modal.module.scss create mode 100644 src/styles/components/admin/adminCard.module.scss create mode 100644 src/styles/components/admin/adminSalesItem.module.scss create mode 100644 src/styles/components/admin/adminSkeleton.module.scss create mode 100644 src/styles/components/admin/customerItem.module.scss create mode 100644 src/styles/components/admin/productAddForm.module.scss create mode 100644 src/styles/components/admin/productItem.module.scss create mode 100644 src/styles/components/admin/productItemHeader.module.scss create mode 100644 src/styles/components/badge.module.scss create mode 100644 src/styles/components/cart/cart.module.scss create mode 100644 src/styles/components/cart/cartFooter.module.scss create mode 100644 src/styles/components/cart/cartHeader.module.scss create mode 100644 src/styles/components/cart/cartItem.module.scss create mode 100644 src/styles/components/cart/cartProducts.module.scss create mode 100644 src/styles/components/cart/cartSummary.module.scss create mode 100644 src/styles/components/errorComponent.module.scss create mode 100644 src/styles/components/main/recentlyList.module.scss create mode 100644 src/styles/components/mypage/myOrderItem.module.scss create mode 100644 src/styles/components/mypage/myOrderList.module.scss create mode 100644 src/styles/components/mypage/myOrderStatus.module.scss create mode 100644 src/styles/components/mypage/myOrderSummary.module.scss create mode 100644 src/styles/components/mypage/myWishItem.module.scss create mode 100644 src/styles/components/mypage/myWishList.module.scss create mode 100644 src/styles/components/mypage/mypage.module.scss create mode 100644 src/styles/components/mypage/mypageNav.module.scss create mode 100644 src/styles/components/mypage/mypageroute.module.scss create mode 100644 src/styles/components/payment/BankSelection.module.scss create mode 100644 src/styles/components/payment/Confirmation.module.scss create mode 100644 src/styles/components/payment/DaumPostCode.module.scss create mode 100644 src/styles/components/payment/Payment.module.scss create mode 100644 src/styles/components/payment/PaymentMethods.module.scss create mode 100644 src/styles/components/payment/PriceInfo.module.scss create mode 100644 src/styles/components/payment/ProductInfo.module.scss create mode 100644 src/styles/components/payment/UserAddress.module.scss create mode 100644 src/styles/components/product/productDetailInfoTab.module.scss create mode 100644 src/styles/layout/BestSeller.scss create mode 100644 src/styles/layout/ColleyNews.scss create mode 100644 src/styles/layout/ImageSlider.scss create mode 100644 src/styles/layout/NewArrival.scss create mode 100644 src/styles/layout/ProductDetail.scss create mode 100644 src/styles/layout/ProductList.scss create mode 100644 src/styles/layout/PromotionSlider.scss create mode 100644 src/styles/layout/footer.module.scss create mode 100644 src/styles/layout/header.module.scss create mode 100644 src/styles/pages/admin.module.scss create mode 100644 src/styles/pages/adminCustomers.module.scss create mode 100644 src/styles/pages/adminDashboard.module.scss create mode 100644 src/styles/pages/adminProductAdd.module.scss create mode 100644 src/styles/pages/adminProducts.module.scss create mode 100644 src/styles/pages/adminSales.module.scss create mode 100644 src/styles/pages/modifyPassword.module.scss create mode 100644 src/styles/pages/myOrders.module.scss create mode 100644 src/styles/pages/myWishList.module.scss create mode 100644 src/styles/pages/signin.module.scss create mode 100644 src/styles/pages/signup.module.scss create mode 100644 src/styles/pages/success.module.scss create mode 100644 src/types/Address.interface.ts create mode 100644 src/types/AdminDashboardCardProps.type.ts create mode 100644 src/types/AdminMoreButtonProps.type.ts create mode 100644 src/types/AdminSalesItemProps.type.ts create mode 100644 src/types/BankAccounts.interface.ts create mode 100644 src/types/BestProductProps.type.ts create mode 100644 src/types/BodyInfo.interface.ts create mode 100644 src/types/Cart.interface.ts create mode 100644 src/types/CommonError.interface.ts create mode 100644 src/types/Customer.interface.ts create mode 100644 src/types/IdInfo.interface.ts create mode 100644 src/types/ModalProps.type.ts create mode 100644 src/types/MyOrderItemProps.type.ts create mode 100644 src/types/MyWishItemProps.type.ts create mode 100644 src/types/Product.interface.ts create mode 100644 src/types/ProductAddFormProps.type.ts create mode 100644 src/types/ProductItemProps.type.ts create mode 100644 src/types/Products.interface.ts create mode 100644 src/types/TransactionDetail.interface.ts create mode 100644 src/types/index.ts create mode 100644 src/types/swiper/index.d.ts create mode 100644 src/utils/CalculateDiscountedPrice.ts create mode 100644 src/utils/CheckIsAdmin.ts create mode 100644 src/utils/convertTagColor.ts create mode 100644 src/utils/index.ts create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 00000000..f5e75171 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,16 @@ +module.exports = { + env: { browser: true, es2020: true, node: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended' + ], + parser: '@typescript-eslint/parser', + parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, + plugins: ['react-refresh', 'prettier'], //추가사항 - prettier + rules: { + 'react-refresh/only-export-components': 'warn', + 'no-extra-semi': 'off', + '@typescript-eslint/no-extra-semi': 'error' + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8617331e --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* +package-lock.json + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +#env +.env diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..755a1f74 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": false, + "singleQuote": true, + "endOfLine": "lf", + "singleAttributePerLine": true, + "bracketSameLine": true, + "trailingComma": "none", + "arrowParens": "avoid" +} diff --git a/README.md b/README.md index c81804bf..9438d087 100644 --- a/README.md +++ b/README.md @@ -1,1319 +1,77 @@ -# 🤝 거래 API 활용, 팀 프로젝트 - -주어진 API를 분석해 어떤 프로젝트를 진행/완성할 것인지 팀 단위로 자유롭게 결정하고 만들어보세요. -TypeScript를 필수로 사용해야 합니다. -과제 수행 및 리뷰 기간은 별도 공지를 참고하세요! - -## 과제 수행 및 제출 방법 - -``` -KDT기수번호_이름 - -E.g, KDT0_ParkYoungWoong -``` - -1. 현재 저장소를 로컬에 클론(Clone)합니다. -1. 자신의 본명으로 브랜치를 생성합니다.(구분 가능하도록 본명을 꼭 파스칼케이스로 표시하세요, `git branch KDTX_ParkYoungWoong`) -1. 자신의 본명 브랜치에서 과제를 수행합니다. -1. 과제 수행이 완료되면, 자신의 본명 브랜치를 원격 저장소에 푸시(Push)합니다.(`main` 브랜치에 푸시하지 않도록 꼭 주의하세요, `git push origin KDTX_ParkYoungWoong`) -1. 저장소에서 `main` 브랜치를 대상으로 Pull Request 생성하면, 과제 제출이 완료됩니다!(E.g, `main` <== `KDTX_ParkYoungWoong`) - -- `main` 혹은 다른 사람의 브랜치로 절대 병합하지 않도록 주의하세요! -- Pull Request에서 보이는 설명을 다른 사람들이 이해하기 쉽도록 꼼꼼하게 작성하세요! -- Pull Request에서 과제 제출 후 절대 병합(Merge)하지 않도록 주의하세요! -- 과제 수행 및 제출 과정에서 문제가 발생한 경우, 바로 담당 멘토나 강사에서 얘기하세요! - -## API 사용법 - -모든 API 요청(Request) `headers`에 아래 정보가 꼭 포함돼야 합니다! -`username`은 `KDT5_TeamX`와 같이 본명 혹은 팀 이름을 포함해야 합니다! -확인할 수 없는 사용자나 팀의 DB 정보는 임의로 삭제될 수 있습니다! - -```json -{ - "content-type": "application/json", - "apikey": "KDT5_nREmPe9B", - "username": "KDT5_TeamX" -} -``` - -
- -## 인증 - -'인증' 관련 API는 모두 일반 사용자 전용입니다. - -### 회원가입 - -사용자가 `username`에 종속되어 회원가입합니다. - -- 사용자 비밀번호는 암호화해 저장합니다.(관리자는 확인할 수 없습니다!) -- 프로필 이미지는 1MB 이하여야 합니다. - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/auth/signup - \ -X 'POST' -``` - -요청 데이터 타입 및 예시: - -```ts -interface RequestBody { - email: string // 사용자 아이디 (필수!) - password: string // 사용자 비밀번호, 8자 이상 (필수!) - displayName: string // 사용자 이름, 20자 이하 (필수!) - profileImgBase64?: string // 사용자 프로필 이미지(base64) - jpg, jpeg, webp, png, gif, svg -} -``` - -```json -{ - "email": "thesecon@gmail.com", - "password": "********", - "displayName": "ParkYoungWoong", - "profileImgBase64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAf...(생략)" -} -``` - -응답 데이터 타입 및 예시: - -```ts -interface ResponseValue { - user: { // 회원가입한 사용자 정보 - email: string // 사용자 아이디 - displayName: string // 사용자 표시 이름 - profileImg: string | null // 사용자 프로필 이미지(URL) - } - accessToken: string // 사용자 접근 토큰 -} -``` - -```json -{ - "user": { - "email": "thesecon@gmail.com", - "displayName": "ParkYoungWoong", - "profileImg": "https://storage.googleapis.com/heropy-api/vjbtIrh5dGv163442.png" - }, - "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IlM3WDhpQ...(생략)" -} -``` - -### 로그인 - -- 발급된 `accessToken`은 24시간 후 만료됩니다.(만료 후 다시 로그인 필요) - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/auth/login - \ -X 'POST' -``` - -요청 데이터 타입 및 예시: - -```ts -interface RequestBody { - email: string // 사용자 아이디 (필수!) - password: string // 사용자 비밀번호 (필수!) -} -``` - -```json -{ - "email": "thesecon@gmail.com", - "password": "********" -} -``` - -응답 데이터 타입 및 예시: - -```ts -interface ResponseValue { - user: { // 회원가입한 사용자 정보 - email: string // 사용자 아이디 - displayName: string // 사용자 표시 이름 - profileImg: string | null // 사용자 프로필 이미지(URL) - } - accessToken: string // 사용자 접근 토큰 -} -``` - -```json -{ - "user": { - "email": "thesecon@gmail.com", - "displayName": "ParkYoungWoong", - "profileImg": "https://storage.googleapis.com/heropy-api/vAKjlJ-Gx5v163442.png" - }, - "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjlQS3I...(생략)" -} -``` - -### 인증 확인 - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/auth/me - \ -X 'POST' - \ -H 'Authorization: Bearer ' -``` - -요청 데이터 타입 및 예시: - -- 없음 - -응답 데이터 타입 및 예시: - -```ts -interface ResponseValue { - email: string // 사용자 아이디 - displayName: string // 사용자 표시 이름 - profileImg: string | null // 사용자 프로필 이미지(URL) -} -``` - -```json -{ - "email": "thesecon@gmail.com", - "displayName": "ParkYoungWoong", - "profileImg": "https://storage.googleapis.com/heropy-api/vAKjlJ-Gx5v163442.png" -} -``` - -### 로그아웃 - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/auth/logout - \ -X 'POST' - \ -H 'Authorization: Bearer ' -``` - -요청 데이터 타입 및 예시: - -- 없음 - -응답 데이터 타입 및 예시: - -```ts -type ResponseValue = true // 로그아웃 처리 상태 -``` - -### 사용자 정보 수정 - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/auth/user - \ -X 'PUT' - \ -H 'Authorization: Bearer ' -``` - -요청 데이터 타입 및 예시: - -```ts -interface RequestBody { - displayName?: string // 새로운 표시 이름 - profileImgBase64?: string // 사용자 프로필 이미지(base64) - jpg, jpeg, webp, png, gif, svg - oldPassword?: string // 기존 비밀번호 - newPassword?: string // 새로운 비밀번호 -} -``` - -```json -{ - "oldPassword": "********", - "newPassword": "**********" -} -``` - -응답 데이터 타입 및 예시: - -```ts -interface ResponseValue { - email: string // 사용자 아이디 - displayName: string // 사용자 표시 이름 - profileImg: string | null // 사용자 프로필 이미지(URL) -} -``` - -```json -{ - "email": "thesecon@gmail.com", - "displayName": "ParkYoungWoong", - "profileImg": "https://storage.googleapis.com/heropy-api/vAKjlJ-Gx5v163442.png" -} -``` - -### 사용자 목록 조회 - -- 관리자 전용 API입니다. - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/auth/users - \ -X 'GET' - \ -H 'masterKey: true' -``` - -요청 데이터 타입 및 예시: - -- 없음 - -응답 데이터 타입 및 예시: - -```ts -type ResponseValue = User[] - -interface User { - email: string // 사용자 아이디 - displayName: string // 사용자 표시 이름 - profileImg: string // 사용자 프로필 이미지 URL -} -``` - -```json -[ - { - "email": "thesecon@gmail.com", - "displayName": "HEROPY", - "profileImg": null - }, - { - "email": "neo@zillinks.com", - "displayName": "박영웅", - "profileImg": "https://storage.googleapis.com/heropy-api/Z_una7lyijv074804.png" - }, - { - "email": "test@test.com", - "displayName": "관리자", - "profileImg": "https://storage.googleapis.com/heropy-api/ZXcXjwsB7nv121507.png" - } -] -``` - -
- -## 계좌 - -'계좌' 관련 API는 모두 일반 사용자 전용입니다. - -### 선택 가능한 은행 목록 조회 - -- 은행 당 하나의 계좌만 허용됩니다. -- 사용자가 계좌를 추가하면, 해당 은행 정보 `disabled` 속성이 `true`로 변경됩니다. -- 은행 정보 `digits` 속성의 숫자를 모두 더하면 각 은행의 유효한 계좌번호 길이가 됩니다. -- `[3, 2, 4, 3]` => 123-12-1234-123 - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/account/banks - \ -X 'GET' - \ -H 'Authorization: Bearer ' -``` - -요청 데이터 타입 및 예시: - -- 없음 - -응답 데이터 타입 및 예시: - -```ts -type ResponseValue = Bank[] // 선택 가능한 은행 정보 목록 - -interface Bank { // 선택 가능한 은행 정보 - name: string // 은행 이름 - code: string // 은행 코드 - digits: number[] // 은행 계좌 자릿수 - disabled: boolean // 사용자가 추가한 계좌 여부 -} -``` - -```json -[ - { - "name": "KB국민은행", - "code": "004", - "digits": [3, 2, 4, 3], - "disabled": false - }, - { - "name": "신한은행", - "code": "088", - "digits": [3, 3, 6], - "disabled": true - }, - { - "name": "우리은행", - "code": "020", - "digits": [4, 3, 6], - "disabled": true - }, - { - "name": "하나은행", - "code": "081", - "digits": [3, 6, 5], - "disabled": false - }, - { - "name": "케이뱅크", - "code": "089", - "digits": [3, 3, 6], - "disabled": false - }, - { - "name": "카카오뱅크", - "code": "090", - "digits": [4, 2, 7], - "disabled": false - }, - { - "name": "NH농협은행", - "code": "011", - "digits": [3, 4, 4, 2], - "disabled": false - } -] -``` - -### 계좌 목록 및 잔액 조회 - -- 계좌번호는 일부만 노출됩니다. E.g. `"123-XXXX-XXXX-XX"` -- 잔액의 단위는 '원화(₩)'입니다. - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/account - \ -X 'GET' - \ -H 'Authorization: Bearer ' -``` - -요청 데이터 타입 및 예시: - -- 없음 - -응답 데이터 타입 및 예시: - -```ts -interface ResponseValue { - totalBalance: number // 사용자 계좌 잔액 총합 - accounts: Bank[] // 사용자 계좌 정보 목록 -} - -interface Bank { // 사용자 계좌 정보 - id: string // 계좌 ID - bankName: string // 은행 이름 - bankCode: string // 은행 코드 - accountNumber: string // 계좌 번호 - balance: number // 계좌 잔액 -} -``` - -```json -{ - "totalBalance": 5999900, - "accounts": [ - { - "id": "jQMfKla8vOIFELA3mAXv", - "bankName": "NH농협은행", - "bankCode": "011", - "accountNumber": "356-XXXX-XXXX-XX", - "balance": 2999900 - }, - { - "id": "wiPgsXvMAmcLw8AuRHIi", - "bankName": "KB국민은행", - "bankCode": "004", - "accountNumber": "123-XX-XXXX-XXX", - "balance": 3000000 - } - ] -} -``` - -### 계좌 연결 - -- 연결된 계좌 잔액에는 자동으로 기본 '3백만원'이 추가됩니다. -- 요청하는 계좌번호와 전화번호에는 `-` 구분이 없어야 합니다. - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/account - \ -X 'POST' - \ -H 'Authorization: Bearer ' -``` - -요청 데이터 타입 및 예시: - -```ts -interface RequestBody { - bankCode: string // 연결할 은행 코드 (필수!) - accountNumber: string // 연결할 계좌번호 (필수!) - phoneNumber: string // 사용자 전화번호 (필수!) - signature: boolean // 사용자 서명 (필수!) -} -``` - -```json -{ - "bankCode": "088", - "accountNumber": "123456789012", - "phoneNumber": "01012345678", - "signature": true -} -``` - -응답 데이터 타입 및 예시: - -```ts -interface ResponseValue { // 연결된 계좌 정보 - id: string // 계좌 ID - bankName: string // 은행 이름 - bankCode: string // 은행 코드 - accountNumber: string // 계좌 번호 - balance: number // 계좌 잔액 -} -``` - -```json -{ - "id": "1qRFC6Ey5VkSu6nyj5Ba", - "bankName": "신한은행", - "bankCode": "088", - "accountNumber": "123-XXX-XXXXXX", - "balance": 3000000 -} -``` - -### 계좌 해지 - -- 해지한 계좌는 다시 연결해도 잔액이 반영되지 않습니다.(기본 금액으로 추가됩니다) - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/account - \ -X 'DELETE' - \ -H 'Authorization: Bearer ' -``` - -요청 데이터 타입 및 예시: - -```ts -interface RequestBody { - accountId: string // 계좌 ID (필수!) - signature: boolean // 사용자 서명 (필수!) -} -``` - -```json -{ - "accountId": "jQMfKla8vOIFELA3mAXv", - "signature": true -} -``` - -응답 데이터 타입 및 예시: - -```ts -type ResponseValue = true // 계좌 해지 처리 상태 -``` - -
- -## 제품 - -'제품' 관련 API는 관리자 전용과 일반 사용자 전용으로 구분됩니다.
-공용 API도 있으니 주의하세요! - -### 모든 제품 조회 - -- 관리자 전용 API입니다. -- 상세 정보가 아닌 기본 정보의 제품 설명은 100자까지만 포함됩니다. -- 상세 정보가 아닌 기본 정보의 제품 상세 사진은 포함되지 않습니다. -- 제품 할인율(`discountRate`)은 제품 가격과 직접 관계가 없는 단순 메모 속성입니다. -- 제품 할인율이 없는 경우, `0`으로 표시됩니다. - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/products - \ -X 'GET' - \ -H 'masterKey: true' -``` - -요청 데이터 타입 및 예시: - -- 없음 - -응답 데이터 타입 및 예시: - -```ts -type ResponseValue = Product[] // 관리하는 모든 제품의 목록 - -interface Product { // 제품 정보 - id: string // 제품 ID - title: string // 제품 이름 - price: number // 제품 가격 - description: string // 제품 설명(최대 100자) - tags: string[] // 제품 태그 - thumbnail: string | null // 제품 썸네일 이미지(URL) - isSoldOut: boolean // 제품 매진 여부 - discountRate: number // 제품 할인율 -} -``` - -```json -[ - { - "id": "cFmeC7aY5KjZbBAdJE9y", - "title": "삼성전자 스마트모니터 M7 S43AM700", - "price": 639000, - "description": "107.9cm(43인치) / 와이드(16:9) / 평면 / VA / 3840 x 2160(4K UHD) / 픽셀피치: 0.2451mm / 8ms(GTG) / 300cd / 5,00", - "tags": [ - "가전", - "모니터", - "컴퓨터" - ], - "thumbnail": "https://storage.googleapis.com/heropy-api/vBAK4MQdH5v195712.png", - "isSoldOut": false, - "discountRate": 20 - }, - { - "id": "nbqtQvEivYwEXTDet7YM", - "title": "MacBook Pro 16", - "price": 3360000, - "description": "역대 가장 강력한 MacBook Pro가 등장했습니다. 최초의 프로용 Apple Silicon인 M1 Pro 또는 M1 Max 칩을 탑재해 쏜살같이 빠른 속도는 물론, 획기적인 성", - "tags": [ - "가전", - "노트북", - "컴퓨터" - ], - "thumbnail": "https://storage.googleapis.com/heropy-api/vIKMk_jy4Yv195256.png", - "isSoldOut": false, - "discountRate": 0 - } -] -``` - -### 전체 거래(판매) 내역 - -- 관리자 전용 API입니다. - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/products/transactions/all - \ -X 'GET' - \ -H 'masterKey: true' -``` - -요청 데이터 타입 및 예시: - -- 없음 - -응답 데이터 타입 및 예시: - -```ts -type RequestValue = TransactionDetail[] // 모든 거래 내역의 목록 - -interface TransactionDetail { // 거래 내역 정보 - detailId: string // 거래 내역 ID - user: { // 거래한 사용자 정보 - email: string - displayName: string - profileImg: string | null - } - account: { // 거래한 사용자의 계좌 정보 - bankName: string - bankCode: string - accountNumber: string - } - product: { // 거래한 제품 정보 - productId: string - title: string - price: number - description: string - tags: string[] - thumbnail: string | null - discountRate: number - } - reservation: Reservation | null // 거래한 제품의 예약 정보 - timePaid: string // 제품을 거래한 시간 - isCanceled: boolean // 거래 취소 여부 - done: boolean // 거래 완료 여부 -} - -interface Reservation { - start: string // 예약 시작 시간 - end: string // 예약 종료 시간 - isCanceled: boolean // 예약 취소 여부 - isExpired: boolean // 예약 만료 여부 -} -``` - -```json -[ - { - "detailId": "dMhfxyrAupQP18OYmywy", - "user": { - "email": "thesecon@gmail.com", - "displayName": "ParkYoungWoong", - "profileImg": "https://storage.googleapis.com/heropy-api/vsLRqTlPO5v200111.png" - }, - "account": { - "bankName": "KB국민은행", - "bankCode": "004", - "accountNumber": "123-XX-XXXX-XXX" - }, - "product": { - "productId": "cFmeC7aY5KjZbBAdJE9y", - "title": "삼성전자 스마트모니터 M7 S43AM700", - "price": 639000, - "description": "107.9cm(43인치) / 와이드(16:9) / 평면 / VA / 3840 x 2160(4K UHD) / 픽셀피치: 0.2451mm / 8ms(GTG) / 300cd / 5,00", - "tags": [ - "가전", - "모니터", - "컴퓨터" - ], - "thumbnail": "https://storage.googleapis.com/heropy-api/vBAK4MQdH5v195712.png", - "discountRate": 0 - }, - "reservation": null, - "timePaid": "2021-11-07T20:01:49.100Z", - "isCanceled": false, - "done": false - } -] -``` - -예약 정보(`reservation`)가 있는 경우: - -```json -[ - { - "reservation": { - "start": "2021-11-12T06:00:00.000Z", - "end": "2021-11-12T07:00:00.000Z", - "isCanceled": false, - "isExpired": true - } - } -] -``` - -### 거래(판매) 내역 완료/취소 및 해제 - -- 관리자 전용 API입니다. -- 거래 내역을 취소하면, 예약도 같이 취소됩니다. -- 거래 내역을 취소 해제하면, 예약도 같이 취소가 해제됩니다. - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/products/transactions/:detailId - \ -X 'PUT' - \ -H 'masterKey: true' -``` - -요청 데이터 타입 및 예시: - -```ts -interface RequestBody { - isCanceled?: boolean // 거래 취소 여부 (사용자의 '제품 거래(구매) 취소' 상태와 같습니다) - done?: boolean // 거래 완료 여부 (사용자의 '제품 거래(구매) 확정' 상태와 같습니다) -} -``` - -```json -{ - "isCanceled": true -} -``` - -응답 데이터 타입 및 예시: - - -```ts -type ResponseValue = true // 거래 내역 완료/취소 및 해제 처리 상태 -``` - -### 제품 추가 - -- 관리자 전용 API입니다. -- 파일(사진)은 Base64로 요청해야 합니다. -- 제품 썸네일 사진은 1MB 이하여야 합니다. -- 제품 상세 사진은 4MB 이하여야 합니다. -- 제품 할인율(`discountRate`)은 제품 가격과 직접 관계가 없는 단순 메모 속성입니다. -- 제품 할인율은 `0`~`99` 사이 숫자를 입력하세요. 만약 할인율이 '20%'인 경우, `20`으로 입력해야 합니다. -- 제품 할인율을 입력하지 않으면, `0`으로 적용됩니다. - -```js -// 할인 전 가격을 계산! -const priceBeforeDiscount = price * 100 / (100 - discountRate) -``` - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/products - \ -X 'POST' - \ -H 'masterKey: true' -``` - -요청 데이터 타입 및 예시: - -```ts -interface RequestBody { - title: string // 제품 이름 (필수!) - price: number // 제품 가격 (필수!) - description: string // 제품 상세 설명 (필수!) - tags?: string[] // 제품 태그 - thumbnailBase64?: string // 제품 썸네일(대표) 사진(base64) - jpg, jpeg, webp, png, gif, svg - photoBase64?: string // 제품 상세 사진(base64) - jpg, jpeg, webp, png, gif, svg - discountRate?: number // 제품 할인율 -} -``` - -```json -{ - "title": "MacBook Pro 16", - "price": 3360000, - "description": "역대 가장 강력한 MacBook Pro가 등장했습니다. 최초의 프로용 Apple Silicon인 M1 Pro 또는 M1 Max 칩을 탑재해 쏜살같이 빠른 속도는 물론, 획기적인 성능과 놀라운 배터리 사용 시간을 자랑하죠. 여기에 시선을 사로잡는 Liquid Retina XDR 디스플레이, Mac 노트북 사상 최고의 카메라 및 오디오 그리고 더할 나위 없이 다양한 포트까지. 기존 그 어떤 카테고리에도 속하지 않는 노트북. 새로운 MacBook Pro는 그야말로 야수입니다.", - "tags": [ - "가전", - "노트북", - "컴퓨터" - ], - "thumbnailBase64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...(생략)" -} -``` - -응답 데이터 타입 및 예시: - -```ts -interface ResponseValue { // 추가한 제품의 상세 내용 - id: string // 제품 ID - title: string // 제품 이름 - price: number // 제품 가격 - description: string // 제품 상세 설명 - tags: string[] // 제품 태그 - thumbnail: string | null // 제품 썸네일 이미지(URL) - photo: string | null // 제품 상세 이미지(URL) - isSoldOut: boolean // 제품 매진 여부 - discountRate: number // 제품 할인율 -} -``` - -```json -{ - "id": "nbqtQvEivYwEXTDet7YM", - "title": "MacBook Pro 16", - "price": 3360000, - "description": "역대 가장 강력한 MacBook Pro가 등장했습니다. 최초의 프로용 Apple Silicon인 M1 Pro 또는 M1 Max 칩을 탑재해 쏜살같이 빠른 속도는 물론, 획기적인 성능과 놀라운 배터리 사용 시간을 자랑하죠. 여기에 시선을 사로잡는 Liquid Retina XDR 디스플레이, Mac 노트북 사상 최고의 카메라 및 오디오 그리고 더할 나위 없이 다양한 포트까지. 기존 그 어떤 카테고리에도 속하지 않는 노트북. 새로운 MacBook Pro는 그야말로 야수입니다.", - "tags": [ - "가전", - "노트북", - "컴퓨터" - ], - "thumbnail": "https://storage.googleapis.com/heropy-api/vIKMk_jy4Yv195256.png", - "photo": "https://storage.googleapis.com/heropy-api/voihKb3NLGcv195257.png", - "isSoldOut": false, - "discountRate": 0 -} -``` - -### 제품 수정 - -- 관리자 전용 API입니다. -- 사용자의 구매 내역 확인을 위해, 제품을 실제로는 삭제하지 않고 매진(Sold Out) 처리해야 합니다. -- 매진은 다시 해제할 수 있습니다. - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/products/:productId - \ -X 'PUT' - \ -H 'masterKey: true' -``` - -요청 데이터 타입 및 예시: - -```ts -interface RequestBody { - title?: string // 제품 이름 - price?: number // 제품 가격 - description?: string // 제품 상세 설명 - tags?: string[] // 제품 태그 - thumbnailBase64?: string // 제품 썸네일(대표) 사진(base64) - jpg, jpeg, webp, png, gif, svg - photoBase64?: string // 제품 상세 사진(base64) - jpg, jpeg, webp, png, gif, svg - isSoldOut?: boolean // 제품 매진 여부 - discountRate?: number // 제품 할인율 -} -``` - -```json -{ - "price": 1500 -} -``` - -응답 데이터 타입 및 예시: - -```ts -interface ResponseValue { // 수정한 제품의 상세 내용 - id: string // 제품 ID - title: string // 제품 이름 - price: number // 제품 가격 - description: string // 제품 상세 설명 - tags: string[] // 제품 태그 - thumbnail: string | null // 제품 썸네일 이미지(URL) - photo: string | null // 제품 상세 이미지(URL) - isSoldOut: boolean // 제품 매진 여부 - discountRate: number // 제품 할인율 -} -``` - -```json -{ - "id": "nbqtQvEivYwEXTDet7YM", - "title": "MacBook Pro 16", - "price": 1500, - "description": "역대 가장 강력한 MacBook Pro가 등장했습니다. 최초의 프로용 Apple Silicon인 M1 Pro 또는 M1 Max 칩을 탑재해 쏜살같이 빠른 속도는 물론, 획기적인 성능과 놀라운 배터리 사용 시간을 자랑하죠. 여기에 시선을 사로잡는 Liquid Retina XDR 디스플레이, Mac 노트북 사상 최고의 카메라 및 오디오 그리고 더할 나위 없이 다양한 포트까지. 기존 그 어떤 카테고리에도 속하지 않는 노트북. 새로운 MacBook Pro는 그야말로 야수입니다.", - "tags": [ - "가전", - "노트북", - "컴퓨터" - ], - "thumbnail": "https://storage.googleapis.com/heropy-api/vIKMk_jy4Yv195256.png", - "photo": "https://storage.googleapis.com/heropy-api/voihKb3NLGcv195257.png", - "isSoldOut": false, - "discountRate": 0 -} -``` - -### 제품 삭제 - -- 관리자 전용 API입니다. - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/products/:productId - \ -X 'DELETE' - \ -H 'masterKey: true' -``` - -요청 데이터 타입 및 예시: - -- 없음 - -응답 데이터 타입 및 예시: - -```ts -type ResponseValue = true // 제품 삭제 처리 상태 -``` - -### 단일 제품 상세 조회 - -- 공용 API입니다. - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/products/:productId - \ -X 'GET' -``` - -요청 데이터 타입 및 예시: - -- 없음 - -응답 데이터 타입 및 예시: - -```ts -interface ResponseValue { // 제품의 상세 내용 - id: string // 제품 ID - title: string // 제품 이름 - price: number // 제품 가격 - description: string // 제품 상세 설명 - tags: string[] // 제품 태그 - thumbnail: string | null // 제품 썸네일 이미지(URL) - photo: string | null // 제품 상세 이미지(URL) - isSoldOut: boolean // 제품 매진 여부 - reservations: Reservation[] // 제품의 모든 예약 정보 목록 - discountRate: number // 제품 할인율 -} - -interface Reservation { - start: string // 예약 시작 시간 - end: string // 예약 종료 시간 - isCanceled: boolean // 예약 취소 여부 - isExpired: boolean // 예약 만료 여부 -} -``` - -```json -{ - "id": "nbqtQvEivYwEXTDet7YM", - "title": "MacBook Pro 16", - "price": 3360000, - "description": "역대 가장 강력한 MacBook Pro가 등장했습니다. 최초의 프로용 Apple Silicon인 M1 Pro 또는 M1 Max 칩을 탑재해 쏜살같이 빠른 속도는 물론, 획기적인 성능과 놀라운 배터리 사용 시간을 자랑하죠. 여기에 시선을 사로잡는 Liquid Retina XDR 디스플레이, Mac 노트북 사상 최고의 카메라 및 오디오 그리고 더할 나위 없이 다양한 포트까지. 기존 그 어떤 카테고리에도 속하지 않는 노트북. 새로운 MacBook Pro는 그야말로 야수입니다.", - "tags": [ - "가전", - "노트북", - "컴퓨터" - ], - "thumbnail": "https://storage.googleapis.com/heropy-api/vIKMk_jy4Yv195256.png", - "photo": "https://storage.googleapis.com/heropy-api/voihKb3NLGcv195257.png", - "isSoldOut": false, - "reservations": [], - "discountRate": 0 -} -``` - -예약 정보(`reservation`)가 있는 경우: - -```json -{ - "reservations": [ - { - "reservation": { - "start": "2021-11-12T06:00:00.000Z", - "end": "2021-11-12T07:00:00.000Z", - "isCanceled": false, - "isExpired": true - } - } - ] -} -``` - -### 제품 검색 - -- 사용자 전용 API입니다. -- 제품 이름과 태그를 동시에 검색할 수 있고, 'And'(검색한 이름과 태그 모두 포함된 제품) 조건으로 결과를 반환합니다. -- 제품 이름과 태그 모두 포함하지 않으면, 모든 제품의 결과를 반환합니다. -- 제품의 기본 정보만 반환합니다. -- 매진된 제품은 검색되지 않습니다. - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/products/search - \ -X 'POST' -``` - -요청 데이터 타입 및 예시: - -```ts -interface RequestBody { - searchText?: string // 검색할 제품 이름 - searchTags?: string[] // 검색할 제품 태그 -} -``` - -```json -{ - "searchText": "삼성전자", - "searchTags": ["가전"] -} -``` - -응답 데이터 타입 및 예시: - -```ts -type ResponseValue = Product[] // 관리하는 모든 제품의 목록 - -interface Product { // 제품 정보 - id: string // 제품 ID - title: string // 제품 이름 - price: number // 제품 가격 - description: string // 제품 설명(최대 100자) - tags: string[] // 제품 태그 - thumbnail: string | null // 제품 썸네일 이미지(URL) - discountRate: number // 제품 할인율 -} -``` - -```json -[ - { - "id": "cFmeC7aY5KjZbBAdJE9y", - "title": "삼성전자 스마트모니터 M7 S43AM700", - "price": 639000, - "description": "107.9cm(43인치) / 와이드(16:9) / 평면 / VA / 3840 x 2160(4K UHD) / 픽셀피치: 0.2451mm / 8ms(GTG) / 300cd / 5,00", - "tags": [ - "가전", - "모니터", - "컴퓨터" - ], - "thumbnail": "https://storage.googleapis.com/heropy-api/vBAK4MQdH5v195712.png", - "discountRate": 0 - } -] -``` - -### 제품 거래(구매) 신청 - -- 사용자 전용 API입니다. -- 거래(구매) 신청시 연결된 계좌에서 결제됩니다. -- 결제할 계좌(ID)를 꼭 선택해야 합니다.(`계좌 목록 및 잔액 조회` API를 사용하세요) -- 선택한 계좌의 잔액보다 결제 금액이 크면 결제가 처리되지 않습니다.(에러 반환) - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/products/buy - \ -X 'POST' - \ -H 'Authorization: Bearer ' -``` - -요청 데이터 타입 및 예시: - -```ts -interface RequestBody { - productId: string // 거래할 제품 ID (필수!) - accountId: string // 결제할 사용자 계좌 ID (필수!) - reservation?: { // 예약 정보(예약 시스템을 사용하는 경우만 필요) - start: string // 예약 시작 시간(ISO) - end: string // 예약 종료 시간(ISO) - } -} -``` - -```js -const isoString = new Date().toISOString() -``` - -```json -{ - "productId": "nbqtQvEivYwEXTDet7YM", - "accountId": "Mq2KKHk8vlmr6Xkg58Fa", - "reservation": { - "start": "2021-11-12T06:00:00.000Z", - "end": "2021-11-12T07:00:00.000Z" - } -} -``` - -응답 데이터 타입 및 예시: - -```ts -type ResponseValue = true // 거래 신청 처리 여부 -``` - -### 제품 거래(구매) 취소 - -- 사용자 전용 API입니다. -- '거래 취소'시 결제한 사용자 계좌로 금액이 환불됩니다. - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/products/cancel - \ -X 'POST' - \ -H 'Authorization: Bearer ' -``` - -요청 데이터 타입 및 예시: - -```ts -interface RequestBody { - detailId: string // 취소할 제품의 거래 내역 ID -} -``` - -```json -{ - "detailId": "dMhfxyrAupQP18OYmywy" -} -``` - -응답 데이터 타입 및 예시: - -```ts -type ResponseValue = true // 거래 취소 처리 여부 -``` - -### 제품 거래(구매) 확정 - -- 사용자 전용 API입니다. -- '거래(구매) 확정' 후에는 '거래 취소'를 할 수 없습니다. - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/products/ok - \ -X 'POST' - \ -H 'Authorization: Bearer ' -``` - -요청 데이터 타입 및 예시: - -```ts -interface RequestBody { - detailId: string // 거래(구매) 확정할 제품의 거래 내역 ID -} -``` - -```json -{ - "detailId": "dMhfxyrAupQP18OYmywy" -} -``` - -응답 데이터 타입 및 예시: - -```ts -type ResponseValue = true // 거래(구매) 확정 처리 여부 -``` - -### 제품 전체 거래(구매) 내역 - -- 사용자 전용 API입니다. -- 거래 내역의 기본 정보만 포함됩니다. - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/products/transactions/details - \ -X 'GET' - \ -H 'Authorization: Bearer ' -``` - -요청 데이터 타입 및 예시: - -- 없음 - -응답 데이터 타입 및 예시: - -```ts -type RequestValue = TransactionDetail[] // 모든 거래 내역의 목록 - -interface TransactionDetail { // 거래 내역 정보 - detailId: string // 거래 내역 ID - product: { // 거래한 제품 정보 - productId: string - title: string - price: number - description: string - tags: string[] - thumbnail: string | null - discountRate: number // 제품 할인율 - } - reservation: Reservation | null // 거래한 제품의 예약 정보 - timePaid: string // 제품을 거래한 시간 - isCanceled: boolean // 거래 취소 여부 - done: boolean // 거래 완료 여부 -} - -interface Reservation { - start: string // 예약 시작 시간 - end: string // 예약 종료 시간 - isCanceled: boolean // 예약 취소 여부 - isExpired: boolean // 예약 만료 여부 -} -``` - -```json -[ - { - "detailId": "9jAoagzrZBkSWI5NctEB", - "product": { - "productId": "nbqtQvEivYwEXTDet7YM", - "title": "MacBook Pro 16", - "price": 3360000, - "description": "역대 가장 강력한 MacBook Pro가 등장했습니다. 최초의 프로용 Apple Silicon인 M1 Pro 또는 M1 Max 칩을 탑재해 쏜살같이 빠른 속도는 물론, 획기적인 성", - "tags": [ - "가전", - "노트북", - "컴퓨터" - ], - "thumbnail": "https://storage.googleapis.com/heropy-api/vIKMk_jy4Yv195256.png", - "discountRate": 0 - }, - "reservation": null, - "timePaid": "2021-11-07T20:17:32.112Z", - "isCanceled": true, - "done": false - }, - { - "detailId": "dMhfxyrAupQP18OYmywy", - "product": { - "productId": "cFmeC7aY5KjZbBAdJE9y", - "title": "삼성전자 스마트모니터 M7 S43AM700", - "price": 639000, - "description": "107.9cm(43인치) / 와이드(16:9) / 평면 / VA / 3840 x 2160(4K UHD) / 픽셀피치: 0.2451mm / 8ms(GTG) / 300cd / 5,00", - "tags": [ - "가전", - "모니터", - "컴퓨터" - ], - "thumbnail": "https://storage.googleapis.com/heropy-api/vBAK4MQdH5v195712.png", - "discountRate": 0 - }, - "reservation": { - "start": "2021-11-12T06:00:00.000Z", - "end": "2021-11-12T07:00:00.000Z", - "isCanceled": false, - "isExpired": true - }, - "timePaid": "2021-11-07T20:01:49.100Z", - "isCanceled": false, - "done": true - } -] -``` - -### 단일 제품 상세 거래(구매) 내역 - -- 사용자 전용 API입니다. - -```curl -curl https://asia-northeast3-heropy-api.cloudfunctions.net/api/products/transactions/detail - \ -X 'POST' - \ -H 'Authorization: Bearer ' -``` - -요청 데이터 타입 및 예시: - -```ts -interface RequestBody { - detailId: string // 상세 내용을 확인할 거래(구매) 내역 ID -} -``` - -```json -{ - "detailId": "dMhfxyrAupQP18OYmywy" -} -``` - -응답 데이터 타입 및 예시: - -```ts -interface TransactionDetail { // 상세 거래 정보 - detailId: string // 거래 내역 ID - account: { // 거래한 사용자의 계좌 정보 - bankName: string - bankCode: string - accountNumber: string - } - product: { // 거래한 제품 정보 - productId: string - title: string - price: number - description: string - tags: string[] - thumbnail: string | null - photo: string | null - discountRate: number // 제품 할인율 - } - reservation: Reservation | null // 거래한 제품의 예약 정보 - timePaid: string // 제품을 거래한 시간 - isCanceled: boolean // 거래 취소 여부 - done: boolean // 거래 완료 여부 -} - -interface Reservation { - start: string // 예약 시작 시간 - end: string // 예약 종료 시간 - isCanceled: boolean // 예약 취소 여부 - isExpired: boolean // 예약 만료 여부 -} -``` - -```json -{ - "detailId": "dMhfxyrAupQP18OYmywy", - "account": { - "bankName": "KB국민은행", - "bankCode": "004", - "accountNumber": "123-XX-XXXX-XXX" - }, - "product": { - "productId": "cFmeC7aY5KjZbBAdJE9y", - "title": "삼성전자 스마트모니터 M7 S43AM700", - "price": 639000, - "description": "107.9cm(43인치) / 와이드(16:9) / 평면 / VA / 3840 x 2160(4K UHD) / 픽셀피치: 0.2451mm / 8ms(GTG) / 300cd / 5,000:1 / 최대 주사율: 60Hz / HDMI 2.0 / USB Type-C / 플리커 프리 / 블루라이트 차단 / 게임모드 지원 / 스피커 / 리모컨 / USB허브 / Wi-Fi(무선) / 스마트TV / 블루투스 / 틸트(상하) / 200 x 200mm / HDR / HDR10 / 10.6kg 기획전 차세대 게임 라이프 PS5 매력분석 관련기사 큐소닉, 43인치 4K UHD 스마트 모니터 ‘삼성전자 M7 S43AM700’ 출시 및 할인 행사 사용기 삼성 스마트모니터 m7 s43am700", - "tags": [ - "가전", - "모니터", - "컴퓨터" - ], - "thumbnail": "https://storage.googleapis.com/heropy-api/vBAK4MQdH5v195712.png", - "photo": "https://storage.googleapis.com/heropy-api/vVLP-ox_zSDv195712.jpg", - "discountRate": 0 - }, - "reservation": null, - "timePaid": "2021-11-07T20:01:49.100Z", - "isCanceled": false, - "done": true -} -``` +

+ +# Colley +### 🔗 배포주소 +[Colley](taem2colley.netlify.app) + +### 📄 프로젝트 소개 +거래 API를 활용한 온라인 쇼핑몰입니다. + +### 🗓 프로젝트 기간 +2023.05.30 ~ 2023.07.02 + +### 🧑‍💻 기술 스택 +
+ + + + + + +
+ +### 🤝 개발팀 소개 + +|**[이은비](https://github.com/)**|**[안중후](https://github.com/)**|**[문현수](https://github.com/)**|**[방미선](https://github.com/)**|**[김동해](https://github.com/THEEASTSEA)** +| :--------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------: | +프로젝트 팀장,
관리자 페이지,
위시리스트,
마이페이지 - 구매 |Github 관리,
구매 페이지, 계좌,
장바구니 | 로그인, 회원가입,
장바구니,
디자인 가이드,
마이페이지 - 조회,수정 | 디자인 가이드 | 메인 페이지,
상품 상세,
상품 카테고리별 페이지| + +### 📂 폴더구조 +``` +📦src +┣ 📂api +┣ 📂assets +┣ 📂components +┃ ┣ 📂admin +┃ ┣ 📂cart +┃ ┣ 📂common +┃ ┣ 📂main +┃ ┣ 📂mypage +┃ ┣ 📂payment +┃ ┣ 📂product +┃ ┣ 📜App.tsx +┃ ┗ 📜index.ts +┣ 📂constants +┣ 📂contexts +┣ 📂hooks +┣ 📂pages +┃ ┣ 📂admin +┃ ┣ 📂login +┃ ┣ 📂mypage +┃ ┣ 📂payment +┃ ┣ 📂product +┃ ┣ 📜Home.tsx +┃ ┣ 📜Router.tsx +┃ ┗ 📜index.ts +┣ 📂services +┣ 📂styles +┃ ┣ 📂abstracts +┃ ┣ 📂components +┃ ┃ ┣ 📂admin +┃ ┃ ┣ 📂cart +┃ ┃ ┣ 📂main +┃ ┃ ┣ 📂mypage +┃ ┃ ┣ 📂payment +┃ ┃ ┣ 📂product +┃ ┃ ┣ 📜Modal.module.scss +┃ ┃ ┣ 📜badge.module.scss +┃ ┃ ┗ 📜errorComponent.module.scss +┃ ┣ 📂layout +┃ ┣ 📂pages +┃ ┗ 📜common.scss +┣ 📂types +┃ ┣ 📂swiper +┣ 📂utils +┣ 📜main.tsx +┗ 📜vite-env.d.ts +``` \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 00000000..57b6ac95 --- /dev/null +++ b/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + Colley + + +
+ + + diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 00000000..f50d6ef1 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,4 @@ +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..325ec19b --- /dev/null +++ b/package.json @@ -0,0 +1,52 @@ +{ + "name": "colley", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview", + "format": "prettier --write --cache ." + }, + "dependencies": { + "@tosspayments/payment-widget-sdk": "^0.8.0", + "@types/react-js-pagination": "^3.0.4", + "axios": "^1.4.0", + "moment": "^2.29.4", + "path": "^0.12.7", + "react": "^18.2.0", + "react-daum-postcode": "^3.1.1", + "react-dom": "^18.2.0", + "react-js-pagination": "^3.0.3", + "react-router-dom": "^6.13.0", + "sass": "^1.62.1", + "swiper": "^9.4.1" + }, + "devDependencies": { + "@swc/cli": "^0.1.62", + "@swc/core": "^1.3.66", + "@types/node": "^20.2.5", + "@types/react": "^18.0.37", + "@types/react-dom": "^18.0.11", + "@types/swiper": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^5.59.0", + "@typescript-eslint/parser": "^5.59.0", + "@vitejs/plugin-react-swc": "^3.0.0", + "eslint": "^8.41.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.3.4", + "prettier": "^2.8.8", + "react-intersection-observer": "^9.5.1", + "swiper": "^9.4.1", + "tsc-alias": "^1.8.6", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.0.2", + "vite": "^4.3.9", + "vite-plugin-remove-console": "^2.1.1", + "vite-tsconfig-paths": "^4.2.0" + } +} diff --git a/public/ico_coupon.png b/public/ico_coupon.png new file mode 100644 index 0000000000000000000000000000000000000000..ef3a5bec97ebfbaf993edbcbaa95feb7c998285f GIT binary patch literal 605 zcmV-j0;2tiP)gD@ZW4kG=xbch(e*6h!C;~9SOS$R#4zy zAPU>jiB5-ZM?@)$W^F`;kSGJ-on`xA4C{Yjk0JA587_jT-`djrcQPP4H;J7RLdnKcJEeZrlv zMeN75_&twrn^jVE17$djwTR7hQ+O!h*QhvSIbtgXT);xuJmw>=6C=a3f=?S?N`$Qo zn5a+)2XHZL9=pRnjsbgf`v>s>M{u=@peJA#&JPfi$D6PpW5DaMZM+SU9o$XC&jcs> zdHjv3f@9fR(8NRCI#|PlSZiY?V&iy zHL%gH_$Nji?{({-gBx+ymWtSQf%z1iDL4(&h*D2}0!txo3CDB(Qwk}_@C#=XhYu2v zBHxbhvGybEDz@d~&IRYdK2RlKUykUJ{*|7JwZ)h(#{6U@Q+a`;oFB{j9~=(u{W-* z;{z74fF*49s>q$Vf;BWdyw)&<{_T=a;7cm;Ys{jCA?%9R5bBu2o0R`&jCVs^!8EpF zq9-_6BEE*FF?K5!UF|fuiaRlQ1Cz>WrG*O4p%HW4>NL-_81M}zx*%>xO%Q1lGeyo5 z!j^6Hh&)|HBj(Q4#7>FeCVNQU!g+}>GrQxeL~z}lc@ZiSVP0-u?346)8MWtOFp@oJ zv)3cXs-lhCM>EO(Z&@aqztEt3A z*9tEAgW6jtGkjKYRPu3pK=?Rq;yd08L&gFg3tRvH;}^}O()UJ{V88$X002ovPDHLk FV1m2+O?dzS literal 0 HcmV?d00001 diff --git a/public/ico_won.png b/public/ico_won.png new file mode 100644 index 0000000000000000000000000000000000000000..84d5fc72495adbca5a1ca3ae253479e442dd6f5a GIT binary patch literal 1035 zcmV+m1oZofP)<_E@Rjn44!%41*9t|2v`uj28rP;Mv zsD@+yLCo)~(0R37*tW*xp$g`1sc4K#rF0U1ncXQDNiIEuD{x9GZWmPOd^l`wsY%_5 z=98XbQu^v1sbnw4$5PBQOyd)&qrMDOx(`Y% zxfHib<*_cM=R#ddv-{u#ZkbQu-rOd>!18l2eyJ4F%=q1{3f_QULhuxxRh^<`=)t2J zKPG!|a~tyDLg_x2HJ&3 \ No newline at end of file diff --git a/public/no-photo.png b/public/no-photo.png new file mode 100644 index 0000000000000000000000000000000000000000..4506248b1894bdcccb89bcfad002af2c6b34395d GIT binary patch literal 2537 zcmVL`gTC6grk8Ifqw#fJ^P~6%nD!}XaNrc zhx&C#sgo7J1R{QW8}aIqQX4k$_W_H1I;GTuP5cSqtv;Pn>R}}+ez2(c0i)svjEWyH zDt^EWqT&aOiXSj4e!!^s0W%2fN5u~k6+d89{D7%d`~ym|^K{^Oz`T;|^&$@;88}54 z1}-J8%lW_qz{{ui$AL$HM+=r$h}QvIkbk&sv+n<0pca@kk|d<&Ri z`EDc%ShgaGv9DMu6|osl$HYu9V-+MxMFm8=ubvE&`^2&5piwvXQ zL7oL_yZ`hNBS?*np3ey+y(0)3A%bGN-*$ zfX57b+p%A*omp)do&-)U!*HP3#NUs+{}DjB=KBJ9r?)*x|b=H=tx&hCWGYTz%Z7b(+Xb9Z zVwq)g2JsfY_5+_Uwiy0w=ytHrE5K)gjVZ@00DeS?ecL6HRP#3g-|*s54txc`D~Tsi zwt#zyw`9#lqPmp6?UGo7UHpx}*S&a@6JG(aiB3W90hR)bfrUt3TeL;%SR^Z{39LbC zcvSor@DAW>UOXy*uK-wX?7tJZd;0Gda50kZ9ObnY`;-M+&2Nj}lX()p0^r%k{;w^^ z{LyXAAvtqo5`U9~RdJg!m*${P!c>)md2xY$Yx}NK(sI;01XcteTJmAW8fW02eseIRqJm`O+@OJQJBq zp>ZpCg&F`!;(rkMjDwv8i1=p0%8!ZPJ{AF2BeQ$-3{|Ft`C=XM7QPnnAxAs&fnQnd zTnijHeawZH@2{lVis#f4m`1+^K$7?$cC<4e>8>{I{EVl>2NC*e%W)si{U4;G0gynF z_#bh!a}d%qY1+9C`{jKu_A5nNK4e}r04$Ux{yglRSl842-hLP|i(2$I6I7G&tN~z< zB>qPo?aV`3D@{8$knO>4DKfl1`rF;Wiyi$)LjxefDCLMh7x=lw&W*qv2RkPcpA_;w z;wf@P&;Xcb6ubDJaI}+D^S9zAew$4eTX*ENWuqtZ0jg${c$aRQ|2Wv0gS^upZ+w0m zGB@A89KKx)eBSu%I3!D|aeDOP6<`#5&2Mk_l5LR59OM#j{{A*(di){`06(7dx|n z?-QST{dOdg^@(C`{+~TauHys7=bJ5Md!tvb07(-sx0hKNG``!1efF$b$UWbzmABKG zwq7PLB0kUWZr~+3?FDxWNE07K*hh?G%k@8*-g9?Bw;d`#Ma)1b}U&jc%)JEkW}7qsPAj5#RPiUZ?nNF0lA9 zhTF4R5CLGDnIp?$3%3xjVYd=L>2+N8Z46*v0OHdQHu^pQ@`?|#AcJ{w&Fy#8U-P5P zMMg_UeVQUZST>2iehoaG`1t(&$nD^&48I>kQnQlSf%MSr%IjbW>;+B+?vH#n15cSm zO}YY{PO&w1+RQ=5^G6$prhv4JSu*6oYr|tdku72fvNzurPZ|N=VPI&+#w&3xQ { + const response = await adminInstance.post('/products', product) + return response.data +} + +// 관리자 - 상품 조회 +export const adminFetchProducts = async () => { + const response = await adminInstance.get('/products') + return response.data +} + +// 관리자 - 상품 삭제 +export const adminDeleteProduct = async (productId: string) => { + const response = await adminInstance.delete(`/products/${productId}`) + return response.data +} + +// 관리자 - 상품 상세 조회 +export const adminGetProductDetail = async (productId: string) => { + const response = await baseInstance.get(`/products/${productId}`) + return response.data +} + +// 관리자 - 상품 판매 상태 변경 +export const adminChangeProductSaleStatus = async ( + productId: string, + isSoldOut: boolean +) => { + const response = await adminInstance.put(`/products/${productId}`, { + isSoldOut: isSoldOut + }) + return response.data +} + +// 관리자 - 상품 수정 +export const adminEditProduct = async (product: ProductAddBody) => { + const response = await adminInstance.put(`/products/${product.id}`, product) + return response.data +} + +// 관리자 - 사용자 목록 조회 +export const adminFetchCustomers = async () => { + const response = await axios + .all([ + await adminInstance.get('auth/users'), + await adminInstance.get('products/transactions/all') + ]) + .then( + axios.spread((res1, res2) => { + const customers = res1.data as Customer[] + const orders = res2.data as TransactionDetail[] + + const customerInfos: CustomerInfo[] = customers.map( + (customer: Customer) => { + const customerTransactions = orders.filter( + (order: TransactionDetail) => + order.user.email === customer.email && + (order.done || !order.isCanceled) + ) + return { + user: customer, + totalTransaction: customerTransactions.length, + totalTransactionPrice: customerTransactions.reduce( + (acc, current) => { + if (current.product.discountRate) { + return (acc += + current.product.price - + (current.product.price * current.product.discountRate) / + 100) + } + return (acc += current.product.price) + }, + 0 + ) + } + } + ) + + return customerInfos + }) + ) + return response +} + +// 관리자 - 대시보드 거래내역 조회 +export const fetchAdminTransactions = async () => { + const response = await adminInstance.get('products/transactions/all') + return response.data +} + +// 사용자 확인 +export const checkIsAdmin = async () => { + const response = await authInstance.post('/auth/me') + return response.data +} + +// 주문 취소/취소 철회 +export const changeIsCanceled = async ( + detailId: string, + isCanceled: boolean +) => { + const response = await adminInstance.put( + `/products/transactions/${detailId}`, + { + isCanceled: isCanceled + } + ) + return response.data +} + +// 구매 확정 처리 +export const adminOrderConfirm = async (detailId: string) => { + const response = await adminInstance.put( + `/products/transactions/${detailId}`, + { + done: true + } + ) + return response.data +} diff --git a/src/api/axios.ts b/src/api/axios.ts new file mode 100644 index 00000000..1114b84f --- /dev/null +++ b/src/api/axios.ts @@ -0,0 +1,16 @@ +import axios, { AxiosInstance } from 'axios' + +axios.defaults.baseURL = import.meta.env.VITE_BASE_URL +axios.defaults.headers.common['apikey'] = import.meta.env.VITE_APIKEY +axios.defaults.headers.common['username'] = import.meta.env.VITE_USERNAME + +// Authorization 설정이 없는 일반 사용자 API용 Instance +export const baseInstance: AxiosInstance = axios.create() + +// Authorization 설정이 추가된 로그인한 사용자 API용 Instance +export const authInstance: AxiosInstance = baseInstance + +// 관리자 API용 Instance +export const adminInstance: AxiosInstance = axios.create({ + headers: { masterKey: true } +}) diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 00000000..325e340e --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,6 @@ +export * from 'api/axios' +export * from 'api/signApi' +export * from 'api/adminRequests' +export * from 'api/mypageRequests' +export * from 'api/paymentRequests' +export * from 'api/productRequests' diff --git a/src/api/mypageRequests.ts b/src/api/mypageRequests.ts new file mode 100644 index 00000000..a8ba66ee --- /dev/null +++ b/src/api/mypageRequests.ts @@ -0,0 +1,20 @@ +import { authInstance } from 'api/index' + +export const featchUserOrders = async () => { + const res = await authInstance.get('products/transactions/details') + return res.data +} + +export const confirmOrder = async (orderId: string) => { + const res = await authInstance.post('products/ok', { + detailId: orderId + }) + return res.data +} + +export const cancelOrder = async (orderId: string) => { + const res = await authInstance.post('products/cancel', { + detailId: orderId + }) + return res.data +} diff --git a/src/api/paymentRequests.ts b/src/api/paymentRequests.ts new file mode 100644 index 00000000..116db754 --- /dev/null +++ b/src/api/paymentRequests.ts @@ -0,0 +1,47 @@ +import { authInstance } from '@/api' +import { CreateRequest, RemoveRequest, Transaction } from 'types/index' + +//선택 가능한 은행 목록 조회 +export const getBankLists = async () => { + const res = await authInstance.get('/account/banks') + return res.data +} + +//계좌 목록 및 잔액 조회 +export const getAccounts = async () => { + try { + const res = await authInstance.get('/account') + return res.data.accounts + } catch (error) { + console.error() + } +} + +//계좌 연결 +export const createAccount = async (request: CreateRequest) => { + try { + const res = await authInstance.post('/account', request) + return res.data + } catch (error) { + console.error() + } +} + +//계좌 해지 +export const removeAccount = async (req: RemoveRequest) => { + try { + const res = await authInstance.delete('/account', { data: req }) + return res.data + } catch (error) { + console.error() + } +} +//결제 +export const transactPayment = async (req: Transaction): Promise => { + try { + const res = await authInstance.post('/products/buy', req) + return res + } catch (error) { + console.error() + } +} diff --git a/src/api/productRequests.ts b/src/api/productRequests.ts new file mode 100644 index 00000000..8fea7b18 --- /dev/null +++ b/src/api/productRequests.ts @@ -0,0 +1,14 @@ +import { baseInstance } from 'api/axios' + +export const fetchAllProducts = async () => { + const res = await baseInstance.post('/products/search', { + searchText: '', + searchTags: [] + }) + return res.data +} + +export const getPorductDetail = async (id: string) => { + const res = await baseInstance.get(`/products/${id}`) + return res.data +} diff --git a/src/api/signApi.ts b/src/api/signApi.ts new file mode 100644 index 00000000..4c949338 --- /dev/null +++ b/src/api/signApi.ts @@ -0,0 +1,22 @@ +import { baseInstance, authInstance } from 'api/axios' +import { BodyInfo, IdInfo, RequestBody } from 'types/index' + +export const postInfo = async (bodyInfo: BodyInfo) => { + const res = await baseInstance.post('/auth/signup', bodyInfo) + return res.data +} + +export const getId = async (idInfo: IdInfo) => { + const res = await baseInstance.post('/auth/login', idInfo) + return res.data +} + +export const logOut = async () => { + const res = await authInstance.post('/auth/logout') + return res.data +} + +export const InfoModify = async (RequestBody: RequestBody) => { + const res = await authInstance.put('/auth/user', RequestBody) + return res.data +} diff --git a/src/components/App.tsx b/src/components/App.tsx new file mode 100644 index 00000000..55a263ae --- /dev/null +++ b/src/components/App.tsx @@ -0,0 +1,126 @@ +import { Outlet, useLocation, useNavigate } from 'react-router-dom' +import { Header, Badge, MyPageNav, Modal } from 'components/index' +import { CommonError, Product, ModalProps, CartProduct } from 'types/index' +import { useState, useEffect } from 'react' + +import { + LoginContext, + RecentlyContext, + LoginedUserContext, + WishListContext, + CartContext +} from 'contexts/index' +import { + useLocalStorage, + useSessionStorage, + useCartLocalStorage, + useAxiosInterceptor +} from 'hooks/index' +import styles from 'src/styles/components/mypage/mypage.module.scss' + +//App은 Outlet을 통해 슬래시로 페이지 경로 이동시의 최상위 컴포넌트로 설정했습니다 +export const App = () => { + const navigate = useNavigate() + const [isModalShow, setIsModalShow] = useState(false) + const [modalProps, setModalProps] = useState(null) + const [isLogined, setIsLogined] = useLocalStorage('isLogined', false) + const [userEmail, setUserEmail] = useLocalStorage('ColleyUser', '') + const [userCart, setUserCart] = useCartLocalStorage(userEmail, []) + const [recentlyViewedList, setRecentlyViewedList] = useSessionStorage< + Product[] + >('RecentlyViewed', []) + const [wishList, setWishList] = useLocalStorage( + `wish-${userEmail}`, + isLogined + ? JSON.parse(localStorage.getItem('wish-${userEmail}') as string) + : [], + isLogined + ) + + userCart.sort((a: CartProduct, b: CartProduct) => + a.product.id.toLowerCase() < b.product.id.toLowerCase() ? -1 : 1 + ) + + const path: string = useLocation().pathname + useEffect(() => { + if ( + path === '/mypage' || + path === '/mypage/order' || + path === '/mypage/wishlist' + ) { + if (isLogined === false) { + setIsModalShow(true) + setModalProps({ + title: '로그인', + content: '로그인이 필요한 서비스입니다.', + isTwoButton: false, + okButtonText: '확인', + onClickOkButton: () => { + setIsModalShow(false) + navigate('/signin') + } + }) + } + } + }, [path]) + + const handleLogout = () => { + setIsLogined(false) + setUserEmail('') + setUserCart([]) + localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN) + } + + const handleErrorModal = (error: CommonError) => { + setIsModalShow(true) + setModalProps({ + title: '오류', + content: error.message, + isTwoButton: false, + okButtonText: '확인', + onClickOkButton: () => { + setIsModalShow(false) + } + }) + } + + useAxiosInterceptor(handleLogout, handleErrorModal) + + return ( + <> + + + + + +
+ + {path.includes('/mypage') && isLogined ? ( +
+ + +
+ ) : ( + + )} + {isModalShow && modalProps ? ( + + ) : null} + + + + + + {/* 결제 페이지/회원가입 페이지 등은 footer미적용일 것 같아서 header만 기본으로 outlet과 함께 배치시켰습니다 */} + + ) +} diff --git a/src/components/admin/AdminCustomerItem.tsx b/src/components/admin/AdminCustomerItem.tsx new file mode 100644 index 00000000..2f920589 --- /dev/null +++ b/src/components/admin/AdminCustomerItem.tsx @@ -0,0 +1,69 @@ +import React, { useMemo } from 'react' +import { CustomerInfo } from 'types/index' +import styled from 'styles/components/admin/customerItem.module.scss' + +export const AdminCustomerItem = React.memo( + ({ user, totalTransaction, totalTransactionPrice }: CustomerInfo) => { + const customerGrade = useMemo(() => { + // 회원등급 (누적금액) + // 브론즈(0~10만) > 실버(10만~20만) > 골드(20만~50만) > 플래티넘(50만~100만) > 다이아몬드(100만~) + if (totalTransactionPrice < 100000) { + return '브론즈' + } else if ( + totalTransactionPrice >= 100000 && + totalTransactionPrice < 200000 + ) { + return '실버' + } else if ( + totalTransactionPrice >= 200000 && + totalTransactionPrice < 500000 + ) { + return '골드' + } else if ( + totalTransactionPrice >= 500000 && + totalTransactionPrice < 1000000 + ) { + return '플래티넘' + } else if (totalTransactionPrice >= 1000000) { + return '다이아몬드' + } + return '-' + }, [totalTransactionPrice]) + + const gradeColor = useMemo(() => { + switch (customerGrade) { + case '실버': + return 'grade--silver' + break + case '골드': + return 'grade--gold' + break + case '플래티넘': + return 'grade--platinum' + break + case '다이아몬드': + return 'grade--diamond' + break + default: + return 'grade--bronze' + break + } + }, [customerGrade]) + + return ( +
  • +
    {user.email}
    +
    {user.displayName}
    +
    + {customerGrade} +
    +
    + {totalTransaction.toLocaleString()}건 +
    +
    + {totalTransactionPrice.toLocaleString()}원 +
    +
  • + ) + } +) diff --git a/src/components/admin/AdminCustomerSkeleton.tsx b/src/components/admin/AdminCustomerSkeleton.tsx new file mode 100644 index 00000000..9970b68c --- /dev/null +++ b/src/components/admin/AdminCustomerSkeleton.tsx @@ -0,0 +1,18 @@ +import styled from 'src/styles/components/admin/adminSkeleton.module.scss' + +export const AdminCustomerSkeleton = () => { + return ( +
    +
    +
    +
    +
    +
    +
    + ) +} diff --git a/src/components/admin/AdminDashboardCard.tsx b/src/components/admin/AdminDashboardCard.tsx new file mode 100644 index 00000000..380d5b41 --- /dev/null +++ b/src/components/admin/AdminDashboardCard.tsx @@ -0,0 +1,22 @@ +import { AdminDashboardCardProps } from 'types/index' +import React from 'react' +import styled from 'styles/components/admin/adminCard.module.scss' + +export const AdminDashboardCard = React.memo( + ({ title, value, unitStr, isLoading }: AdminDashboardCardProps) => { + return ( +
    +

    {title}

    +
    + {isLoading ? ( +
    + ) : ( + {value} + )} + + {unitStr} +
    +
    + ) + } +) diff --git a/src/components/admin/AdminLoading.tsx b/src/components/admin/AdminLoading.tsx new file mode 100644 index 00000000..7ba25714 --- /dev/null +++ b/src/components/admin/AdminLoading.tsx @@ -0,0 +1,19 @@ +import styled from 'styles/pages/admin.module.scss' + +export const AdminLoading = () => { + return ( +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Loading...

    +
    +
    + ) +} diff --git a/src/components/admin/AdminMoreButton.tsx b/src/components/admin/AdminMoreButton.tsx new file mode 100644 index 00000000..09e811e7 --- /dev/null +++ b/src/components/admin/AdminMoreButton.tsx @@ -0,0 +1,48 @@ +import { AdminMoreButtonProps } from 'types/index' +import styled from 'styles/components/admin/productItem.module.scss' + +export const AdminMoreButton = ({ + isShow, + onToggleMenu, + onClickEdit, + onClickDelete, + product, + onClickChangeStatus +}: AdminMoreButtonProps) => { + const handleClickMenu = (e: React.MouseEvent) => { + e.stopPropagation() + onToggleMenu() + } + + const handleClickEdit = (e: React.MouseEvent) => { + e.stopPropagation() + onClickEdit() + } + + const handleClickDelete = (e: React.MouseEvent) => { + e.stopPropagation() + onClickDelete(product) + } + + return ( +
    +
    + more_vert +
    +
    +
      +
    • 상품 수정
    • +
    • 상품 삭제
    • +
    • + {product.isSoldOut ? '판매처리' : '품절처리'} +
    • +
    +
    +
    + ) +} diff --git a/src/components/admin/AdminNav.tsx b/src/components/admin/AdminNav.tsx new file mode 100644 index 00000000..9d91c958 --- /dev/null +++ b/src/components/admin/AdminNav.tsx @@ -0,0 +1,34 @@ +import { Link, useLocation } from 'react-router-dom' +import styled from 'styles/pages/admin.module.scss' + +export const AdminNav = () => { + const path: string = useLocation().pathname + + return ( + + ) +} diff --git a/src/components/admin/AdminPrivateRoute.tsx b/src/components/admin/AdminPrivateRoute.tsx new file mode 100644 index 00000000..b297419d --- /dev/null +++ b/src/components/admin/AdminPrivateRoute.tsx @@ -0,0 +1,84 @@ +import { useCallback, useEffect, useState, useContext } from 'react' +import { Outlet, useNavigate } from 'react-router-dom' +import { checkIsAdmin } from 'api/index' +import { AdminNav, Modal } from 'components/index' +import { CommonError, ModalProps } from 'types/index' +import { LoginContext, LoginedUserContext, CartContext } from 'contexts/index' +import { useAxiosInterceptor } from 'hooks/index' +import styled from 'styles/pages/admin.module.scss' + +export const AdminPrivateRoute = () => { + const [isAdmin, setIsAdmin] = useState(false) + const navigate = useNavigate() + + const moveSignIn = useCallback(() => { + alert('관리자만 접근할 수 있습니다.') + navigate('/signin') + }, [navigate]) + + const moveHome = useCallback(() => { + alert('관리자만 접근할 수 있습니다.') + navigate('/') + }, [navigate]) + + useEffect(() => { + checkIsAdmin() + .then(user => { + if (user.email !== import.meta.env.VITE_ADMIN_EMAIL) { + moveHome() + } else { + setIsAdmin(true) + } + }) + .catch(error => { + console.log(error) + moveSignIn() + }) + }, [moveSignIn, moveHome]) + + const { setIsLogined } = useContext(LoginContext) + const { setUserEmail } = useContext(LoginedUserContext) + const { setUserCart } = useContext(CartContext) + const [isModalShow, setIsModalShow] = useState(false) + const [modalProps, setModalProps] = useState(null) + + const handleLogout = () => { + setIsLogined(false) + setUserEmail('') + setUserCart([]) + localStorage.removeItem(import.meta.env.VITE_STORAGE_KEY_ACCESSTOKEN) + } + + const handleErrorModal = (error: CommonError) => { + setIsModalShow(true) + setModalProps({ + title: '오류', + content: error.message, + isTwoButton: false, + okButtonText: '확인', + onClickOkButton: () => { + setIsModalShow(false) + } + }) + } + + useAxiosInterceptor(handleLogout, handleErrorModal) + + return isAdmin ? ( +
    + + + {isModalShow && modalProps ? ( + + ) : null} +
    + ) : null +} diff --git a/src/components/admin/AdminProductItem.tsx b/src/components/admin/AdminProductItem.tsx new file mode 100644 index 00000000..b88db0a9 --- /dev/null +++ b/src/components/admin/AdminProductItem.tsx @@ -0,0 +1,84 @@ +import React from 'react' +import { useNavigate } from 'react-router-dom' +import { ProductItemProps } from 'types/index' +import { convertTagColor } from 'utils/index' +import { AdminMoreButton } from 'components/index' + +import styled from 'styles/components/admin/productItem.module.scss' + +export const AdminProductItem = React.memo( + ({ + product, + isMenuShow, + showMenu, + hideMenu, + onClickDelete, + onChangeSaleStatus + }: ProductItemProps) => { + const handleToogleMenu = React.useCallback(() => { + if (isMenuShow) { + hideMenu() + } else { + showMenu(product.id ?? '') + } + }, [hideMenu, showMenu, isMenuShow, product]) + + const navigate = useNavigate() + + const onClickProductEdit = React.useCallback(() => { + if (isMenuShow) { + hideMenu() + } + navigate('/admin/product-add', { state: product }) + }, [isMenuShow, hideMenu]) + + const onClickChangeStatus = React.useCallback(() => { + if (product.isSoldOut) { + onChangeSaleStatus(product.id, false) + } else { + onChangeSaleStatus(product.id, true) + } + }, []) + + return ( +
    +
    {product.title}
    +
    + {product.tags.map(tag => { + return ( + + {tag} + + ) + })} +
    +
    {product.price.toLocaleString()}원
    +
    + {product.discountRate === 0 || !product.discountRate + ? '-' + : `${product.discountRate}%`} +
    +
    + {product.isSoldOut ? ( + 품절 + ) : ( + 판매중 + )} +
    +
    + +
    +
    + ) + } +) diff --git a/src/components/admin/AdminProductItemHeader.tsx b/src/components/admin/AdminProductItemHeader.tsx new file mode 100644 index 00000000..28323784 --- /dev/null +++ b/src/components/admin/AdminProductItemHeader.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import styled from 'styles/components/admin/productItemHeader.module.scss' + +export const AdminProductItemHeader = React.memo(() => { + return ( +
    +
    상품명
    +
    태그
    +
    가격
    +
    할인율
    +
    판매상태
    +
    + ) +}) diff --git a/src/components/admin/AdminProductsSkeleton.tsx b/src/components/admin/AdminProductsSkeleton.tsx new file mode 100644 index 00000000..53903bb1 --- /dev/null +++ b/src/components/admin/AdminProductsSkeleton.tsx @@ -0,0 +1,18 @@ +import styled from 'styles/components/admin/adminSkeleton.module.scss' + +export const AdminProductsSkeleton = () => { + return ( +
    +
    +
    +
    +
    +
    +
    + ) +} diff --git a/src/components/admin/AdminSalesItem.tsx b/src/components/admin/AdminSalesItem.tsx new file mode 100644 index 00000000..10854783 --- /dev/null +++ b/src/components/admin/AdminSalesItem.tsx @@ -0,0 +1,74 @@ +import React, { useMemo } from 'react' +import moment from 'moment' +import { AdminSalesItemProps } from 'types/index' +import styled from 'styles/components/admin/adminSalesItem.module.scss' + +export const AdminSalesItem = React.memo( + ({ + detail, + onChangeOrderIsCanceled, + onClickOrderConfirm + }: AdminSalesItemProps) => { + const paidDate = useMemo(() => { + const date = moment(detail.timePaid).format('YYYY-MM-DD HH:mm') + return date + }, [detail]) + + const paidPrice = useMemo(() => { + if (detail.product.discountRate) { + return ( + (detail.product.price * (100 - detail.product.discountRate)) / 100 + ) + } else { + return detail.product.price + } + }, [detail]) + + const handleOrderCancelClick = () => { + onChangeOrderIsCanceled(detail.detailId, true) + } + + const handleOrderCancelBackClick = () => { + onChangeOrderIsCanceled(detail.detailId, false) + } + + const handleOrderConfirmClick = () => { + onClickOrderConfirm(detail.detailId) + } + + return ( +
  • +
    {paidDate.toString()}
    +
    {detail.user.email}
    +
    {detail.product.title}
    +
    + {paidPrice.toLocaleString()}원 +
    +
    + {!detail.done && detail.isCanceled ? ( + + ) : !detail.done ? ( + + ) : null} + {detail.done ? ( + 판매완료 + ) : ( + + )} +
    +
  • + ) + } +) diff --git a/src/components/admin/AdminSalesSkeleton.tsx b/src/components/admin/AdminSalesSkeleton.tsx new file mode 100644 index 00000000..1c32fe6b --- /dev/null +++ b/src/components/admin/AdminSalesSkeleton.tsx @@ -0,0 +1,18 @@ +import styled from 'styles/components/admin/adminSkeleton.module.scss' + +export const AdminSalesSkeleton = () => { + return ( +
    +
    +
    +
    +
    +
    +
    + ) +} diff --git a/src/components/admin/ProductAddForm.tsx b/src/components/admin/ProductAddForm.tsx new file mode 100644 index 00000000..68cdc657 --- /dev/null +++ b/src/components/admin/ProductAddForm.tsx @@ -0,0 +1,246 @@ +import { useCallback, useEffect, useRef, useState, useMemo } from 'react' +import styled from 'styles/components/admin/productAddForm.module.scss' +import { ProductAddBody, ProductAddFormProps } from 'types/index' + +export const ProductAddForm = ({ product, onSubmit }: ProductAddFormProps) => { + const [title, setTitle] = useState('') + const [description, setDescription] = useState('') + const [tagStr, setTagStr] = useState('') + const [price, setPrice] = useState('') + const [discountRate, setDiscountRate] = useState('') + const [isValid, setIsValid] = useState(false) + const isAddMode = useMemo(() => !product, [product]) + + useEffect(() => { + if (product) { + const { + title, + description, + tags, + price, + discountRate, + thumbnail, + photo + } = product + console.log(thumbnail) + setTitle(title) + setDescription(description) + setTagStr(tags.join(', ')) + setPrice(price.toString()) + setDiscountRate(discountRate ? discountRate.toString() : '') + setThumbnailImage(thumbnail ? thumbnail : '') + setDetailImage(photo ? photo : '') + } + }, [product]) + + // 유효성 검사 + useEffect(() => { + if ( + title && + description && + price && + tagStr && + !isNaN(parseInt(price)) && + !isNaN(parseInt(discountRate === '' ? '0' : discountRate)) && + parseInt(price) > 0 + ) { + setIsValid(true) + } else { + setIsValid(false) + } + }, [title, description, price, tagStr, discountRate]) + + // 대표이미지 + const fileInputRef = useRef(null) + const [thumbnailImage, setThumbnailImage] = useState(null) + + // 상세이미지 + const detailInputRef = useRef(null) + const [detailImage, setDetailImage] = useState(null) + + // 이미지 프리뷰 설정 + const handleUploadImage = (e: React.ChangeEvent) => { + const id = e.target.id + onChangeImage(e, id === 'thumbnail-upload') + } + + // 이미지 convert base64 + const onChangeImage = useCallback( + (e: React.ChangeEvent, isThumb: boolean) => { + if (!e.target.files?.length) { + return + } + const reader = new FileReader() + + reader.onloadend = () => { + const base64 = reader.result + if (base64) { + isThumb + ? setThumbnailImage(base64.toString()) + : setDetailImage(base64.toString()) + } + } + + reader.readAsDataURL(e.target.files[0]) + }, + [] + ) + + const onClickThumbnailImageUpload = useCallback((e: React.MouseEvent) => { + e.preventDefault() + if (!fileInputRef.current) { + return + } + fileInputRef.current.click() + }, []) + + const onClickDetailImageUpload = useCallback((e: React.MouseEvent) => { + e.preventDefault() + if (!detailInputRef.current) { + return + } + detailInputRef.current.click() + }, []) + + const handleSumitAddForm = (e: React.ChangeEvent) => { + e.preventDefault() + if (!isValid) return + const tags: string[] = tagStr.toUpperCase().split(', ') + let newProduct: ProductAddBody + if (isAddMode) { + newProduct = { + title, + description, + tags, + price: parseInt(price), + discountRate: parseInt(discountRate === '' ? '0' : discountRate), + thumbnailBase64: thumbnailImage, + photoBase64: detailImage + } + } else { + newProduct = { + id: product?.id, + title, + description, + tags, + price: parseInt(price), + discountRate: parseInt(discountRate === '' ? '0' : discountRate), + ...(thumbnailImage !== product?.thumbnail + ? { thumbnailBase64: thumbnailImage } + : {}), + ...(detailImage !== product?.photo ? { photoBase64: detailImage } : {}) + } + } + + onSubmit(newProduct) + } + + return ( +
    + +