Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JR-871] 오늘의 우리술 추천 기능 구현 #177

Merged
merged 8 commits into from
Oct 15, 2023
46 changes: 46 additions & 0 deletions apps/jurumarble/src/app/main/components/Banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import styled, { css } from "styled-components";
import Image from "next/image";
import { MainBannerImage } from "public/images";

function Banner() {
return (
<Container>
<Image alt="배너" src={MainBannerImage} fill style={{ borderRadius: "16px" }} />
<BannerText>
<MainText>여행의 즐거움을 우리술과 함께 레벨업!</MainText>
<SubText>
여행지에서 우리술이 고민된다면 <br /> 주루마블에서 해결해보세요
</SubText>
</BannerText>
</Container>
);
}

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;
157 changes: 130 additions & 27 deletions apps/jurumarble/src/app/main/components/Carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,125 @@ 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[];
}

function Carousel({ hotDrinkList }: Props) {
const router = useRouter();
const slideRef = useRef<HTMLOListElement>(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 (
<Container>
<Slides>
{hotDrinkList.map((hotDrink: GetHotDrinkResponse, index: number) => {
const { drinkId, image, name, manufactureAddress } = hotDrink;
return (
<Slide key={drinkId} onClick={() => router.push(`${Path.DRINK_INFO_PAGE}/${drinkId}`)}>
<Box>
<DrinkImageWrapper>
<RankginMark>{index + 1}</RankginMark>
<Image alt="전통주" src={image} fill style={{ borderRadius: "10px" }} />
</DrinkImageWrapper>
<DrinkText>
{name}
<AreaName>{manufactureAddress}</AreaName>
</DrinkText>
</Box>
</Slide>
);
})}
</Slides>
</Container>
<>
<Container>
<Slides ref={slideRef} isAnimation={isAnimation}>
{hotDrinkList.map((hotDrink: GetHotDrinkResponse, index: number) => {
const { drinkId, image, name, manufactureAddress } = hotDrink;
return (
<Slide
key={drinkId}
onClick={() => router.push(`${Path.DRINK_INFO_PAGE}/${drinkId}`)}
>
<Box>
<DrinkImageWrapper>
<RankginMark>{index + 1}</RankginMark>
<Image alt="전통주" src={image} fill style={{ borderRadius: "10px" }} />
</DrinkImageWrapper>
<DrinkText>
{name}
<AreaName>{manufactureAddress}</AreaName>
</DrinkText>
</Box>
</Slide>
);
})}
</Slides>
<CarouselControlContainer>
<DivideLine />
<SlideButtonContainer>
<SlideButton onClick={onPrevSlide}>
<SvgIcPrevious width={20} height={20} />
</SlideButton>
<SlideButton onClick={onNextSlide}>
<SvgNext width={20} height={20} />
</SlideButton>
</SlideButtonContainer>
</CarouselControlContainer>
</Container>
</>
);
}

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 모바일에서는 보이게 하기
**/
Expand All @@ -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;
`;

Expand Down Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ 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;
}
const { voteId, voteTitle, drinkAImage, drinkBImage } = hotDrinkVote;

const nowTime = new Date().getHours();

const router = useRouter();

return (
<>
<H2>
Expand Down
101 changes: 101 additions & 0 deletions apps/jurumarble/src/app/main/components/TodayDrinkRecommendation.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(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 (
<Container>
<H3>
<SvgStamp width={24} height={24} fill={theme.colors.main_01} />
오늘의 우리술 추천
</H3>
<Slider ref={slideRef} isAnimation={isAnimation}>
{drinkRecommendationList?.map(({ 전통주명 }) => (
<DrinkName key={전통주명}>{전통주명}</DrinkName>
))}
</Slider>
</Container>
);
}

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;
Original file line number Diff line number Diff line change
@@ -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<typeof getDrinkRecommendationListAPI>[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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Loading