diff --git a/apps/jurumarble/public/images/VoteOnboardD01.png b/apps/jurumarble/public/images/VoteOnboardD01.png new file mode 100644 index 00000000..d0f963bd Binary files /dev/null and b/apps/jurumarble/public/images/VoteOnboardD01.png differ diff --git a/apps/jurumarble/public/images/VoteOnboardD02.png b/apps/jurumarble/public/images/VoteOnboardD02.png new file mode 100644 index 00000000..b95490d0 Binary files /dev/null and b/apps/jurumarble/public/images/VoteOnboardD02.png differ diff --git a/apps/jurumarble/public/images/VoteOnboardD03.png b/apps/jurumarble/public/images/VoteOnboardD03.png new file mode 100644 index 00000000..b722f774 Binary files /dev/null and b/apps/jurumarble/public/images/VoteOnboardD03.png differ diff --git a/apps/jurumarble/public/images/VoteOnboardD04.png b/apps/jurumarble/public/images/VoteOnboardD04.png new file mode 100644 index 00000000..6186daaf Binary files /dev/null and b/apps/jurumarble/public/images/VoteOnboardD04.png differ diff --git a/apps/jurumarble/public/images/VoteOnboardM01.png b/apps/jurumarble/public/images/VoteOnboardM01.png new file mode 100644 index 00000000..129ad7b0 Binary files /dev/null and b/apps/jurumarble/public/images/VoteOnboardM01.png differ diff --git a/apps/jurumarble/public/images/VoteOnboardM02.png b/apps/jurumarble/public/images/VoteOnboardM02.png new file mode 100644 index 00000000..012290a2 Binary files /dev/null and b/apps/jurumarble/public/images/VoteOnboardM02.png differ diff --git a/apps/jurumarble/public/images/VoteOnboardM03.png b/apps/jurumarble/public/images/VoteOnboardM03.png new file mode 100644 index 00000000..37a4cd0d Binary files /dev/null and b/apps/jurumarble/public/images/VoteOnboardM03.png differ diff --git a/apps/jurumarble/public/images/VoteOnboardM04.png b/apps/jurumarble/public/images/VoteOnboardM04.png new file mode 100644 index 00000000..fb185a33 Binary files /dev/null and b/apps/jurumarble/public/images/VoteOnboardM04.png differ diff --git a/apps/jurumarble/public/images/index.ts b/apps/jurumarble/public/images/index.ts index 8aa56547..3adb6739 100644 --- a/apps/jurumarble/public/images/index.ts +++ b/apps/jurumarble/public/images/index.ts @@ -10,3 +10,12 @@ export { default as DrinkCapacityMedium } from './DrinkCapacityMedium.png'; export { default as DrinkCapacityHigh } from './DrinkCapacityHigh.png'; export { default as ImgScroll } from './ImgScroll.png'; export { default as restaurantImg } from './restaurantImg.png'; +export { default as Onboarding } from './onboarding.png'; +export { default as DesktopOnboarding1 } from './VoteOnboardD01.png'; +export { default as DesktopOnboarding2 } from './VoteOnboardD02.png'; +export { default as DesktopOnboarding3 } from './VoteOnboardD03.png'; +export { default as DesktopOnboarding4 } from './VoteOnboardD04.png'; +export { default as MobileOnboarding1 } from './VoteOnboardM01.png'; +export { default as MobileOnboarding2 } from './VoteOnboardM02.png'; +export { default as MobileOnboarding3 } from './VoteOnboardM03.png'; +export { default as MobileOnboarding4 } from './VoteOnboardM04.png'; diff --git a/apps/jurumarble/public/images/onboarding.png b/apps/jurumarble/public/images/onboarding.png new file mode 100644 index 00000000..d2a031d1 Binary files /dev/null and b/apps/jurumarble/public/images/onboarding.png differ diff --git a/apps/jurumarble/src/app/main/components/Banner.tsx b/apps/jurumarble/src/app/main/components/Banner.tsx index 291d92de..7c25e940 100644 --- a/apps/jurumarble/src/app/main/components/Banner.tsx +++ b/apps/jurumarble/src/app/main/components/Banner.tsx @@ -1,10 +1,23 @@ 'use client'; +import { useEffect } from 'react'; + +import Path from 'lib/Path'; +import userStorage from 'lib/utils/userStorage'; import Image from 'next/image'; +import { useRouter } from 'next/navigation'; import { MainBannerImage } from 'public/images'; import styled, { css } from 'styled-components'; function Banner() { + const router = useRouter(); + useEffect(() => { + if (!userStorage.get() || !!localStorage.getItem('visited_home')) { + return; + } + router.push(Path.ONBOARDING_PAGE); + localStorage.setItem('visited_home', 'false'); + }, []); return ( { + const router = useRouter(); + const cx = getClassNames(styles); + return ( +
+ +
+ ); +}; + +export default BottomButton; diff --git a/apps/jurumarble/src/app/onboarding/page.module.css b/apps/jurumarble/src/app/onboarding/page.module.css new file mode 100644 index 00000000..7056208f --- /dev/null +++ b/apps/jurumarble/src/app/onboarding/page.module.css @@ -0,0 +1,28 @@ +.container { + width: 100%; + display: flex; + justify-content: center; + /* background-color: var(--bg_01); */ + padding-bottom: 96px; +} + +.container img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.img-wrapper { + max-width: 720px; + margin: 0 auto; + background-color: var(--white); +} + +.bottom-wrapper { + position: fixed; + bottom: 0; + padding: 20px; + width: 100%; + max-width: 480px; + background-color: var(--white); +} diff --git a/apps/jurumarble/src/app/onboarding/page.tsx b/apps/jurumarble/src/app/onboarding/page.tsx new file mode 100644 index 00000000..54abaee3 --- /dev/null +++ b/apps/jurumarble/src/app/onboarding/page.tsx @@ -0,0 +1,21 @@ +import { getClassNames } from 'lib/styles/getClassNames'; +import Image from 'next/image'; +import { Onboarding } from 'public/images'; + +import BottomButton from './components/BottomButton'; +import styles from './page.module.css'; + +const OnboardingPage = () => { + const cx = getClassNames(styles); + + return ( +
+
+ 온보딩 +
+ +
+ ); +}; + +export default OnboardingPage; diff --git a/apps/jurumarble/src/app/vote/components/OnboardingBottomsheet.tsx b/apps/jurumarble/src/app/vote/components/OnboardingBottomsheet.tsx new file mode 100644 index 00000000..088665e5 --- /dev/null +++ b/apps/jurumarble/src/app/vote/components/OnboardingBottomsheet.tsx @@ -0,0 +1,276 @@ +import { forwardRef, useRef, useState } from 'react'; + +import { Portal } from 'components/index'; +import { transitions } from 'lib/styles'; +import Image, { StaticImageData } from 'next/image'; +import { + DesktopOnboarding1, + DesktopOnboarding2, + DesktopOnboarding3, + DesktopOnboarding4, + MobileOnboarding1, + MobileOnboarding2, + MobileOnboarding3, +} from 'public/images'; +import { SvgIcX } from 'src/assets/icons/components'; +import styled, { css } from 'styled-components'; + +interface CardProps { + title: string; + description: string; + imgSrc: string | StaticImageData; +} + +const Card = forwardRef( + ({ title, description, imgSrc }, ref) => { + return ( + +
{title}
+
{description}
+ img +
+ ); + }, +); + +interface Props { + onToggleOnboarding: () => void; +} + +const TAB_LIST = [ + { tabName: '후보 확대', id: 'enlarge' }, + { tabName: '후보 선택', id: 'select' }, + { tabName: '투표 이동', id: 'move' }, + { tabName: '자세히 보기', id: 'detail' }, +]; + +const CARD_LIST = [ + { + title: '후보를 확대해서 보기', + description: '마우스를 후보에 올리거나 좌우 방향키를 이용하세요.', + imgSrc: DesktopOnboarding1, + mobileImgSrc: MobileOnboarding1, + }, + { + title: '투표 후보를 선택하기', + description: '원하는 후보를 클릭하세요.', + imgSrc: DesktopOnboarding2, + mobileImgSrc: MobileOnboarding2, + }, + { + title: '자세한 내용을 확인하기', + description: '스크롤을 하거나 상하 방향키를 이용하세요.', + imgSrc: DesktopOnboarding3, + mobileImgSrc: MobileOnboarding3, + }, + { + title: '자세히 보기', + description: '더보기 버튼을 클릭해주세요.', + imgSrc: DesktopOnboarding4, + mobileImgSrc: MobileOnboarding1, + }, +]; + +const OnboardingBottomsheet = ({ onToggleOnboarding }: Props) => { + const [chip, setChip] = useState('mobile'); + + const card1Ref = useRef(null); + const card2Ref = useRef(null); + const card3Ref = useRef(null); + const card4Ref = useRef(null); + + const cardRefs = [card1Ref, card2Ref, card3Ref, card4Ref]; + + // 탭 클릭시 ref로 이동하는 함수 + + const handleTabClick = (index: number) => { + cardRefs[index]?.current?.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + inline: 'center', + }); + }; + + return ( + + + + + + + 투표를 좀 더 재밌게 참여해 볼까요? + + 투표는 여러가지 조작 방법을 통해 참여할 수 있어요. + + + {TAB_LIST.map(({ id, tabName }, index) => ( + handleTabClick(index)} + > + {tabName} + + ))} + + + setChip('mobile')}> + 모바일 + + setChip('desktop')} + > + PC + + + {CARD_LIST.map( + ({ title, description, imgSrc, mobileImgSrc }, index) => ( + + ), + )} + + + + + ); +}; + +const BottomSheet = styled.div` + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 9999; +`; + +const Inner = styled.div` + position: absolute; + z-index: 9999; + background-color: white; + bottom: 0; + right: 0; + left: 0; + margin: auto; + width: 100%; + max-width: 720px; + height: 90%; + animation: ${transitions.popInFromBottom} 0.4s ease-in-out; + border-radius: 16px 16px 0px 0px; + overflow-y: scroll; + &::-webkit-scrollbar { + display: none; + } +`; + +const Background = styled.div` + display: block; + width: 100%; + height: 100%; + background-color: black; + position: absolute; + left: 0; + top: 0; + opacity: 0.4; +`; + +const Title = styled.div` + display: flex; + ${({ theme }) => css` + ${theme.typography.headline02} + `} + justify-content: flex-start; + padding: 64px 20px 0 20px; +`; + +const Description = styled.div` + display: flex; + ${({ theme }) => css` + ${theme.typography.body02} + color: ${theme.colors.black_02}; + `} + padding:12px 20px 16px 20px; +`; +// padding: 26px 20px 20px 20px; +const Exit = styled.div` + position: absolute; + top: 26px; + right: 20px; + width: 24px; + height: 24px; + cursor: pointer; +`; + +const TabWrapper = styled.div` + display: flex; + + padding: 16px 20px 0 20px; + border-bottom: 1px solid ${({ theme }) => theme.colors.line_01}; +`; + +const Tab = styled.div<{ active: boolean }>` + padding: 16px 10px; + width: 25%; + text-align: center; + cursor: pointer; + ${({ active, theme }) => + active + ? css` + ${theme.typography.body01} + color: ${({ theme }) => theme.colors.black_01}; + border-bottom: 3px solid ${({ theme }) => theme.colors.black_01}; + ` + : css` + ${theme.typography.body02} + color: ${({ theme }) => theme.colors.black_03}; + `} +`; + +const ChipWrapper = styled.div` + padding: 28px 20px; + display: flex; + gap: 4px; +`; + +const Chip = styled.div<{ active: boolean }>` + padding: 10px; + border-radius: 4px; + cursor: pointer; + ${({ theme, active }) => css` + ${theme.typography.caption_chip} + color: ${active ? theme.colors.white : theme.colors.black_02}; + background-color: ${active ? theme.colors.black_02 : theme.colors.bg_01}; + `} +`; + +const CardWrapper = styled.div` + padding: 0 20px; + margin-bottom: 42px; + .title { + padding-bottom: 4px; + ${({ theme }) => css` + ${theme.typography.body01} + `} + } + .description { + ${({ theme }) => css` + ${theme.typography.body_long03} + padding-bottom: 16px; + `} + } + + .img { + width: 100%; + height: 100%; + border-radius: 16px; + } +`; + +export default OnboardingBottomsheet; diff --git a/apps/jurumarble/src/app/vote/page.tsx b/apps/jurumarble/src/app/vote/page.tsx index 7b143bdf..583f4fba 100644 --- a/apps/jurumarble/src/app/vote/page.tsx +++ b/apps/jurumarble/src/app/vote/page.tsx @@ -10,6 +10,7 @@ import { Button } from 'components/button'; import Path from 'lib/Path'; import { media } from 'lib/styles'; import { isLogin } from 'lib/utils/auth'; +import userStorage from 'lib/utils/userStorage'; import Image from 'next/image'; import { useRouter, useSearchParams } from 'next/navigation'; import { ImgScroll } from 'public/images'; @@ -22,12 +23,24 @@ import ChipContainer from './[id]/components/ChipContainer'; import VoteDescription from './[id]/components/VoteDescription'; import useExecuteVoteService from './[id]/services/useExecuteVoteService'; import useFilteredStatisticsService from './[id]/services/useFilterStatisticsService'; +import OnboardingBottomsheet from './components/OnboardingBottomsheet'; import useFlipAnimation from './hooks/useFlipAnimation'; import useInfiniteMainListService from './services/useGetVoteListService'; export type Drag = 'up' | 'down' | null; function VoteHomePage() { + const router = useRouter(); + const [isOnboarding, onToggleOnboarding] = useToggle(); + + useEffect(() => { + if (!userStorage.get() || !!localStorage.getItem('visited_vote')) { + return; + } + onToggleOnboarding(); + localStorage.setItem('visited_home', 'true'); + }, []); + const searchParams = useSearchParams(); const params = new URLSearchParams(searchParams); /** @@ -38,8 +51,6 @@ function VoteHomePage() { toastId: 'voteSuccess', }); - const router = useRouter(); - const { isError, isLoading, mainVoteList, nowShowing, onChangeNowShowing } = useInfiniteMainListService({ size: 10, @@ -203,6 +214,9 @@ function VoteHomePage() { onToggleReplaceLoginPageModal={onToggleReplaceLoginPageModal} /> )} + {isOnboarding && ( + + )} ); } diff --git a/apps/jurumarble/src/lib/Path.ts b/apps/jurumarble/src/lib/Path.ts index deeecb82..ed1357ab 100644 --- a/apps/jurumarble/src/lib/Path.ts +++ b/apps/jurumarble/src/lib/Path.ts @@ -13,6 +13,7 @@ const Path = { MY_PAGE: '/my', NAVER_LOGIN_PROCESS: '/login/naver-login-process', NOTIFICATION_PAGE: '/notification', + ONBOARDING_PAGE: '/onboarding', POST_PAGE: '/vote/post', PROFILE_EDIT: '/my/edit',