diff --git a/apps/jurumarble/src/app/main/components/Banner.tsx b/apps/jurumarble/src/app/main/components/Banner.tsx new file mode 100644 index 00000000..2864e860 --- /dev/null +++ b/apps/jurumarble/src/app/main/components/Banner.tsx @@ -0,0 +1,46 @@ +import styled, { css } from "styled-components"; +import Image from "next/image"; +import { MainBannerImage } from "public/images"; + +function Banner() { + return ( + + 배너 + + 여행의 즐거움을 우리술과 함께 레벨업! + + 여행지에서 우리술이 고민된다면
주루마블에서 해결해보세요 +
+
+
+ ); +} + +const Container = styled.div` + position: relative; + margin-top: 12px; + aspect-ratio: 16 / 9; +`; + +const BannerText = styled.div` + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + padding: 20px; +`; + +const MainText = styled.div` + ${({ theme }) => css` + ${theme.typography.headline03} + `}; +`; + +const SubText = styled.div` + ${({ theme }) => css` + ${theme.typography.body_long03} + margin-top: 8px; + `}; +`; +export default Banner; diff --git a/apps/jurumarble/src/app/main/components/Carousel.tsx b/apps/jurumarble/src/app/main/components/Carousel.tsx index db0286e0..c9350f90 100644 --- a/apps/jurumarble/src/app/main/components/Carousel.tsx +++ b/apps/jurumarble/src/app/main/components/Carousel.tsx @@ -3,6 +3,12 @@ import Path from "lib/Path"; import Image from "next/image"; import { useRouter } from "next/navigation"; import styled, { css } from "styled-components"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { SvgIcPrevious, SvgNext } from "src/assets/icons/components"; + +const SLIDE_MOVE_COUNT = 1; +const ORIGINAL_IMAGE_LENGTH = 10; +const MOVE_DISTANCE = 300; interface Props { hotDrinkList: GetHotDrinkResponse[]; @@ -10,42 +16,112 @@ interface Props { function Carousel({ hotDrinkList }: Props) { const router = useRouter(); + const slideRef = useRef(null); + const [currentSlide, setCurrentSlide] = useState(1); + const [isAnimation, setIsAnimation] = useState(true); + const [isFlowing, setIsFlowing] = useState(true); + + const onNextSlide = useCallback(() => { + setCurrentSlide((prev) => prev + SLIDE_MOVE_COUNT); + }, [currentSlide]); + + const onPrevSlide = useCallback(() => { + setCurrentSlide((prev) => prev - SLIDE_MOVE_COUNT); + }, [currentSlide]); + + useEffect(() => { + if (!slideRef.current) return; + + if (currentSlide === ORIGINAL_IMAGE_LENGTH + 1) { + setTimeout(() => { + setIsAnimation(false); + slideRef.current!.style.transform = `translateX(-${ + MOVE_DISTANCE * ORIGINAL_IMAGE_LENGTH + }px)`; + setCurrentSlide(1); + }, 500); + + setTimeout(() => { + setIsAnimation(true); + }, 600); + } else if (currentSlide === 0) { + setTimeout(() => { + setIsAnimation(false); + slideRef.current!.style.transform = `translateX(+${MOVE_DISTANCE}px)`; + setCurrentSlide(ORIGINAL_IMAGE_LENGTH); + }, 500); + + setTimeout(() => { + setIsAnimation(true); + }, 600); + } + slideRef.current.style.transform = `translateX(-${MOVE_DISTANCE * (currentSlide - 1)}px)`; + }, [currentSlide]); + + useEffect(() => { + let intervalId: NodeJS.Timeout; + if (isFlowing) { + intervalId = setInterval(() => { + setCurrentSlide((prev) => prev + SLIDE_MOVE_COUNT); + }, 3500); + } + return () => clearTimeout(intervalId); + }, [isFlowing, currentSlide]); + return ( - - - {hotDrinkList.map((hotDrink: GetHotDrinkResponse, index: number) => { - const { drinkId, image, name, manufactureAddress } = hotDrink; - return ( - router.push(`${Path.DRINK_INFO_PAGE}/${drinkId}`)}> - - - {index + 1} - 전통주 - - - {name} - {manufactureAddress} - - - - ); - })} - - + <> + + + {hotDrinkList.map((hotDrink: GetHotDrinkResponse, index: number) => { + const { drinkId, image, name, manufactureAddress } = hotDrink; + return ( + router.push(`${Path.DRINK_INFO_PAGE}/${drinkId}`)} + > + + + {index + 1} + 전통주 + + + {name} + {manufactureAddress} + + + + ); + })} + + + + + + + + + + + + + + ); } const Container = styled.div` - height: 188px; margin-top: 32px; + overflow: hidden; `; -const Slides = styled.ol` +const Slides = styled.ol<{ isAnimation: boolean }>` display: flex; - height: 168px; - overflow-x: auto; - scroll-snap-type: x mandatory; + /* overflow-x: auto; + scroll-snap-type: x mandatory; */ gap: 8px; + transition: transform 0.5s ease-in-out; + ${({ isAnimation }) => isAnimation && `transform: translateX(-${MOVE_DISTANCE}px);`} + /** @Todo 모바일에서는 보이게 하기 **/ @@ -60,7 +136,7 @@ const Slide = styled.li` width: 292px; height: 120px; padding-top: 20px; - scroll-snap-align: start; + /* scroll-snap-align: start; */ cursor: pointer; `; @@ -120,4 +196,31 @@ const AreaName = styled.span` `} `; +const CarouselControlContainer = styled.div` + margin-top: 28px; + display: flex; + align-items: center; + gap: 20px; +`; + +const DivideLine = styled.div` + ${({ theme }) => css` + background-color: ${theme.colors.line_01}; + height: 2px; + width: 100%; + `} +`; + +const SlideButtonContainer = styled.div` + display: flex; + gap: 10px; +`; + +const SlideButton = styled.button` + border-radius: 100px; + border: 1px solid ${({ theme }) => theme.colors.line_01}; + width: 40px; + height: 40px; +`; + export default Carousel; diff --git a/apps/jurumarble/src/app/main/components/HotDrinkVoteContainer.tsx b/apps/jurumarble/src/app/main/components/HotDrinkVoteContainer.tsx index 5935a8ab..790b540b 100644 --- a/apps/jurumarble/src/app/main/components/HotDrinkVoteContainer.tsx +++ b/apps/jurumarble/src/app/main/components/HotDrinkVoteContainer.tsx @@ -5,7 +5,9 @@ import styled, { css } from "styled-components"; import useGetHotDrinkVoteService from "../services/useGetHotDrinkVoteService"; function HotDrinkVoteContainer() { - const { data: hotDrinkVote } = useGetHotDrinkVoteService(); + const router = useRouter(); + + const { hotDrinkVote } = useGetHotDrinkVoteService(); if (!hotDrinkVote) { return null; } @@ -13,8 +15,6 @@ function HotDrinkVoteContainer() { const nowTime = new Date().getHours(); - const router = useRouter(); - return ( <>

diff --git a/apps/jurumarble/src/app/main/components/TodayDrinkRecommendation.tsx b/apps/jurumarble/src/app/main/components/TodayDrinkRecommendation.tsx new file mode 100644 index 00000000..b35cf607 --- /dev/null +++ b/apps/jurumarble/src/app/main/components/TodayDrinkRecommendation.tsx @@ -0,0 +1,101 @@ +import { useEffect, useRef, useState } from "react"; +import { SvgStamp } from "src/assets/icons/components"; +import styled, { css, useTheme } from "styled-components"; +import useGetDrinkRecommendationListService from "../services/useGetDrinkRecommendationListService"; + +const SLIDE_MOVE_COUNT = 1; +const ORIGINAL_IMAGE_LENGTH = 10; +const MOVE_DISTANCE = 20; + +function TodayDrinkRecommendation() { + const theme = useTheme(); + const slideRef = useRef(null); + const [currentSlide, setCurrentSlide] = useState(1); + const [isAnimation, setIsAnimation] = useState(true); + const [isFlowing, setIsFlowing] = useState(true); + + const date = new Date(); + const drinkRecommendationList = useGetDrinkRecommendationListService({ + page: date.getDate(), + perPage: 10, + }); + + useEffect(() => { + if (!slideRef.current) return; + + if (currentSlide === ORIGINAL_IMAGE_LENGTH + 1) { + setTimeout(() => { + setIsAnimation(false); + slideRef.current!.style.transform = `translateY(-${ + MOVE_DISTANCE * ORIGINAL_IMAGE_LENGTH + }px)`; + setCurrentSlide(1); + }, 500); + + setTimeout(() => { + setIsAnimation(true); + }, 600); + } + slideRef.current.style.transform = `translateY(-${MOVE_DISTANCE * (currentSlide - 1)}px)`; + }, [currentSlide]); + + useEffect(() => { + let intervalId: NodeJS.Timeout; + if (isFlowing) { + intervalId = setInterval(() => { + setCurrentSlide((prev) => prev + SLIDE_MOVE_COUNT); + }, 3500); + } + return () => clearTimeout(intervalId); + }, [isFlowing, currentSlide]); + + return ( + +

+ + 오늘의 우리술 추천 +

+ + {drinkRecommendationList?.map(({ 전통주명 }) => ( + {전통주명} + ))} + +
+ ); +} + +const Container = styled.div` + display: flex; + align-items: center; + padding: 0 20px; + margin-top: 8px; + gap: 8px; + overflow: hidden; +`; + +const Slider = styled.div<{ isAnimation: boolean }>` + display: flex; + flex-direction: column; + height: 18px; + transition: transform 0.5s ease-in-out; + gap: 2px; + ${({ isAnimation }) => isAnimation && `transform: translateY(-${MOVE_DISTANCE}px);`} +`; + +const H3 = styled.h3` + ${({ theme }) => css` + ${theme.typography.body01}; + color: ${theme.colors.main_01}; + display: flex; + align-items: center; + gap: 2px; + overflow: hidden; + white-space: nowrap; + `}; +`; + +const DrinkName = styled.span` + ${({ theme }) => theme.typography.body03}; +`; + +export default TodayDrinkRecommendation; diff --git a/apps/jurumarble/src/app/main/services/useGetDrinkRecommendationListService.ts b/apps/jurumarble/src/app/main/services/useGetDrinkRecommendationListService.ts new file mode 100644 index 00000000..3c962bf1 --- /dev/null +++ b/apps/jurumarble/src/app/main/services/useGetDrinkRecommendationListService.ts @@ -0,0 +1,22 @@ +import { useQuery } from "@tanstack/react-query"; +import { getDrinkRecommendationListAPI } from "lib/apis/drink"; +import { queryKeys } from "lib/queryKeys"; + +type GetDrinkRecommendationListProps = Exclude< + Parameters[0], + undefined +>; + +const getDrinkRecommendationListQueryKey = (params: GetDrinkRecommendationListProps) => [ + queryKeys.TODAY_DRINK_RECOMMENDATION, + { ...params }, +]; + +export default function useGetDrinkRecommendationListService( + params: GetDrinkRecommendationListProps, +) { + const { data } = useQuery(getDrinkRecommendationListQueryKey(params), () => + getDrinkRecommendationListAPI(params), + ); + return data?.data; +} diff --git a/apps/jurumarble/src/app/main/services/useGetHotDrinkVoteService.ts b/apps/jurumarble/src/app/main/services/useGetHotDrinkVoteService.ts index 52bf713e..0e484036 100644 --- a/apps/jurumarble/src/app/main/services/useGetHotDrinkVoteService.ts +++ b/apps/jurumarble/src/app/main/services/useGetHotDrinkVoteService.ts @@ -5,7 +5,7 @@ import { queryKeys } from "lib/queryKeys"; const getQueryKey = () => [queryKeys.HOT_DRINK_VOTE]; export default function useGetHotDrinkVoteService() { - const { data } = useQuery(getQueryKey(), getHotDrinkVote); + const { data: hotDrinkVote } = useQuery(getQueryKey(), getHotDrinkVote); - return { data }; + return { hotDrinkVote }; } diff --git a/apps/jurumarble/src/app/map/components/RegionBottomsheet.tsx b/apps/jurumarble/src/app/map/components/RegionBottomsheet.tsx index 8b3bcbf8..ee9f5931 100644 --- a/apps/jurumarble/src/app/map/components/RegionBottomsheet.tsx +++ b/apps/jurumarble/src/app/map/components/RegionBottomsheet.tsx @@ -1,7 +1,6 @@ import { Button, Portal } from "components/index"; import { REGION_LIST } from "lib/constants"; import { transitions } from "lib/styles"; -import Image from "next/image"; import React from "react"; import { SvgIcPrev, SvgIcX } from "src/assets/icons/components"; import styled, { css } from "styled-components"; diff --git a/apps/jurumarble/src/app/page.tsx b/apps/jurumarble/src/app/page.tsx index 382acfd0..c2239b55 100644 --- a/apps/jurumarble/src/app/page.tsx +++ b/apps/jurumarble/src/app/page.tsx @@ -1,26 +1,21 @@ "use client"; import BottomBar from "components/BottomBar"; -import DivideLine from "components/DivideLine"; import Header from "components/Header"; -import { KAKAO_MAP_API_KEY } from "lib/constants"; -import Image from "next/image"; -import Script from "next/script"; -import { MainBannerImage } from "public/images"; -import styled from "styled-components"; +import styled, { css } from "styled-components"; +import Banner from "./main/components/Banner"; import HotDrinkContainer from "./main/components/HotDrinkContainer"; import HotDrinkVoteContainer from "./main/components/HotDrinkVoteContainer"; import SearchInputWrapper from "./main/components/SearchInputWrapper"; +import TodayDrinkRecommendation from "./main/components/TodayDrinkRecommendation"; function MainPage() { return ( <>
- + - - 배너 - + @@ -37,16 +32,18 @@ const TopSection = styled.section` padding: 0 20px; `; -const BannerImageWrapper = styled.div` - position: relative; - margin-top: 36px; - aspect-ratio: 16 / 9; -`; - const BottomSection = styled.section` padding: 0 20px 96px; // 64(BottomBar height) + 32(margin) = 96 margin-top: 8px; overflow: auto; `; +const DivideLine = styled.div` + ${({ theme }) => css` + background-color: ${theme.colors.bg_01}; + height: 8px; + margin: 40px 0 8px 0; + `} +`; + export default MainPage; diff --git a/apps/jurumarble/src/assets/icons/components/IcNext.tsx b/apps/jurumarble/src/assets/icons/components/IcNext.tsx new file mode 100644 index 00000000..9b7717b8 --- /dev/null +++ b/apps/jurumarble/src/assets/icons/components/IcNext.tsx @@ -0,0 +1,45 @@ +import type { SVGProps } from "react"; +const SvgNext = (props: SVGProps) => ( + + + + + + + + + + +); +export default SvgNext; diff --git a/apps/jurumarble/src/assets/icons/components/IcPrevious.tsx b/apps/jurumarble/src/assets/icons/components/IcPrevious.tsx index eaf7f7b1..a25a6a69 100644 --- a/apps/jurumarble/src/assets/icons/components/IcPrevious.tsx +++ b/apps/jurumarble/src/assets/icons/components/IcPrevious.tsx @@ -1,33 +1,42 @@ import type { SVGProps } from "react"; -const SvgIcPrevious = (props: SVGProps) => ( +const SvgPrevious = (props: SVGProps) => ( - - - - - + + + + + + + ); -export default SvgIcPrevious; +export default SvgPrevious; diff --git a/apps/jurumarble/src/assets/icons/components/index.ts b/apps/jurumarble/src/assets/icons/components/index.ts index e4a1e98e..897ec1f8 100644 --- a/apps/jurumarble/src/assets/icons/components/index.ts +++ b/apps/jurumarble/src/assets/icons/components/index.ts @@ -26,3 +26,4 @@ export { default as SvgIcThunder } from "./IcThunder"; export { default as SvgWarningIcon } from "./IcWarningIcon"; export { default as SvgNotificationCheck } from "./IcNotificationCheck"; export { default as SvgInfo } from "./IcInfo"; +export { default as SvgNext } from "./IcNext"; diff --git a/apps/jurumarble/src/lib/apis/drink.ts b/apps/jurumarble/src/lib/apis/drink.ts index e7b7c8fe..366df215 100644 --- a/apps/jurumarble/src/lib/apis/drink.ts +++ b/apps/jurumarble/src/lib/apis/drink.ts @@ -1,3 +1,5 @@ +import axios from "axios"; +import { DATA_GO_API_KEY } from "lib/constants"; import { DrinkInfoSortType } from "src/types/common"; import { DrinkListResponse, DrinkMapResponse } from "src/types/drink"; import { baseApi } from "./http/base"; @@ -102,3 +104,39 @@ export const getDrinkInfo = async (drinkId: number) => { const response = await baseApi.get(`api/drinks/${drinkId}`); return response.data; }; + +interface GetDrinkRecommendationListRequest { + page: number; + perPage: number; + returnType?: "json" | "xml"; +} + +interface GetDrinkRecommendationListResponse { + currentCount: number; + data: Data[]; + matchCount: number; + page: number; + perPage: number; + totalCount: number; +} + +interface Data { + 규격: string; + 도수: string; + 전통주명: string; + 제조사: string; + 주원료: string; +} + +export const getDrinkRecommendationListAPI = async (params: GetDrinkRecommendationListRequest) => { + const response = await axios.get( + "https://api.odcloud.kr/api/15048755/v1/uddi:fec53d3a-2bef-4494-b50e-f4e566f403e0", + { + params: { + ...params, + serviceKey: DATA_GO_API_KEY, + }, + }, + ); + return response.data; +}; diff --git a/apps/jurumarble/src/lib/constants.tsx b/apps/jurumarble/src/lib/constants.tsx index 27f1bc05..a68cb2e6 100644 --- a/apps/jurumarble/src/lib/constants.tsx +++ b/apps/jurumarble/src/lib/constants.tsx @@ -24,6 +24,8 @@ export const NAVER_LOGIN_REDIRECT_URL = ? `http://localhost:3000${Path.NAVER_LOGIN_PROCESS}` : `${CLIENT_URL}${Path.NAVER_LOGIN_PROCESS}`; +export const DATA_GO_API_KEY = process.env.NEXT_PUBLIC_DATA_GO_API_KEY || ""; + export const REGION_LIST = [ { value: "SEOUL", label: "서울", lat: 37.53391, long: 126.9775 }, { value: "INCHEON", label: "인천", lat: 37.45323333333334, long: 126.70735277777779 }, diff --git a/apps/jurumarble/src/lib/queryKeys.ts b/apps/jurumarble/src/lib/queryKeys.ts index fe5f6fac..9e42fef0 100644 --- a/apps/jurumarble/src/lib/queryKeys.ts +++ b/apps/jurumarble/src/lib/queryKeys.ts @@ -24,6 +24,7 @@ export const queryKeys = { DRINKS_INFO: "drinksInfo" as const, NOTIFICATION_LIST: "notificationList" as const, LOGIN_INFO: "loginInfo" as const, + TODAY_DRINK_RECOMMENDATION: "todayDrinkRecommendation" as const, }; export const reactQueryKeys = {