From cd9aff3bf601b902aeaa55a32ba34a452f5e74f8 Mon Sep 17 00:00:00 2001 From: jhynsoo Date: Wed, 1 Nov 2023 16:58:36 +0900 Subject: [PATCH] feat: Add logout, my posts and password change page --- package-lock.json | 21 +++-- package.json | 1 + src/API.js | 2 + src/Router.jsx | 14 +++ src/components/Post/PostList.jsx | 5 +- src/hooks/network/post.js | 11 ++- src/pages/MyPage/index.jsx | 7 +- src/pages/MyPost/index.jsx | 26 ++++++ src/pages/PasswordChanger/index.jsx | 132 +++++++++++++++++++++++++++ src/pages/Posts/index.jsx | 15 ++- src/styles/MyPost.styles.js | 5 + src/styles/PasswordChanger.styles.js | 20 ++++ src/styles/Themes.styles.js | 1 + 13 files changed, 242 insertions(+), 18 deletions(-) create mode 100644 src/pages/MyPost/index.jsx create mode 100644 src/pages/PasswordChanger/index.jsx create mode 100644 src/styles/MyPost.styles.js create mode 100644 src/styles/PasswordChanger.styles.js diff --git a/package-lock.json b/package-lock.json index 2034cde..83e7aa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@toss/error-boundary": "^1.4.4", "axios": "^1.5.0", "chart.js": "^4.4.0", + "qs": "^6.11.2", "react": "^18.2.0", "react-chartjs-2": "^5.2.0", "react-cookie": "^6.1.1", @@ -2688,7 +2689,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -3571,7 +3571,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -3722,7 +3721,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -3734,7 +3732,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -4448,7 +4445,6 @@ "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4735,6 +4731,20 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5159,7 +5169,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", diff --git a/package.json b/package.json index ef20bc2..65da587 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@toss/error-boundary": "^1.4.4", "axios": "^1.5.0", "chart.js": "^4.4.0", + "qs": "^6.11.2", "react": "^18.2.0", "react-chartjs-2": "^5.2.0", "react-cookie": "^6.1.1", diff --git a/src/API.js b/src/API.js index a89b32c..6c1e3f2 100644 --- a/src/API.js +++ b/src/API.js @@ -29,6 +29,8 @@ export const URL = { login: '/user/login/', logout: '/user/logout/', myPage: '/user/mypage/', + myPost: '/mypost/', + password: '/user/password/', user: '/user/', brand: '/brand/', product: '/product/', diff --git a/src/Router.jsx b/src/Router.jsx index 31c5996..e1876b7 100644 --- a/src/Router.jsx +++ b/src/Router.jsx @@ -15,6 +15,8 @@ import WriteSteps from './pages/WriteSteps'; import CurrencyInput from './components/CurrencyInput'; import { useState } from 'react'; import MyPage from './pages/MyPage'; +import PasswordChanger from './pages/PasswordChanger'; +import MyPost from './pages/MyPost'; const Router = () => ( @@ -40,6 +42,14 @@ const Router = () => ( path={URL.myPage} element={} /> + } + /> + } + /> } @@ -69,6 +79,10 @@ const Router = () => ( element={} /> + } + /> } diff --git a/src/components/Post/PostList.jsx b/src/components/Post/PostList.jsx index 0c704d3..f5c18d1 100644 --- a/src/components/Post/PostList.jsx +++ b/src/components/Post/PostList.jsx @@ -1,9 +1,6 @@ -import { usePosts } from '../../hooks/network/post'; import PostItem from '../PostItem'; -const PostList = ({ page }) => { - const { data } = usePosts({ page }); - +const PostList = ({ data, page }) => { return ( <> {data?.results?.map((post) => ( diff --git a/src/hooks/network/post.js b/src/hooks/network/post.js index 2886bd7..935bd4c 100644 --- a/src/hooks/network/post.js +++ b/src/hooks/network/post.js @@ -1,14 +1,17 @@ import { API, URL } from '../../API'; import { useQuery } from 'react-query'; -const getPosts = async ({ page }) => { +const getPosts = async ({ my, page }) => { const pageString = page ? `?page=${page}` : ''; - const { data } = await API.get(`${URL.post}${pageString}`); + const urlString = my + ? `${URL.post}my${pageString}` + : `${URL.post}${pageString}`; + const { data } = await API.get(urlString); return data; }; -export const usePosts = ({ page }) => { - const data = useQuery('posts', () => getPosts({ page }), { +export const usePosts = ({ my, page }) => { + const data = useQuery(['post', my, page], () => getPosts({ my, page }), { suspense: true, }); diff --git a/src/pages/MyPage/index.jsx b/src/pages/MyPage/index.jsx index 4f05ea3..1c9b7d4 100644 --- a/src/pages/MyPage/index.jsx +++ b/src/pages/MyPage/index.jsx @@ -1,3 +1,4 @@ +import { URL } from '../../API'; import Title from '../../components/Title'; import { SubTitle } from '../../styles/Common.styles'; import * as S from '../../styles/MyPage.styles'; @@ -10,12 +11,12 @@ function MyPage() { 거래 - 내가 쓴 판매글 + 내가 쓴 판매글 계정 - 내가 쓴 판매글 - 비밀번호 바꾸기 + 비밀번호 바꾸기 + 로그아웃 회원 탈퇴 diff --git a/src/pages/MyPost/index.jsx b/src/pages/MyPost/index.jsx new file mode 100644 index 0000000..6e1c948 --- /dev/null +++ b/src/pages/MyPost/index.jsx @@ -0,0 +1,26 @@ +import { useLocation } from 'react-router-dom'; +import Title from '../../components/Title'; +import * as S from '../../styles/MyPost.styles'; +import { usePosts } from '../../hooks/network/post'; +import QueryString from 'qs'; +import PostList from '../../components/Post/PostList'; + +function MyPost() { + const location = useLocation(); + const queryData = QueryString.parse(location.search, { + ignoreQueryPrefix: true, + }); + const page = queryData.page || 1; + const { data } = usePosts({ my: true, page }); + return ( + + 내가 쓴 판매글 + + + ); +} + +export default MyPost; diff --git a/src/pages/PasswordChanger/index.jsx b/src/pages/PasswordChanger/index.jsx new file mode 100644 index 0000000..166a1a1 --- /dev/null +++ b/src/pages/PasswordChanger/index.jsx @@ -0,0 +1,132 @@ +import { Fragment, useState } from 'react'; +import Title from '../../components/Title'; +import * as S from '../../styles/PasswordChanger.styles'; +import ButtonArea from '../../components/ButtonArea'; +import Button from '../../components/Button'; +import { Input, SubTitle } from '../../styles/Common.styles'; +import { API, URL } from '../../API'; + +function PasswordChanger() { + const inputField = [ + { + id: 'oldPassword', + name: '현재 비밀번호', + placeholder: '현재 비밀번호를 입력하세요', + }, + { + id: 'newPassword', + name: '새 비밀번호', + placeholder: '비밀번호는 8자리 이상 영문, 숫자를 조합해서 입력하세요', + }, + { + id: 'newPasswordConfirm', + name: '새 비밀번호 확인', + placeholder: '새 비밀번호를 한번 더 입력하세요', + }, + ]; + const [passwordData, setPasswordData] = useState({ + oldPassword: { value: '' }, + newPassword: { value: '' }, + newPasswordConfirm: { value: '' }, + error: { + id: null, + msg: null, + }, + }); + const handleInputChange = (fieldName) => (event) => { + const newValue = event.target.value; + setPasswordData((prevData) => ({ + ...prevData, + [fieldName]: { ...prevData[fieldName], value: newValue }, + })); + }; + const setError = (id, msg) => { + setPasswordData((prevData) => ({ + ...prevData, + error: { + id, + msg, + }, + })); + }; + const handleSubmit = (event) => { + if (passwordData.oldPassword.value === '') { + setError('oldPassword', '현재 비밀번호를 입력해주세요'); + return; + } + if (passwordData.newPassword.value === '') { + setError('newPassword', '새 비밀번호를 입력해주세요'); + return; + } + if (passwordData.oldPassword.value === passwordData.newPassword.value) { + setError('newPassword', '현재 비밀번호와 다른 비밀번호를 입력해주세요'); + return; + } + if ( + passwordData.newPassword.value !== passwordData.newPasswordConfirm.value + ) { + setError('newPasswordConfirm', '비밀번호가 일치하지 않습니다'); + return; + } + if ( + passwordData.newPassword.value.length < 8 || + !passwordData.newPassword.value.match( + /^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]+$/ + ) + ) { + setError( + 'newPassword', + '비밀번호는 영문과 숫자를 포함하고 8자리 이상이어야 합니다' + ); + return; + } + API.patch(URL.password, { + oldPassword: passwordData.oldPassword.value, + newPassword: passwordData.newPassword.value, + }) + .then((response) => { + if (response.status === 204) { + alert('비밀번호가 변경되었습니다\n다시 로그인해주세요'); + window.location.href = '/'; + return; + } + alert('에러'); + }) + .catch(({ response }) => { + if (response.status === 400) { + setError('newPassword', response.data.msg); + return; + } + }); + }; + + return ( + <> + 비밀번호 바꾸기 + + {inputField.map(({ id, name, placeholder }, index) => ( + + {name} + + + + {passwordData.error.id === id && ( + {passwordData.error.msg} + )} + + ))} + + + + + + ); +} + +export default PasswordChanger; diff --git a/src/pages/Posts/index.jsx b/src/pages/Posts/index.jsx index 2bc08d6..cd7011b 100644 --- a/src/pages/Posts/index.jsx +++ b/src/pages/Posts/index.jsx @@ -1,13 +1,26 @@ import * as S from '../../styles/Posts.styles'; +import QueryString from 'qs'; // import { Title } from '../../styles/Common.styles'; import PostList from '../../components/Post/PostList'; import Title from '../../components/Title'; +import { usePosts } from '../../hooks/network/post'; +import { useLocation } from 'react-router-dom'; const Posts = () => { + const location = useLocation(); + const queryData = QueryString.parse(location.search, { + ignoreQueryPrefix: true, + }); + const page = queryData.page || 1; + const { data } = usePosts({ page }); + return ( 최신글 - + ); }; diff --git a/src/styles/MyPost.styles.js b/src/styles/MyPost.styles.js new file mode 100644 index 0000000..3cafdaa --- /dev/null +++ b/src/styles/MyPost.styles.js @@ -0,0 +1,5 @@ +import styled from 'styled-components'; + +export const MyPost = styled.div` + margin: 0 auto; +`; diff --git a/src/styles/PasswordChanger.styles.js b/src/styles/PasswordChanger.styles.js new file mode 100644 index 0000000..92137ed --- /dev/null +++ b/src/styles/PasswordChanger.styles.js @@ -0,0 +1,20 @@ +import styled from 'styled-components'; + +export const PasswordChanger = styled.div` + display: flex; + flex-direction: column; +`; + +export const PasswordInput = styled.div` + ${({ theme }) => theme.boxShadow}; + ${({ theme }) => theme.hoverBorder}; + ${({ theme }) => theme.activeTransform}; + padding: 0; +`; + +export const ErrorText = styled.p` + color: ${({ theme }) => theme.colors.error}; + font-size: 14px; + font-weight: 600; + margin: 14px 0 14px 20px; +`; diff --git a/src/styles/Themes.styles.js b/src/styles/Themes.styles.js index 40af524..db25b33 100644 --- a/src/styles/Themes.styles.js +++ b/src/styles/Themes.styles.js @@ -11,6 +11,7 @@ const colors = { gray700: '#4e5968', gray800: '#333d4b', gray900: '#191f28', + error: '#e0245e', background: '#fff', grayBackground: '#f2f4f6', primary: '#2060ee',