From 4a244eb95d4bad1ab0252f9cdf12e04bc0c2ee58 Mon Sep 17 00:00:00 2001 From: Jaewoong Hwang <95916813+w00ngja@users.noreply.github.com> Date: Wed, 17 Jan 2024 01:02:00 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20=EB=B0=98=EC=9D=91=ED=98=95=20=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: 헤더 버튼 selectedDot 위치 문제 해결 * Feat: 헤더 반응형 로직 구현 * Feat: 검색창 컴포넌트 반응형 개선 * Feat: 모바일 환경 헤더 다크모드 버튼 추가 --------- Co-authored-by: Cho-Ik-Jun --- .../Common/DropDownOnlyOption/index.tsx | 2 + .../Common/DropDownOnlyOption/style.ts | 3 +- .../Common/DropDownOnlyOption/type.ts | 1 + .../Common/Header/Hamburger/index.tsx | 167 ++++++++++++++++++ .../Common/Header/Hamburger/style.ts | 10 ++ .../Common/Header/HeaderLogo/style.ts | 8 + .../Common/Header/HeaderTab/index.tsx | 2 +- .../Common/Header/HeaderTab/style.ts | 5 - src/Components/Common/Header/index.tsx | 7 +- src/Components/Common/Header/style.ts | 4 + src/Components/SearchModal/index.tsx | 23 ++- src/Components/SearchModal/style.ts | 12 ++ src/Styles/Animation.ts | 13 ++ 13 files changed, 246 insertions(+), 11 deletions(-) create mode 100644 src/Components/Common/Header/Hamburger/index.tsx create mode 100644 src/Components/Common/Header/Hamburger/style.ts diff --git a/src/Components/Common/DropDownOnlyOption/index.tsx b/src/Components/Common/DropDownOnlyOption/index.tsx index ad35c127..2d6b54fe 100644 --- a/src/Components/Common/DropDownOnlyOption/index.tsx +++ b/src/Components/Common/DropDownOnlyOption/index.tsx @@ -36,6 +36,7 @@ const DropDownOnlyOption = forwardRef( labelProps, isShow = true, initialValue, + inset, ...props }: DropDownProps, ref: ForwardedRef, @@ -51,6 +52,7 @@ const DropDownOnlyOption = forwardRef( return ( {label && ( diff --git a/src/Components/Common/DropDownOnlyOption/style.ts b/src/Components/Common/DropDownOnlyOption/style.ts index 5be19d19..f2494bc4 100644 --- a/src/Components/Common/DropDownOnlyOption/style.ts +++ b/src/Components/Common/DropDownOnlyOption/style.ts @@ -5,8 +5,9 @@ import { StyledLabelProp, } from './type'; -export const StyledDropDown = styled.div` +export const StyledDropDown = styled.div<{ $inset?: string }>` position: absolute; + inset: ${({ $inset }) => $inset}; `; export const StyledLabel = styled.span` diff --git a/src/Components/Common/DropDownOnlyOption/type.ts b/src/Components/Common/DropDownOnlyOption/type.ts index 0fbeb9ea..11e8a338 100644 --- a/src/Components/Common/DropDownOnlyOption/type.ts +++ b/src/Components/Common/DropDownOnlyOption/type.ts @@ -17,6 +17,7 @@ export interface DropDownProps { isShow?: boolean; initialValue?: string; onSelect?: (selected: string) => void; + inset?: string; buttonProps?: HTMLAttributes; optionProps?: HTMLAttributes; diff --git a/src/Components/Common/Header/Hamburger/index.tsx b/src/Components/Common/Header/Hamburger/index.tsx new file mode 100644 index 00000000..273705bd --- /dev/null +++ b/src/Components/Common/Header/Hamburger/index.tsx @@ -0,0 +1,167 @@ +import { useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useTheme } from 'styled-components'; +import StyledWrapper from './style'; + +import DropDownOnlyOption from '../../DropDownOnlyOption'; +import useTabStore from '@/Stores/Tab'; +import useClickAway from '@/Hooks/UseClickAway'; +import ModalButton from '../HeaderTab/ModalButton'; +import { checkAuth, logout } from '@/Services/Auth'; +import NotificationModal from '@/Components/NotificationModal'; +import { useDarkModeStore } from '@/Stores'; + +const Hamburger = () => { + const location = useLocation(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const { colors } = useTheme(); + + const [isDrop, setIsDrop] = useState(false); + const { tab, prev, setTab, setPrev } = useTabStore(); + const [alarm, setAlarm] = useState(false); + + const { isDarkMode, toggleDarkMode } = useDarkModeStore(); + const isAuthUser = !!sessionStorage.getItem('AUTH_TOKEN'); + const { data } = useQuery({ + queryKey: ['auth'], + queryFn: checkAuth, + }); + + useEffect(() => { + if ( + tab === 'home' || + tab === 'message' || + (tab === 'account' && location.pathname.includes('/profile')) + ) { + setPrev(tab); + sessionStorage.setItem('prev', prev); + } + sessionStorage.setItem('tab', tab); + }, [tab, prev, setPrev, location]); + + const styledNavIcon = { + fontSize: '3.2rem', + color: colors.text, + fontWeight: '300', + }; + + const { mutate: mutateLogout } = useMutation({ + mutationFn: logout, + onSuccess: async () => { + // 로그아웃 성공 시 + queryClient.setQueryData(['auth'], null); + sessionStorage.removeItem('AUTH_TOKEN'); + navigate('/'); + setTab('home'); + }, + }); + + const options: string[] = isAuthUser + ? [ + '홈', + '포스트 작성', + '검색', + '알림', + '메시지', + '마이페이지', + '비밀번호 변경', + '로그아웃', + isDarkMode ? '🌞 라이트모드' : '🌝 다크모드', + ] + : ['홈', '포스트 작성', '검색', '로그인', '다크모드']; + + const onSelectOption = (option: string) => { + setIsDrop(!isDrop); + + switch (option) { + case '홈': + navigate('/'); + break; + case '포스트 작성': + navigate( + `${location.pathname !== '/' ? location.pathname : ''}/add-post`, + ); + break; + case '검색': + navigate( + `${location.pathname !== '/' ? location.pathname : ''}/search`, + ); + break; + case '알림': + setAlarm((prevIsShow) => !prevIsShow); + break; + case '메시지': + navigate('/directmessage'); + break; + case '로그인': + navigate('/login'); + break; + case '마이페이지': + // eslint-disable-next-line no-underscore-dangle + navigate(`/profile/${data?._id}`); + setPrev('account'); + break; + case '비밀번호 변경': + navigate( + `${ + location.pathname !== '/' ? location.pathname : '' + // eslint-disable-next-line no-underscore-dangle + }/edit-password/${data?._id}`, + ); + break; + case '로그아웃': + mutateLogout(); + break; + case '🌞 라이트모드': + toggleDarkMode(); + break; + case '🌝 다크모드': + toggleDarkMode(); + break; + default: + break; + } + }; + + const ref = useClickAway((e) => { + setIsDrop(false); + setTab(prev); + }); + + return ( + <> + {' '} + + setIsDrop(!isDrop)} + /> + { + onSelectOption(option); + }} + inset="5rem 0rem 0rem -12rem" + /> + + {alarm && ( + { + setAlarm(false); + setTab(prev); + }} + /> + )} + + ); +}; + +export default Hamburger; diff --git a/src/Components/Common/Header/Hamburger/style.ts b/src/Components/Common/Header/Hamburger/style.ts new file mode 100644 index 00000000..88a6cf0b --- /dev/null +++ b/src/Components/Common/Header/Hamburger/style.ts @@ -0,0 +1,10 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + position: relative; +`; + +export default StyledWrapper; diff --git a/src/Components/Common/Header/HeaderLogo/style.ts b/src/Components/Common/Header/HeaderLogo/style.ts index c5356550..8367427e 100644 --- a/src/Components/Common/Header/HeaderLogo/style.ts +++ b/src/Components/Common/Header/HeaderLogo/style.ts @@ -1,4 +1,5 @@ import { styled } from 'styled-components'; +import { floatSmall } from '@/Styles/Animation'; export const StyledLogo = styled.img` width: 20rem; @@ -9,6 +10,13 @@ export const StyledLogo = styled.img` &:hover { transform: scale(1.1); } + + @media ${({ theme }) => theme.device.tablet} { + &:hover { + transform: none; + } + animation: ${floatSmall} 3s ease-in-out infinite; + } `; export const StyledContainer = styled.div` diff --git a/src/Components/Common/Header/HeaderTab/index.tsx b/src/Components/Common/Header/HeaderTab/index.tsx index eb6e745b..16fcea65 100644 --- a/src/Components/Common/Header/HeaderTab/index.tsx +++ b/src/Components/Common/Header/HeaderTab/index.tsx @@ -250,7 +250,7 @@ const HeaderTab = () => { onSelect={(option) => { onSelectOption(option); }} - // style={{ right: '16rem', top: '5rem' }} + inset="5rem 0rem 0rem -11rem" /> diff --git a/src/Components/Common/Header/HeaderTab/style.ts b/src/Components/Common/Header/HeaderTab/style.ts index 80ee7a5e..1cbeb0d7 100644 --- a/src/Components/Common/Header/HeaderTab/style.ts +++ b/src/Components/Common/Header/HeaderTab/style.ts @@ -28,11 +28,6 @@ export const StyledButtonContainer = styled.div` transition: transform 0.3s ease-in-out; } } - - > :nth-child(2) { - right: 16rem; - top: 5rem; - } `; export const StyledFocusedCircle = styled.div<{ $visible: boolean }>` diff --git a/src/Components/Common/Header/index.tsx b/src/Components/Common/Header/index.tsx index 63fc78fd..16bfb25f 100644 --- a/src/Components/Common/Header/index.tsx +++ b/src/Components/Common/Header/index.tsx @@ -4,9 +4,13 @@ import { StyledHeaderContainer, StyledDivider } from './style'; import HeaderTab from './HeaderTab'; import HeaderLogo from './HeaderLogo'; import HeaderProps from './type'; +import useResize from '@/Hooks/useResize'; +import Hamburger from './Hamburger'; // import useTabStore from '@/Stores/Tab'; const Header = ({ activeHeader }: HeaderProps) => { + const { isMobileSize } = useResize(); + // 뒤로가기 핸들러.. /** const { prev, setTab } = useTabStore(); @@ -33,7 +37,8 @@ const Header = ({ activeHeader }: HeaderProps) => { - + + {isMobileSize ? : } ); }; diff --git a/src/Components/Common/Header/style.ts b/src/Components/Common/Header/style.ts index 0299132f..dd2f915a 100644 --- a/src/Components/Common/Header/style.ts +++ b/src/Components/Common/Header/style.ts @@ -10,6 +10,10 @@ export const StyledHeaderContainer = styled.div` display: flex; position: fixed; z-index: 9; + + @media ${({ theme }) => theme.device.tablet} { + padding: 2rem 2rem; + } `; export const StyledDivider = styled.div` diff --git a/src/Components/SearchModal/index.tsx b/src/Components/SearchModal/index.tsx index 17a57daf..a148bca0 100644 --- a/src/Components/SearchModal/index.tsx +++ b/src/Components/SearchModal/index.tsx @@ -14,11 +14,14 @@ import SearchPostList from './SearchPostList'; import { Props } from './type'; import { StyledBody, + StyledButton, StyledHeader, StyledHeaderTab, StyledHeaderTitle, StyledWrapper, } from './style'; +import useResize from '@/Hooks/useResize'; +import Icon from '../Base/Icon'; // TODO: SearchPostList, SearchUserList 컴포넌트 통합 // TODO: 검색 결과 상세화 @@ -30,6 +33,8 @@ const SearchModal = ({ onChangeOpen }: Props) => { const [userResult, setUserResult] = useState(null); const [postResult, setPostResult] = useState(null); + const { isMobileSize } = useResize(); + /** * @brief 사용자가 입력한 검색어를 담아 요청을 보냅니다. 응답 데이터는 유저와 포스트 결과를 담고 있는 배열 형태이며, 이를 순회하며 유저 데이터와 포스트 데이터를 분리합니다. 이후 분리한 데이터를 하위 컴포넌트에게 전달하여 검색 결과를 화면에 표시합니다. */ @@ -81,12 +86,24 @@ const SearchModal = ({ onChangeOpen }: Props) => { /> )} + {isMobileSize && ( + onChangeOpen(false)} + hoverBackgroundColor="tranparent" + > + + + )} {searchQuery ? `"${searchQuery}" 검색 결과` : '검색'} diff --git a/src/Components/SearchModal/style.ts b/src/Components/SearchModal/style.ts index a6702652..ad9e8233 100644 --- a/src/Components/SearchModal/style.ts +++ b/src/Components/SearchModal/style.ts @@ -1,4 +1,5 @@ import styled from 'styled-components'; +import Button from '../Base/Button'; export const StyledWrapper = styled.div` width: 100%; @@ -7,11 +8,18 @@ export const StyledWrapper = styled.div` display: flex; flex-direction: column; + position: relative; justify-content: start; align-items: center; `; +export const StyledButton = styled(Button)` + position: absolute; + top: 2rem; + right: 3rem; +`; + export const StyledHeader = styled.header` width: 100%; height: auto; @@ -23,6 +31,10 @@ export const StyledHeader = styled.header` margin-bottom: 2rem; cursor: default; + + @media ${({ theme }) => theme.device.tablet} { + padding: 2rem; + } `; export const StyledHeaderTitle = styled.h1` diff --git a/src/Styles/Animation.ts b/src/Styles/Animation.ts index de5ee6a2..c6b11c36 100644 --- a/src/Styles/Animation.ts +++ b/src/Styles/Animation.ts @@ -92,3 +92,16 @@ export const slideOut = keyframes` opacity: 0; } `; + +export const floatSmall = keyframes` + 0% { + transform: translateY(0rem); + } + 50% { + transform: translateY(-0.6rem); + } + + 100% { + transform: translateY(0rem); + } +`;