diff --git a/src/components/Input/Input/index.js b/src/components/Input/index.js similarity index 100% rename from src/components/Input/Input/index.js rename to src/components/Input/index.js diff --git a/src/components/Input/Input/style.js b/src/components/Input/style.js similarity index 100% rename from src/components/Input/Input/style.js rename to src/components/Input/style.js diff --git a/src/components/TopBar/components/ChangePasswordModalChildren.js b/src/components/TopBar/components/ChangePasswordModalChildren.js new file mode 100644 index 0000000..4da9f24 --- /dev/null +++ b/src/components/TopBar/components/ChangePasswordModalChildren.js @@ -0,0 +1,29 @@ +import Input from '../../Input'; +import { VStack } from '../../../styles/Stack.styles'; +import { forwardRef } from 'react'; + +const ChangePasswordModalChildren = forwardRef( + ( + { nextPasswordRef, changePasswordFocus, checkedNextPasswordRef, checkedChangePasswordFocus }, + ref + ) => { + return ( + + + + + ); + } +); + +export default ChangePasswordModalChildren; diff --git a/src/components/TopBar/components/TopBarLeft.js b/src/components/TopBar/components/TopBarLeft.js new file mode 100644 index 0000000..a0f6568 --- /dev/null +++ b/src/components/TopBar/components/TopBarLeft.js @@ -0,0 +1,15 @@ +import { useNavigate } from 'react-router-dom'; +import logo from '../../../assets/logo.svg'; +import typo from '../../../assets/typo.svg'; +import { HStack } from '../../../styles/Stack.styles'; + +const TopBarLeft = () => { + const navigate = useNavigate(); + return ( + navigate('/')}> + logo + typo + + ); +}; +export default TopBarLeft; diff --git a/src/components/TopBar/components/TopBarRight.js b/src/components/TopBar/components/TopBarRight.js new file mode 100644 index 0000000..0ca5519 --- /dev/null +++ b/src/components/TopBar/components/TopBarRight.js @@ -0,0 +1,44 @@ +import S from '../style'; +import { HStack } from '../../../styles/Stack.styles'; +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import useLoginState from '../../../hooks/useLoginState'; +import UserMenuConainer from './UserMenuContainer'; +import { TopBarItems } from '../constants'; +import { goRoute } from '../utils'; + +const TopBarRight = () => { + const [selectedItem, _] = useState(window.location.pathname); + const { isLoggedIn } = useLoginState(); + const navigate = useNavigate(); + + useEffect(() => { + const shopChecked = localStorage.getItem('shopChecked'); + if (shopChecked === null) { + // shopChecked 항목이 없으면 false로 초기화 + localStorage.setItem('shopChecked', 'false'); + } + }, []); + + return ( + + {TopBarItems.map((item, index) => ( + goRoute(item.route, selectedItem, navigate)} + > + {item.name} + + ))} + {isLoggedIn ? ( + + ) : ( + navigate('/login')}> + 로그인 + + )} + + ); +}; +export default TopBarRight; diff --git a/src/components/TopBar/components/UserMenuChildren.js b/src/components/TopBar/components/UserMenuChildren.js new file mode 100644 index 0000000..951a226 --- /dev/null +++ b/src/components/TopBar/components/UserMenuChildren.js @@ -0,0 +1,36 @@ +import Button from '../../Buttons'; +const UserMenuChildren = ({ + showChangePasswordModal, + showProfileChangeModal, + hideUserMenu, + showLogoutModal, +}) => { + return ( + <> + + + + + ); +}; +export default UserMenuChildren; diff --git a/src/components/TopBar/components/UserMenuContainer.js b/src/components/TopBar/components/UserMenuContainer.js new file mode 100644 index 0000000..720d75c --- /dev/null +++ b/src/components/TopBar/components/UserMenuContainer.js @@ -0,0 +1,110 @@ +import { useState, useRef } from 'react'; +import useUserState from '../../../hooks/useUserState'; +import useLoginState from '../../../hooks/useLoginState'; + +import { Message } from '../../Message'; +import useModal from '../../../hooks/useModal'; +import useContainer from '../../../hooks/useContainer'; + +import { checkPasswordValidate, uploadImage } from '../utils'; +import { renderUserImage } from '../utils/renderUserImage'; + +import ChangePasswordModalChildren from '../components/ChangePasswordModalChildren'; +import ProfileChangeModalChildren from '../components/ProfileChangeModalChildren'; +import UserMenuChildren from '../components/UserMenuChildren'; + +import { HStack } from '../../../styles/Stack.styles'; + +const UserMenuConainer = () => { + const [messageText, setMessageText] = useState(''); + const [selectedFile, setselectedFile] = useState(null); + const [changePasswordFocus, setChangePasswordFocus] = useState(false); + const [checkedChangePasswordFocus, setCheckedChangePasswordFocus] = useState(false); + + const nextPasswordRef = useRef(); + const checkedNextPasswordRef = useRef(); + + const userMenu = useContainer(); + const { user, setUserInfo } = useUserState(); + const { initLoginStatus } = useLoginState(); + + const logoutModal = useModal({ + description: '정말 로그아웃하시겠어요?', + cancelText: '취소', + okText: '확인', + closable: true, + onOk: () => { + initLoginStatus(); + }, + }); + + const profileChangeModal = useModal({ + description: '변경할 이미지를 올려주세요', + cancelText: '취소', + okText: '확인', + closable: true, + onOk: () => + uploadImage(selectedFile, setUserInfo, setselectedFile, setMessageText, toastMessage), + }); + + const changePasswordModal = useModal({ + cancelText: '취소', + closable: true, + onOk: () => + checkPasswordValidate( + nextPasswordRef, + checkedNextPasswordRef, + setChangePasswordFocus, + setCheckedChangePasswordFocus, + setMessageText, + changePasswordModal, + toastMessage + ), + }); + const toastMessage = Message(); + return ( + <> + {toastMessage.render({ + children: ( + +
+
{messageText}
+
+ ), + })} + {profileChangeModal.render({ + children: ( + + ), + })} + {changePasswordModal.render({ + children: ( + + ), + })} + {logoutModal.render()} +
+ {userMenu.render({ + children: ( + + ), + })} +
+ {renderUserImage(user, userMenu)} + + ); +}; +export default UserMenuConainer; diff --git a/src/components/TopBar/constants/index.js b/src/components/TopBar/constants/index.js new file mode 100644 index 0000000..253ef22 --- /dev/null +++ b/src/components/TopBar/constants/index.js @@ -0,0 +1,17 @@ +import { PATH_NAME } from '../../../constants/index'; + +export const MIN_PASSWORD_LENGTH = 4; +export const TopBarItems = [ + { + name: '문제 목록', + route: PATH_NAME.PROBLEM_LIST, + }, + { + name: '구성원', + route: PATH_NAME.MEMBER, + }, + { + name: '상점', + route: PATH_NAME.SHOP, + }, +]; diff --git a/src/components/TopBar/index.js b/src/components/TopBar/index.js index 58975c5..3148814 100644 --- a/src/components/TopBar/index.js +++ b/src/components/TopBar/index.js @@ -1,306 +1,14 @@ -import styled from 'styled-components'; -import logo from '../../assets/logo.svg'; -import typo from '../../assets/typo.svg'; -import React, { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import Input from '../Input/Input'; -import { - TopBarContainer, - TopBarLeft, - TopBarRight, - TopBarItem, - TopBarButton, - ImageWrapper, - LogoImage, - TypoImage, - UserImage, - UserImageWrapper, -} from './style'; -import ProfileChangeModalChildren from './components/ProfileChangeModalChildren'; -import useLoginState from '../../hooks/useLoginState'; -import useUserState from '../../hooks/useUserState'; -import DefaultProfile from '../../assets/default-profile.svg'; -import useContainer from '../../hooks/useContainer'; -import Button from '../Buttons'; -import useModal from '../../hooks/useModal'; -import memberIcon from '../../assets/member-icon.svg'; -import { useRef } from 'react'; -import { serverAPI } from '../../api/axios'; -import { Message } from '../Message'; - -const TopBarItems = [ - { - name: '문제 목록', - route: '/problem', - }, - { - name: '구성원', - route: '/member', - }, - { - name: '상점', - route: '/shop', - }, -]; - -const TopBar = ({ active }) => { - const [selectedItem, setSelectedItem] = useState(window.location.pathname); - const [isScroll, setIsScroll] = useState(false); - const { isLoggedIn, initLoginStatus } = useLoginState(); - const [shopUpdated, setShopUpdated] = useState(true); - const { user, setUserInfo } = useUserState(); - const navigate = useNavigate(); - const nextPasswordRef = useRef(); - const [selectedFile, setselectedFile] = useState(null); - const checkedNextPasswordRef = useRef(); - const [changePasswordFocus, setChangePasswordFocus] = useState(false); - const [checkedChangePasswordFocus, setCheckedChangePasswordFocus] = useState(false); - const [messageText, setMessageText] = useState(''); - - const userMenu = useContainer(); - const logoutModal = useModal({ - description: '정말 로그아웃하시겠어요?', - cancelText: '취소', - okText: '확인', - closable: true, - onOk: () => { - initLoginStatus(); - }, - }); - - const profileChangeModal = useModal({ - description: '변경할 이미지를 올려주세요', - cancelText: '취소', - okText: '확인', - closable: true, - onOk: async () => { - const formData = new FormData(); - formData.append('file', selectedFile); - try { - await serverAPI.post('/images/upload/profile', formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - - const response = await serverAPI.get('/user'); - setUserInfo(response.data.result); - setselectedFile(null); - } catch (error) { - console.log(error); - if (error.code === 'ERR_NETWORK') { - setMessageText('이미지 용량이 너무 커요!'); - passwordChangeMessage.toast(); - } - } - }, - }); - const changePasswordModal = useModal({ - cancelText: '취소', - closable: true, - onOk: () => { - const nextPassword = nextPasswordRef.current.getValue(); - const checkedPassword = checkedNextPasswordRef.current.getValue(); - //비밀번호 검증 - if (nextPassword.length <= 3) { - nextPasswordRef.current.focus(); - nextPasswordRef.current.setValue(''); - checkedNextPasswordRef.current.setValue(''); - nextPasswordRef.current.setPlaceholder('비밀번호는 4글자 이상이어야 합니다.'); - setChangePasswordFocus(true); - - return false; - } - if (nextPassword !== checkedPassword) { - checkedNextPasswordRef.current.focus(); - checkedNextPasswordRef.current.setValue(''); - checkedNextPasswordRef.current.setPlaceholder('비밀번호가 일치하지 않습니다.'); - setCheckedChangePasswordFocus(true); - return false; - } - serverAPI - .patch('/user/reset-password', { password: nextPasswordRef.current.getValue() }) - .then(response => { - setMessageText(response.data.result); - setChangePasswordFocus(false); - nextPasswordRef.current.setValue(''); - checkedNextPasswordRef.current.setValue(''); - nextPasswordRef.current.setPlaceholder('변경할 비밀번호'); - checkedNextPasswordRef.current.setPlaceholder('비밀번호 재입력'); - changePasswordModal.setIsPending(false); - passwordChangeMessage.toast(); - }) - .catch(error => { - if (error.data) { - setMessageText(error.data.result); - passwordChangeMessage.toast(); - } else { - console.log(error); - } - }); - }, - }); - const passwordChangeMessage = Message(); - - useEffect(() => { - const shopChecked = localStorage.getItem('shopChecked'); - if (shopChecked === null) { - // shopChecked 항목이 없으면 false로 초기화 - localStorage.setItem('shopChecked', 'false'); - setShopUpdated(true); - } - setShopUpdated(shopChecked === 'false'); - }, []); - - useEffect(() => { - const path = window.location.pathname; - if (path === '/') { - setSelectedItem('/'); - } else if (path === '/problem/' || path === '/problem') { - setSelectedItem('/problem'); - } else if (path === '/member/' || path === '/member') { - setSelectedItem('/member'); - } else if (path === '/shop/' || path === '/shop') { - setSelectedItem('/shop'); - } - }, []); - - useEffect(() => { - window.addEventListener('scroll', () => { - if (window.scrollY > 10) { - setIsScroll(true); - } else { - setIsScroll(false); - } - }); - }, []); - - function goRoute(route) { - if (route === selectedItem) return; - window.scrollTo(0, 0); - if (route === '/shop') { - localStorage.setItem('shopChecked', 'true'); - setShopUpdated(false); - } - navigate(route); - } - - const renderUserImage = () => { - return user ? ( - - {user.profileImageFileName ? ( - - ) : ( - - )} - - ) : ( - - - - ); - }; +import TopBarLeft from './components/TopBarLeft'; +import TopBarRight from './components/TopBarRight'; +import React from 'react'; +import S from './style'; +const TopBar = () => { return ( - - {passwordChangeMessage.render({ - children: ( -
✅   {messageText}
- ), - })} - {logoutModal.render()} - {profileChangeModal.render({ - children: ( - - ), - })} - {changePasswordModal.render({ - children: ( -
- - -
- ), - })} - navigate('/')}> - - - - - - - - - {TopBarItems.map((item, index) => ( - goRoute(item.route)} - > - {item.name} - {item.route === '/shop' && shopUpdated && ( - - )} - - ))} - {isLoggedIn ? ( - <> - {renderUserImage()} -
- {userMenu.render({ - children: ( - <> - - - - - ), - })} -
- - ) : ( - navigate('/login')}> - 로그인 - - )} -
-
+ + + + ); }; diff --git a/src/components/TopBar/style.js b/src/components/TopBar/style.js index 83558fc..4b29cc8 100644 --- a/src/components/TopBar/style.js +++ b/src/components/TopBar/style.js @@ -1,9 +1,9 @@ import styled, { css } from 'styled-components'; -const TopBarContainer = styled('div')` +const TopBarContainer = styled.div` z-index: 100; position: fixed; - top: 0px; + top: 0; left: 0; right: 0; display: flex; @@ -17,29 +17,17 @@ const TopBarContainer = styled('div')` -webkit-backdrop-filter: blur(8px); box-shadow: 0 4px 32px rgba(0, 0, 0, 0.05); - ${props => - props.isScroll && - css` - background-color: ${props => props.theme.foreground}18; - `} @media (max-width: 480px) { top: 0; margin: 0; padding: 8px 16px; - border-radius: 0px; + border-radius: 0; } `; - -const TopBarLeft = styled.div` - display: flex; - flex-direction: row; - gap: 10px; - cursor: pointer; -`; -const TopBarRight = styled.div` +const ImageWrapper = styled.div` display: flex; - flex-direction: row; - gap: 40px; + justify-content: center; + align-items: center; `; const TopBarItem = styled.div` @@ -68,7 +56,7 @@ const TopBarButton = styled.div` border-radius: 12px; background-color: ${props => props.theme.primary}; - color: #ffffff; + color: ${props => props.theme.white}; font-size: 12px; font-weight: 500; cursor: pointer; @@ -78,38 +66,9 @@ const TopBarButton = styled.div` } `; -const ImageWrapper = styled.div` - display: flex; - justify-content: center; - align-items: center; -`; - -const LogoImage = styled.img` - width: 32px; - height: 32px; -`; - -const TypoImage = styled.img` - width: 55px; - height: 32px; -`; - -const UserImage = styled.img` - width: 32px; - height: 32px; - border-radius: 32px; - cursor: pointer; -`; -const UserImageWrapper = styled.div``; -export { +export default { TopBarContainer, - TopBarLeft, - TopBarRight, + ImageWrapper, TopBarItem, TopBarButton, - ImageWrapper, - TypoImage, - LogoImage, - UserImage, - UserImageWrapper, }; diff --git a/src/components/TopBar/utils/checkPasswordValidate.js b/src/components/TopBar/utils/checkPasswordValidate.js new file mode 100644 index 0000000..29b4cad --- /dev/null +++ b/src/components/TopBar/utils/checkPasswordValidate.js @@ -0,0 +1,51 @@ +import { serverAPI } from '../../../api/axios'; +import MIN_PASSWORD_LENGTH from '../constants'; + +const resetInput = (ref, placeholderText) => { + ref.current.setValue(''); + ref.current.setPlaceholder(''); + ref.current.setPlaceholder(placeholderText); +}; +export const checkPasswordValidate = ( + nextPasswordRef, + checkedNextPasswordRef, + setChangePasswordFocus, + setCheckedChangePasswordFocus, + setMessageText, + changePasswordModal, + passwordChangeMessage +) => { + const nextPassword = nextPasswordRef.current.getValue(); + const checkedPassword = checkedNextPasswordRef.current.getValue(); + //비밀번호 검증 + if (nextPassword.length < MIN_PASSWORD_LENGTH) { + resetInput(nextPasswordRef, '비밀번호는 4글자 이상이어야 합니다.'); + checkedNextPasswordRef.current.setValue(''); + setChangePasswordFocus(true); + + return false; + } + if (nextPassword !== checkedPassword) { + resetInput(checkedNextPasswordRef, '비밀번호가 일치하지 않습니다.'); + setCheckedChangePasswordFocus(true); + return false; + } + serverAPI + .patch('/user/reset-password', { password: nextPasswordRef.current.getValue() }) + .then(response => { + setMessageText(response.data.result); + setChangePasswordFocus(false); + resetInput(nextPasswordRef, '비밀번호는 4글자 이상이어야 합니다.'); + resetInput(checkedNextPasswordRef, '비밀번호가 일치하지 않습니다.'); + changePasswordModal.setIsPending(false); + passwordChangeMessage.toast(); + }) + .catch(error => { + if (error.data) { + setMessageText(error.data.result); + passwordChangeMessage.toast(); + } else { + console.log(error); + } + }); +}; diff --git a/src/components/TopBar/utils/index.js b/src/components/TopBar/utils/index.js new file mode 100644 index 0000000..62b641d --- /dev/null +++ b/src/components/TopBar/utils/index.js @@ -0,0 +1,88 @@ +import { serverAPI } from '../../../api/axios'; +import { MIN_PASSWORD_LENGTH } from '../constants'; + +const resetInput = (ref, placeholderText) => { + ref.current.setValue(''); + ref.current.setPlaceholder(''); + ref.current.setPlaceholder(placeholderText); +}; +export const checkPasswordValidate = ( + nextPasswordRef, + checkedNextPasswordRef, + setChangePasswordFocus, + setCheckedChangePasswordFocus, + setMessageText, + changePasswordModal, + passwordChangeMessage +) => { + const nextPassword = nextPasswordRef.current.getValue(); + const checkedPassword = checkedNextPasswordRef.current.getValue(); + //비밀번호 검증 + if (nextPassword.length < MIN_PASSWORD_LENGTH) { + resetInput(nextPasswordRef, '비밀번호는 4글자 이상이어야 합니다.'); + checkedNextPasswordRef.current.setValue(''); + setChangePasswordFocus(true); + + return false; + } + if (nextPassword !== checkedPassword) { + resetInput(checkedNextPasswordRef, '비밀번호가 일치하지 않습니다.'); + setCheckedChangePasswordFocus(true); + return false; + } + serverAPI + .patch('/user/reset-password', { password: nextPasswordRef.current.getValue() }) + .then(response => { + setMessageText(response.data.result); + setChangePasswordFocus(false); + resetInput(nextPasswordRef, '비밀번호는 4글자 이상이어야 합니다.'); + resetInput(checkedNextPasswordRef, '비밀번호가 일치하지 않습니다.'); + changePasswordModal.setIsPending(false); + passwordChangeMessage.toast(); + }) + .catch(error => { + if (error.data) { + setMessageText(error.data.result); + passwordChangeMessage.toast(); + } else { + console.log(error); + } + }); +}; + +export const uploadImage = async ( + selectedFile, + setUserInfo, + setselectedFile, + setMessageText, + passwordChangeMessage +) => { + const formData = new FormData(); + formData.append('file', selectedFile); + try { + await serverAPI.post('/images/upload/profile', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + const response = await serverAPI.get('/user'); + setUserInfo(response.data.result); + setselectedFile(null); + } catch (error) { + console.log(error); + if (error.code === 'ERR_NETWORK') { + setMessageText('이미지 용량이 너무 커요!'); + passwordChangeMessage.toast(); + } + } +}; + +export function goRoute(route, selectedItem, navigate) { + if (route === selectedItem) return; + window.scrollTo(0, 0); + if (route === '/shop') { + localStorage.setItem('shopChecked', 'true'); + } + navigate(route); +} diff --git a/src/components/TopBar/utils/renderUserImage.js b/src/components/TopBar/utils/renderUserImage.js new file mode 100644 index 0000000..6e6344c --- /dev/null +++ b/src/components/TopBar/utils/renderUserImage.js @@ -0,0 +1,25 @@ +import styled from 'styled-components'; +import memberIcon from '../../../assets/member-icon.svg'; +import DefaultProfile from '../../../assets/default-profile.svg'; + +export const renderUserImage = (user, userMenu) => { + return user ? ( + user.profileImageFileName ? ( + + ) : ( + + ) + ) : ( + + ); +}; + +const UserImage = styled.img` + width: 32px; + height: 32px; + border-radius: 32px; + cursor: pointer; +`; diff --git a/src/components/TopBar/utils/uploadImage.js b/src/components/TopBar/utils/uploadImage.js new file mode 100644 index 0000000..d0cc7de --- /dev/null +++ b/src/components/TopBar/utils/uploadImage.js @@ -0,0 +1,28 @@ +import { serverAPI } from '../../../api/axios'; +export const uploadImage = async ( + selectedFile, + setUserInfo, + setselectedFile, + setMessageText, + passwordChangeMessage +) => { + const formData = new FormData(); + formData.append('file', selectedFile); + try { + await serverAPI.post('/images/upload/profile', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + const response = await serverAPI.get('/user'); + setUserInfo(response.data.result); + setselectedFile(null); + } catch (error) { + console.log(error); + if (error.code === 'ERR_NETWORK') { + setMessageText('이미지 용량이 너무 커요!'); + passwordChangeMessage.toast(); + } + } +}; diff --git a/src/constants/index.js b/src/constants/index.js new file mode 100644 index 0000000..63387d2 --- /dev/null +++ b/src/constants/index.js @@ -0,0 +1,5 @@ +export const PATH_NAME = { + PROBLEM_LIST: '/problem', + MEMBER: '/member', + SHOP: '/shop', +}; diff --git a/src/hooks/useContainer.js b/src/hooks/useContainer.js index 4ed3d1f..9de04e5 100644 --- a/src/hooks/useContainer.js +++ b/src/hooks/useContainer.js @@ -5,9 +5,8 @@ const useContainer = () => { const [isOpen, setIsOpen] = useState(false); const show = () => setIsOpen(true); const hide = () => setIsOpen(false); - const toggle = () => { - setIsOpen(prev => !prev); - }; + const toggle = () => setIsOpen(prev => !prev); + const render = ({ children = null } = {}) => { return {children}; };