Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/app/(with-header)/wines/_components/RecommendWine.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use client';

import { useState } from 'react';
import Image from 'next/image';
import Slider from 'react-slick';
import { Wine } from '@/types/wine';
import RecommendWineCard from './RecommendWineCard';
import rightArrowIcon from '@/assets/icons/right_arrow.svg';
import leftArrowIcon from '@/assets/icons/left_arrow.svg';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';

interface RecommendWineProps {
recommendedList: Wine[];
}

export default function RecommendWine({ recommendedList }: RecommendWineProps) {
const [currentIndex, setCurrentIndex] = useState(0);

const settings = {
dots: false,
infinite: false,
speed: 500,
slidesToScroll: 1,
variableWidth: true,
afterChange: (index: number) => setCurrentIndex(index),
nextArrow: currentIndex < recommendedList.length - 1 ? <NextArrowButton /> : undefined,
prevArrow: <PrevArrowButton disabled={currentIndex === 0} />,
};

return (
<div>
<div className='flex h-[299px] flex-col gap-[30px] overflow-hidden rounded-2xl bg-gray-100 p-[30px] pr-0 mobile:h-[241px] mobile:gap-5 mobile:p-[20px] mobile:pr-0'>
<p className='text-[20px] font-bold leading-[23px] text-gray-800 mobile:text-[18px] mobile:leading-[21px]'>이번 달 추천 와인</p>
<div className='relative'>
<Slider {...settings}>
{recommendedList.map((wine, idx) => (
<div key={idx} className='pr-[15px] mobile:pr-[10px]'>
<RecommendWineCard key={wine.id} id={wine.id} image={wine.image} name={wine.name} avgRating={wine.avgRating} />
</div>
))}
</Slider>
</div>
</div>
</div>
);
}

interface ArrowProps {
onClick?: () => void;
disabled?: boolean;
}

function NextArrowButton({ onClick }: ArrowProps) {
return (
<button className='absolute right-5 top-1/2 z-10 flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full border border-gray-300 bg-white' onClick={onClick}>
<Image src={rightArrowIcon} alt='다음' width={24} height={24} />
</button>
);
}

function PrevArrowButton({ onClick, disabled }: ArrowProps) {
return (
<button
className={`absolute left-[-10px] top-1/2 z-10 flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full border border-gray-300 bg-white transition-opacity ${
disabled ? 'pointer-events-none opacity-0' : 'opacity-100'
}`}
onClick={onClick}
disabled={disabled}
>
<Image src={leftArrowIcon} alt='이전' width={24} height={24} />
</button>
);
}
88 changes: 17 additions & 71 deletions src/app/(with-header)/wines/_components/RecommendWineContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,88 +1,34 @@
'use client';

import { useEffect, useState } from 'react';
import Image from 'next/image';
import Slider from 'react-slick';
import { fetchRecommendWine } from '@/lib/fetchRecommendWine';
import RecommendWine from './RecommendWine';
import RecommendWineListSkeleton from './skeleton/RecommendWineListSkeleton';
import { Wine } from '@/types/wine';
import RecommendWineCard from './RecommendWineCard';
import rightArrowIcon from '@/assets/icons/right_arrow.svg';
import leftArrowIcon from '@/assets/icons/left_arrow.svg';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';

