From dcee1d09a834d19be8af7c3dd4909d6e068a0082 Mon Sep 17 00:00:00 2001 From: howooking Date: Thu, 22 Jun 2023 11:09:11 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=9C=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 181 +++++++++++----------- src/App.tsx | 12 +- src/components/Layout.tsx | 2 +- src/components/SubNavbar.tsx | 1 + src/components/product/ProductBar.tsx | 2 +- src/components/product/ProductSection.tsx | 2 +- src/routes/Login.tsx | 111 +++++++++++++ src/routes/LogoutNeededRoute.tsx | 15 ++ src/routes/SignUp.tsx | 171 ++++++++++++++++++++ src/store.ts | 7 +- 10 files changed, 406 insertions(+), 98 deletions(-) create mode 100644 src/routes/Login.tsx create mode 100644 src/routes/LogoutNeededRoute.tsx create mode 100644 src/routes/SignUp.tsx diff --git a/README.md b/README.md index 3033bfec..cb8e9d38 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,10 @@ $ npm run dev


-zustand : 전역 상태관리
-react-hot-toast : 팝업 안내 메시지
-nuka-carousel : 이미지 슬라이더
+ : 전역 상태관리
+ : 팝업 안내 메시지 +
+ : 이미지 슬라이더

# 화면 구성 @@ -94,9 +95,9 @@ nuka-carousel : 이미지 슬라이더
- 팀원 내 입문자를 배려하여 상대적으로 사용이 쉬운 [ZUSTAND](https://zustand-demo.pmnd.rs/)를 사용 - context wrapping하는 과정이 필요하지 않음 + - [src/store.ts](https://github.com/howooking/KDT5-M5/blob/0172a31077634c42139005c52c4e62156e3ab2ba/src/store.ts#L1-L64) ```js - (src/store.ts) import { create } from 'zustand'; import { authenticate } from '@/api/authApi'; import { ADMINS } from '@/constants/constants'; @@ -138,6 +139,10 @@ nuka-carousel : 이미지 슬라이더
isAdmin, }, }); + localStorage.setItem( + 'user', + JSON.stringify({ user, accessToken: userInfo.accessToken, isAdmin }) + ); return; } set({ @@ -186,6 +191,7 @@ nuka-carousel : 이미지 슬라이더
- 이 방법은 보안상 위험하지만 다음과 같은 대응 전략을 취할 수 있다. - 비건전한 사용자가 local storage에 접근하여 isAdmin을 true로 바꿀 경우
👉 관리자만 접근 할 수 있는 route 분기점에 인증 api를 사용하여 사용자의 신원을 확인한다. + - [src/routes/admin/Admin.tsx](https://github.com/howooking/KDT5-M5/blob/0172a31077634c42139005c52c4e62156e3ab2ba/src/routes/admin/Admin.tsx#L1C1-L26) ```js export default function Admin() { @@ -236,6 +242,7 @@ nuka-carousel : 이미지 슬라이더
- 로그인 상태, 관리자 여부에 따라서 접근할 수 있는 페이지를 제한해야 한다. - ProdtectedRoute에서 전역 User 상태와 adminRequired props 속성에 따라서 접근을 제한하게 하였다. + - [src/routes/ProtectedRoute.tsx](https://github.com/howooking/KDT5-M5/blob/0172a31077634c42139005c52c4e62156e3ab2ba/src/routes/ProtectedRoute.tsx#L1-L22) ```js import { Navigate } from 'react-router-dom'; @@ -284,11 +291,11 @@ nuka-carousel : 이미지 슬라이더
- 브랜치 전략 - 5명이 각자 맡은 기능의 branch를 생성하여 develope 브랜치에 merge하고 최종적으로 main 브랜치에 merge하는 방식으로 진행 - 이 보다는 git hub에서 pull request를 하고 다같이 리뷰를 한 후 merge하는 방식이 바람직하다. - - 지속적으로 develope 브렌치를 pull을 해야 한꺼번에 많은 양의 conflict가 발생하는 것을 방지할 수 있다. + - 정기적으로 develope 브렌치를 pull해야 한꺼번에 많은 양의 conflict가 발생하는 것을 방지할 수 있다. - commit 단위 & commit message - commit의 단위는 기능 단위여야 한다. - commit message를 적기 힘들다면 해당 commit은 너무 많은 기능을 담고 있을 가능성이 높다. - - commit 단위는 파일 단위가 아니여도 된다. 줄 단위로 commit이 가능하다! + - commit 단위는 파일 단위가 아니여도 된다. 줄 단위로 commit이 가능하다. - 5명의 commit message가 제각각이라 다른 사람의 commit을 한번에 이해하기 어려웠다. - 협업을 진행하기 전 commit 규칙을 반드시 세우고 시작해야 함

@@ -296,86 +303,84 @@ nuka-carousel : 이미지 슬라이더
# 디렉토리 구조 ``` -┣ 📂public -┃ ┣ 📂products -┃ ┣ 📂readme -┃ ┣ 📂slider -┣ 📂src -┃ ┣ 📂api -┃ ┃ ┣ 📜adminApi.ts -┃ ┃ ┣ 📜authApi.ts -┃ ┃ ┣ 📜bankApi.ts -┃ ┃ ┗ 📜transactionApi.ts -┃ ┣ 📂components -┃ ┃ ┣ 📂product -┃ ┃ ┃ ┣ 📜ProductBar.tsx -┃ ┃ ┃ ┣ 📜ProductCard.tsx -┃ ┃ ┃ ┣ 📜ProductSection.tsx -┃ ┃ ┃ ┗ 📜ProductSortOptions.tsx -┃ ┃ ┣ 📂ui -┃ ┃ ┃ ┣ 📜Breadcrumbs.tsx -┃ ┃ ┃ ┣ 📜Button.tsx -┃ ┃ ┃ ┣ 📜CrazyLoading.tsx -┃ ┃ ┃ ┣ 📜ImageUpload.tsx -┃ ┃ ┃ ┣ 📜Input.tsx -┃ ┃ ┃ ┣ 📜LoadingSpinner.tsx -┃ ┃ ┃ ┣ 📜ProfileImage.tsx -┃ ┃ ┃ ┣ 📜SectionTitle.tsx -┃ ┃ ┃ ┣ 📜Select.tsx -┃ ┃ ┃ ┗ 📜Skeleton.tsx -┃ ┃ ┣ 📜Footer.tsx -┃ ┃ ┣ 📜ImageSlider.tsx -┃ ┃ ┣ 📜Layout.tsx -┃ ┃ ┣ 📜Navbar.tsx -┃ ┃ ┣ 📜Search.tsx -┃ ┃ ┣ 📜SingleUser.tsx -┃ ┃ ┗ 📜SubNavbar.tsx -┃ ┣ 📂constants -┃ ┃ ┣ 📜constants.ts -┃ ┃ ┗ 📜library.ts -┃ ┣ 📂routes -┃ ┃ ┣ 📂admin -┃ ┃ ┃ ┣ 📜AddProduct.tsx -┃ ┃ ┃ ┣ 📜Admin.tsx -┃ ┃ ┃ ┣ 📜AdminClients.tsx -┃ ┃ ┃ ┣ 📜AdminProducts.tsx -┃ ┃ ┃ ┣ 📜AllTransactions.tsx -┃ ┃ ┃ ┗ 📜EditProduct.tsx -┃ ┃ ┣ 📂myAccount -┃ ┃ ┃ ┣ 📂bank -┃ ┃ ┃ ┃ ┣ 📜BankAccounts.tsx -┃ ┃ ┃ ┃ ┗ 📜ConnectBankAccount.tsx -┃ ┃ ┃ ┣ 📜ChangeName.tsx -┃ ┃ ┃ ┣ 📜ChangePassword.tsx -┃ ┃ ┃ ┣ 📜Info.tsx -┃ ┃ ┃ ┣ 📜Login.tsx -┃ ┃ ┃ ┣ 📜LogoutNeededRoute.tsx -┃ ┃ ┃ ┣ 📜MyAccount.tsx -┃ ┃ ┃ ┣ 📜OrderDetail.tsx -┃ ┃ ┃ ┣ 📜OrderList.tsx -┃ ┃ ┃ ┗ 📜SignUp.tsx -┃ ┃ ┣ 📜Home.tsx -┃ ┃ ┣ 📜NotFound.tsx -┃ ┃ ┣ 📜ProductDetail.tsx -┃ ┃ ┣ 📜Products.tsx -┃ ┃ ┣ 📜ProtectedRoute.tsx -┃ ┃ ┗ 📜SearchProducts.tsx -┃ ┣ 📜App.tsx -┃ ┣ 📜index.css -┃ ┣ 📜main.tsx -┃ ┣ 📜store.ts -┃ ┗ 📜vite-env.d.ts -┣ 📜.eslintrc.cjs -┣ 📜.gitignore -┣ 📜.prettierrc -┣ 📜custom.d.ts -┣ 📜index.html -┣ 📜package-lock.json -┣ 📜package.json -┣ 📜postcss.config.js -┣ 📜README.md -┣ 📜tailwind.config.js -┣ 📜tsconfig.json -┣ 📜tsconfig.node.json -┗ 📜vite.config.ts +kdt5-m5 + ┣ public + ┣ src + ┃ ┣ api + ┃ ┃ ┣ adminApi.ts + ┃ ┃ ┣ authApi.ts + ┃ ┃ ┣ bankApi.ts + ┃ ┃ ┗ transactionApi.ts + ┃ ┣ components + ┃ ┃ ┣ product + ┃ ┃ ┃ ┣ ProductBar.tsx + ┃ ┃ ┃ ┣ ProductCard.tsx + ┃ ┃ ┃ ┣ ProductSection.tsx + ┃ ┃ ┃ ┗ ProductSortOptions.tsx + ┃ ┃ ┣ ui + ┃ ┃ ┃ ┣ Breadcrumbs.tsx + ┃ ┃ ┃ ┣ Button.tsx + ┃ ┃ ┃ ┣ CrazyLoading.tsx + ┃ ┃ ┃ ┣ ImageUpload.tsx + ┃ ┃ ┃ ┣ Input.tsx + ┃ ┃ ┃ ┣ LoadingSpinner.tsx + ┃ ┃ ┃ ┣ ProfileImage.tsx + ┃ ┃ ┃ ┣ SectionTitle.tsx + ┃ ┃ ┃ ┣ Select.tsx + ┃ ┃ ┃ ┗ Skeleton.tsx + ┃ ┃ ┣ Footer.tsx + ┃ ┃ ┣ ImageSlider.tsx + ┃ ┃ ┣ Layout.tsx + ┃ ┃ ┣ Navbar.tsx + ┃ ┃ ┣ Search.tsx + ┃ ┃ ┣ SingleUser.tsx + ┃ ┃ ┗ SubNavbar.tsx + ┃ ┣ constants + ┃ ┃ ┣ constants.ts + ┃ ┃ ┗ library.ts + ┃ ┣ routes + ┃ ┃ ┣ admin + ┃ ┃ ┃ ┣ AddProduct.tsx + ┃ ┃ ┃ ┣ Admin.tsx + ┃ ┃ ┃ ┣ AdminClients.tsx + ┃ ┃ ┃ ┣ AdminProducts.tsx + ┃ ┃ ┃ ┣ AllTransactions.tsx + ┃ ┃ ┃ ┗ EditProduct.tsx + ┃ ┃ ┣ myAccount + ┃ ┃ ┃ ┣ bank + ┃ ┃ ┃ ┃ ┣ BankAccounts.tsx + ┃ ┃ ┃ ┃ ┗ ConnectBankAccount.tsx + ┃ ┃ ┃ ┣ ChangeName.tsx + ┃ ┃ ┃ ┣ ChangePassword.tsx + ┃ ┃ ┃ ┣ Info.tsx + ┃ ┃ ┃ ┣ MyAccount.tsx + ┃ ┃ ┃ ┣ OrderDetail.tsx + ┃ ┃ ┃ ┗ OrderList.tsx + ┃ ┃ ┣ Home.tsx + ┃ ┃ ┣ Login.tsx + ┃ ┃ ┣ LogoutNeededRoute.tsx + ┃ ┃ ┣ NotFound.tsx + ┃ ┃ ┣ ProductDetail.tsx + ┃ ┃ ┣ Products.tsx + ┃ ┃ ┣ ProtectedRoute.tsx + ┃ ┃ ┣ SearchProducts.tsx + ┃ ┃ ┗ SignUp.tsx + ┃ ┣ App.tsx + ┃ ┣ index.css + ┃ ┣ main.tsx + ┃ ┣ store.ts + ┃ ┗ vite-env.d.ts + ┣ .eslintrc.cjs + ┣ .gitignore + ┣ .prettierrc + ┣ custom.d.ts + ┣ index.html + ┣ package-lock.json + ┣ package.json + ┣ postcss.config.js + ┣ README.md + ┣ tailwind.config.js + ┣ tsconfig.json + ┣ tsconfig.node.json + ┗ vite.config.ts ``` diff --git a/src/App.tsx b/src/App.tsx index d2ab5601..8be54eed 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,8 +2,7 @@ import { Route, Routes } from 'react-router-dom'; import Layout from '@/components/Layout'; import Home from '@/routes/Home'; import NotFound from '@/routes/NotFound'; -import Login from '@/routes/myAccount/Login'; -import SignUp from '@/routes/myAccount/SignUp'; +import Login from '@/routes/Login'; import Admin from '@/routes/admin/Admin'; import AddProduct from '@/routes/admin/AddProduct'; import ProtectedRoute from '@/routes/ProtectedRoute'; @@ -19,10 +18,11 @@ import ProductDetail from '@/routes/ProductDetail'; import BankAccounts from '@/routes/myAccount/bank/BankAccounts'; import SearchProducts from '@/routes/SearchProducts'; import OrderList from '@/routes/myAccount/OrderList'; -import AllTransactions from './routes/admin/AllTransactions'; -import OrderDetail from './routes/myAccount/OrderDetail'; -import LogoutNeededRoute from './routes/myAccount/LogoutNeededRoute'; -import EditProduct from './routes/admin/EditProduct'; +import AllTransactions from '@/routes/admin/AllTransactions'; +import OrderDetail from '@/routes/myAccount/OrderDetail'; +import EditProduct from '@/routes/admin/EditProduct'; +import LogoutNeededRoute from '@/routes/LogoutNeededRoute'; +import SignUp from '@/routes/SignUp'; export default function App() { return ( diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 748f4aa1..e1f17267 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,7 +1,7 @@ import { Outlet } from 'react-router-dom'; import Navbar from '@/components/Navbar'; import { Toaster } from 'react-hot-toast'; -import Footer from './Footer'; +import Footer from '@/components/Footer'; export default function Layout() { return ( diff --git a/src/components/SubNavbar.tsx b/src/components/SubNavbar.tsx index 76497016..266996ab 100644 --- a/src/components/SubNavbar.tsx +++ b/src/components/SubNavbar.tsx @@ -1,4 +1,5 @@ import { Link } from 'react-router-dom'; + interface SubNavbarProps { menus: { label: string; diff --git a/src/components/product/ProductBar.tsx b/src/components/product/ProductBar.tsx index 81015fe4..57cbc2fb 100644 --- a/src/components/product/ProductBar.tsx +++ b/src/components/product/ProductBar.tsx @@ -1,7 +1,7 @@ import { DICTIONARY_SHOES, PRODUCT_BRAND } from '@/constants/constants'; import Breadcrumbs from '@/components/ui/Breadcrumbs'; import SectionTitle from '@/components/ui/SectionTitle'; -import Button from '../ui/Button'; +import Button from '@/components/ui/Button'; import ProductSortOptions from '@/components/product/ProductSortOptions'; interface ProductBarProps { diff --git a/src/components/product/ProductSection.tsx b/src/components/product/ProductSection.tsx index 06bfb5b8..0ffd9230 100644 --- a/src/components/product/ProductSection.tsx +++ b/src/components/product/ProductSection.tsx @@ -5,7 +5,7 @@ import ProductBar from '@/components/product/ProductBar'; import ProductCard from '@/components/product/ProductCard'; import Skeleton from '@/components/ui/Skeleton'; import toast from 'react-hot-toast'; -import SectionTitle from '../ui/SectionTitle'; +import SectionTitle from '@/components/ui/SectionTitle'; interface ProductSectionProps { category?: string; diff --git a/src/routes/Login.tsx b/src/routes/Login.tsx new file mode 100644 index 00000000..69eaa699 --- /dev/null +++ b/src/routes/Login.tsx @@ -0,0 +1,111 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { userStore } from '@/store'; +import { signIn } from '@/api/authApi'; +import { ADMINS, EMAIL_REGEX } from '@/constants/constants'; +import Input from '@/components/ui/Input'; +import Button from '@/components/ui/Button'; +import LoadingSpinner from '@/components/ui/LoadingSpinner'; +import SectionTitle from '@/components/ui/SectionTitle'; +import toast from 'react-hot-toast'; + +export default function Login() { + // 전역 로컬 유저를 세팅해주는 함수 + const { setUser } = userStore(); + + //로그인 후 직전의 페이지로 이동하기 위해 + const navigate = useNavigate(); + + const [isSending, setIsSending] = useState(false); + const [loginData, setLoginData] = useState({ email: '', password: '' }); + + const handleChange = (event: React.ChangeEvent) => { + const { name, value } = event.target; + setLoginData({ + ...loginData, + [name]: value, + }); + }; + + const handleLogin = async (event: React.FormEvent) => { + // form이벤트의 기본 새로고침을 막음 + event.preventDefault(); + + // 이메일과 비밀번호를 입력하지 않은경우 + if (loginData.email.trim() === '' || loginData.password.trim() === '') { + toast.error('이메일 또는 비밀번호를 입력해주세요.', { id: 'login' }); + return; + } + + // 이메일의 유효성 검사 + if (!EMAIL_REGEX.test(loginData.email)) { + toast.error('올바른 이메일을 입력해주세요.', { id: 'login' }); + return; + } + + // 통신 시작 + setIsSending(true); + toast.loading('로그인 중', { id: 'login' }); + const res = await signIn(loginData); + + // 로그인 성공 + if (res.statusCode === 200) { + const user = res.data as UserResponseValue; + // 어드민 여부 확인(보안상 매우 안좋음) + const isAdmin = ADMINS.includes(user.user.email); + // // 로컬 저장소에 user정보와 isAdmin을 문자열화시켜서 저장 + localStorage.setItem('user', JSON.stringify({ ...user, isAdmin })); + // 로컬 user의 상태도 저장 + setUser({ ...user, isAdmin }); + setIsSending(false); + navigate(-1); + toast.success(isAdmin ? '주인님 오셨습니다!👸👸' : res.message, { + id: 'login', + }); + return; + } + + // 로그인 실패 + const errorMessage = res.message; + toast.error(errorMessage, { id: 'login' }); + setIsSending(false); + }; + + return ( +
+
+ +
+
+ + +
+
+
+
+
+
+ ); +} diff --git a/src/routes/LogoutNeededRoute.tsx b/src/routes/LogoutNeededRoute.tsx new file mode 100644 index 00000000..6b2b685b --- /dev/null +++ b/src/routes/LogoutNeededRoute.tsx @@ -0,0 +1,15 @@ +import { Navigate } from 'react-router-dom'; +import { userStore } from '@/store'; + +type LogoutNeededRouteProps = { + element: React.ReactNode; +}; + +export default function LogoutNeededRoute({ element }: LogoutNeededRouteProps) { + const { userInfo } = userStore(); + + if (userInfo) { + return ; + } + return <>{element}; +} diff --git a/src/routes/SignUp.tsx b/src/routes/SignUp.tsx new file mode 100644 index 00000000..7b7b499f --- /dev/null +++ b/src/routes/SignUp.tsx @@ -0,0 +1,171 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { userStore } from '@/store'; +import { signUp } from '@/api/authApi'; +import { EMAIL_REGEX } from '@/constants/constants'; +import Button from '@/components/ui/Button'; +import Input from '@/components/ui/Input'; +import LoadingSpinner from '@/components/ui/LoadingSpinner'; +import ImageUpload from '@/components/ui/ImageUpload'; +import SectionTitle from '@/components/ui/SectionTitle'; +import toast from 'react-hot-toast'; + +export default function SignUp() { + const navigate = useNavigate(); + const { setUser } = userStore(); + + const [signUpData, setSignData] = useState({ + email: '', + password: '', + passwordRepeat: '', + displayName: '', + profileImgBase64: '', + }); + + const [isSending, setIsSending] = useState(false); + + const handleChange = (event: React.ChangeEvent) => { + const { name, value } = event.target; + // 이미지 파일 다루는 로직 + if (name === 'profileImgBase64') { + const files = event.target.files as FileList; + const reader = new FileReader(); + reader.readAsDataURL(files[0]); + reader.onloadend = () => { + setSignData({ ...signUpData, [name]: reader.result as string }); + }; + } + setSignData((prevData) => ({ + ...prevData, + [name]: value, + })); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + // 이메일 or 비밀번호 or 이름을 입력하지 않은경우 + if ( + signUpData.email.trim() === '' || + signUpData.password.trim() === '' || + signUpData.displayName.trim() === '' + ) { + toast.error('이메일, 비밀번호, 닉네임을 모두 입력해주세요.', { + id: 'signUp', + }); + return; + } + + // 이메일의 유효성 검사 + if (!EMAIL_REGEX.test(signUpData.email)) { + toast.error('올바른 이메일을 입력해주세요.', { + id: 'signUp', + }); + return; + } + + // 비번 8자리 유효성검사 + if (signUpData.password.length < 7) { + toast.error('비밀번호를 8자리 이상 입력해주세요.', { + id: 'signUp', + }); + return; + } + + // 이름 길이 유효성검사 + if (signUpData.displayName.length > 20) { + toast.error('닉네임은 20자 이하로 작성해주세요.', { + id: 'signUp', + }); + return; + } + + // 비밀번호 확인 + if (signUpData.password !== signUpData.passwordRepeat) { + toast.error('비밀번호가 일치하지 않습니다.', { + id: 'signUp', + }); + return; + } + + // 통신 시작 + setIsSending(true); + toast.loading('회원가입 요청 중...', { id: 'signUp' }); + const res = await signUp(signUpData); + // 회원가입에 성공하는 경우 + if (res.statusCode === 200) { + const user = res.data as UserResponseValue; + setIsSending(false); + toast.success(`${user.user.displayName}님 즐거운 쇼핑 되세요!`, { + id: 'signUp', + }); + // 새로 가입하는 사람이 관리자일리 없음 + localStorage.setItem('user', JSON.stringify({ ...user, isAdmin: false })); + setUser({ ...user, isAdmin: false }); + navigate('/', { replace: true }); + return; + } + // 회원가입 실패 + toast.error(res.message, { + id: 'signUp', + }); + setIsSending(false); + }; + + return ( +
+
+ +
+
+ + + + + +
+
+
+
+
+
+ ); +} diff --git a/src/store.ts b/src/store.ts index 4d926326..166b425b 100644 --- a/src/store.ts +++ b/src/store.ts @@ -37,7 +37,6 @@ export const userStore = create((set) => ({ }); return '로그인을 해주세요.'; } - // 로컬저장소에 user가 있는 경우 const res = await authenticate(userInfo.accessToken); // 인증에 성공하는 경우 @@ -52,6 +51,12 @@ export const userStore = create((set) => ({ isAdmin, }, }); + // 로컬 저장소에도 세팅 + localStorage.setItem( + 'user', + JSON.stringify({ user, accessToken: userInfo.accessToken, isAdmin }) + ); + return; } // 토큰은 있으나 인증에 실패한 경우 (expired or 토큰 값 인위적 조작)