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 = {