export default function RecommendWineContainer() {
const [recommendedList, setRecommendedList] = useState<Wine[]>([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [loading, setLoading] = useState<boolean>(true);

useEffect(() => {
async function getRecommendWines() {
const getRecommendedWines = async () => {
setLoading(true);
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/wines/recommended?limit=8`, {
next: { revalidate: 1800 },
});
const wines: Wine[] = await response.json();
const wines = await fetchRecommendWine();
setRecommendedList(wines);
} catch (error) {
console.error('추천 와인 목록을 가져오는 중 오류 발생:', error);
console.error('추천 와인 목록을 가져오는 데 실패했습니다.', error);
} finally {
setLoading(false);
}
}
getRecommendWines();
}, []);

const settings = {
dots: false,
infinite: false,
speed: 500,
slidesToScroll: 1,
variableWidth: true,
afterChange: (index: number) => setCurrentIndex(index),
nextArrow: currentIndex < recommendedList.length - 1 ? <NextArrowButton /> : undefined,
prevArrow: <PrevArrowButton disabled={currentIndex === 0} />,
};

return (
<section>
<div className=''>
<div className='flex h-[299px] flex-col gap-[30px] overflow-hidden rounded-2xl bg-gray-100 p-[30px] pr-0 mobile:h-[241px] mobile:gap-5 mobile:p-[20px] mobile:pr-0'>
<p className='text-[20px] font-bold leading-[23px] text-gray-800 mobile:text-[18px] mobile:leading-[21px]'>이번 달 추천 와인</p>
<div className='relative'>
<Slider {...settings}>
{recommendedList.map((wine, idx) => (
<div key={idx} className='pr-[15px] mobile:pr-[10px]'>
<RecommendWineCard key={wine.id} id={wine.id} image={wine.image} name={wine.name} avgRating={wine.avgRating} />
</div>
))}
</Slider>
</div>
</div>
</div>
</section>
);
}
};

interface ArrowProps {
onClick?: () => void;
disabled?: boolean;
}
getRecommendedWines();
}, []);

function NextArrowButton({ onClick }: ArrowProps) {
return (
<button className='absolute right-5 top-1/2 z-10 flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full border border-gray-300 bg-white' onClick={onClick}>
<Image src={rightArrowIcon} alt='다음' width={24} height={24} />
</button>
);
}
if (loading) {
return <RecommendWineListSkeleton count={8} />;
}

function PrevArrowButton({ onClick, disabled }: ArrowProps) {
return (
<button
className={`absolute left-[-10px] top-1/2 z-10 flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full border border-gray-300 bg-white transition-opacity ${
disabled ? 'pointer-events-none opacity-0' : 'opacity-100'
}`}
onClick={onClick}
disabled={disabled}
>
<Image src={leftArrowIcon} alt='이전' width={24} height={24} />
</button>
);
return <RecommendWine recommendedList={recommendedList} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default function RecommendWineCardSkeleton() {
return (
<div className='flex h-[185px] w-[232px] animate-pulse justify-between gap-[28px] rounded-xl bg-white px-[30px] pt-6 mobile:h-[160px] mobile:w-[193px] mobile:gap-[25px] mobile:px-[25px]'>
<div className='h-[161px] w-[44px] flex-shrink-0 rounded bg-gray-200 mobile:h-[136px] mobile:w-[38px]'></div>
<div className='flex w-full flex-col gap-[10px] mobile:h-[136px] mobile:w-[80px] mobile:gap-3'>
<div className='h-[40px] w-[60px] rounded bg-gray-200 mobile:h-[30px] mobile:w-[50px]'></div>
<div className='h-[24px] w-[100px] rounded bg-gray-200 mobile:h-[18px] mobile:w-[80px]'></div>
<div className='h-[18px] w-[100px] rounded bg-gray-200 mobile:h-[18px] mobile:w-[80px]'></div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import RecommendWineCardSkeleton from './RecommendWineCardSkeleton';

export default function RecommendWineListSkeleton({ count }: { count: number }) {
return (
<div className='flex h-[299px] flex-col gap-[30px] overflow-hidden rounded-2xl bg-gray-100 p-[30px] pr-0 mobile:h-[241px] mobile:gap-5 mobile:p-[20px] mobile:pr-0'>
<p className='text-[20px] font-bold leading-[23px] text-gray-800 mobile:text-[18px] mobile:leading-[21px]'>이번 달 추천 와인</p>
<div className='relative'>
<div className='flex animate-pulse gap-[15px] overflow-hidden rounded-2xl bg-gray-100 mobile:gap-[10px]'>
{new Array(count).fill(0).map((_, idx) => (
<RecommendWineCardSkeleton key={`wine-item-skeleton-${idx}`} />
))}
</div>
</div>
</div>
);
}
18 changes: 18 additions & 0 deletions src/lib/fetchRecommendWine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Wine } from '@/types/wine';

export async function fetchRecommendWine(): Promise<Wine[]> {
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/wines/recommended?limit=8`, {
next: { revalidate: 1800 },
});

if (!res.ok) {
throw new Error('추천 와인 목록을 가져오는 데 실패했습니다.');
}

return res.json();
} catch (error) {
console.error(error);
return [];
}
}