Skip to content

Commit

Permalink
[JR-871] 오늘의 우리술 추천 기능 구현 (#177)
Browse files Browse the repository at this point in the history
* feat: 한국농수산식품유통공사의 전통주 정보를 가져오는 로직 추가

* feat: 오늘의 우리술 추천을 보여주는 슬라이더 구현

* feat: 배너 텍스트 추가

* feat: 이전 다음 아이콘 추가

* fix: 키값 추가

* fix: 훅의 규칙을 위반하지 않도록 수정

* feat: 캐러셀 버튼을 클릭하여 조작할 수 있도록 구현

* fix: undefined 처리되어 에러나던 것 null 처리
  • Loading branch information
Leejha authored Oct 15, 2023
1 parent b37a832 commit 5f405d3
Show file tree
Hide file tree
Showing 14 changed files with 435 additions and 71 deletions.
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

0 comments on commit 5f405d3

Please sign in to comment.