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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useCallback, useEffect, useState } from 'react';
import Image from 'next/image';
import { fetchWithAuth } from '@/lib/auth';
import { MyWineListResponse, WineDetails } from '@/types/wine';
import { WineListResponse, WineDetails } from '@/types/wine';
import emptyData from '@/assets/icons/empty_review.svg';
import WineCard from '@/components/WineCard';
import { WineDataProps } from './MyWIneKebabDropDown ';
Expand All @@ -26,7 +26,7 @@ export default function MyWineListContainer({ setDataCount }: { setDataCount: (v
return;
}

const data: MyWineListResponse = await response.json();
const data: WineListResponse = await response.json();
setMyWineData(data.list);
setDataCount(data.totalCount);
} catch (error) {
Expand Down
2 changes: 1 addition & 1 deletion src/app/(with-header)/wines/Wines.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import WineListContainer from './_components/WineListContainer';

export default function Wines() {
return (
<div className='mx-auto mb-[100px] max-w-[1140px] tablet:max-w-[1050px] mobile:min-w-[343px] mobile:max-w-[752px]'>
<div className='mx-auto mb-[80px] max-w-[1140px] tablet:max-w-[1050px] mobile:min-w-[343px] mobile:max-w-[752px]'>
<div className='mx-auto mt-[20px] w-[1140px] tablet:mx-6 tablet:w-auto tablet:min-w-[700px] tablet:max-w-[1000px] mobile:mx-5 mobile:mt-[15px] mobile:min-w-[343px] mobile:max-w-[700px]'>
<RecommendedWineContainer />
<WineListContainer />
Expand Down
11 changes: 8 additions & 3 deletions src/app/(with-header)/wines/_components/WineCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import { forwardRef } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/contexts/AuthProvider';
Expand All @@ -11,7 +12,7 @@ type WineCardProps = {
wine: WineDetails;
};

export default function WineCard({ wine }: WineCardProps) {
const WineCard = forwardRef<HTMLDivElement, WineCardProps>(({ wine }, ref) => {
const router = useRouter();
const { isLoggedIn } = useAuth();
const handleClick = () => {
Expand All @@ -23,7 +24,7 @@ export default function WineCard({ wine }: WineCardProps) {
};

return (
<div onClick={handleClick} className='cursor-pointer rounded-2xl border border-gray-300 hover:shadow-lg'>
<div ref={ref} onClick={handleClick} className='cursor-pointer rounded-2xl border border-gray-300 hover:shadow-lg'>
<div className='flex justify-between gap-[81px] tablet:gap-[47px] mobile:gap-9'>
<div className='relative ml-[60px] mt-10 h-[208px] w-[60px] overflow-hidden tablet:ml-10 mobile:ml-5'>
<Image src={wine.image} alt={wine.name} sizes='30vw' fill className='absolute object-cover' />
Expand Down Expand Up @@ -58,4 +59,8 @@ export default function WineCard({ wine }: WineCardProps) {
</div>
</div>
);
}
});

WineCard.displayName = 'WineCard';

export default WineCard;
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default function WineFilterModal({ isOpen, onClose, onApply, onFilterChan
>
<div className='mb-8 flex flex-row justify-between'>
<p className='text-xl font-bold text-gray-800'>필터</p>
<Image src={closeIcon} alt='닫기 아이콘' width={24} height={24} className='cursor-pointer' onClick={onClose} />
<Image src={closeIcon} alt='닫기 아이콘' width={24} height={24} className='h-6 w-6 cursor-pointer' onClick={onClose} />
</div>

<div className='flex flex-col gap-[56px]'>
Expand Down
94 changes: 66 additions & 28 deletions src/app/(with-header)/wines/_components/WineListContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
'use client';

import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useRef } from 'react';
import { useAuth } from '@/contexts/AuthProvider';
import Image from 'next/image';
import { fetchWines } from '@/lib/fetchWines';
import { WineDetails } from '@/types/wine';
import SearchBar from './SearchBar';
import WineFilter from './WineFilter';
import WineFilterModal from './WineFilterModal';
import WineCard from './WineCard';
import PostWineModal from '@/components/modal/PostWineModal';
import LoadingSpinner from '@/components/LoadingSpinner';
import filterIcon from '@/assets/icons/filter.svg';

const MAX_PRICE = 2000000;
Expand All @@ -25,6 +27,62 @@ export default function WineListContainer() {
});
const [isFilterModalOpen, setFilterModalOpen] = useState<boolean>(false);
const [pendingFilters, setPendingFilters] = useState(filters);
const [isLoading, setIsLoading] = useState(false);
const [nextCursor, setNextCursor] = useState<number | null>(null);
const [hasMore, setHasMore] = useState(true);
const lastWineRef = useRef<HTMLDivElement | null>(null);

const loadMoreWines = useCallback(async () => {
if (isLoading || !hasMore) return;
setIsLoading(true);
try {
const response = await fetchWines({
limit: 8,
cursor: nextCursor ?? undefined,
type: filters.type ?? undefined,
minPrice: filters.minPrice || undefined,
maxPrice: filters.maxPrice || undefined,
rating: filters.rating ?? undefined,
name: searchQuery || undefined,
});
setWines((prev) => [...prev, ...response.list]);
setNextCursor(response.nextCursor);
setHasMore(response.nextCursor !== null);
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
}, [isLoading, hasMore, nextCursor, filters, searchQuery]);

useEffect(() => {
if (!hasMore || !lastWineRef.current) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
loadMoreWines();
}
},
{
root: null,
threshold: 0.5,
},
);
observer.observe(lastWineRef.current);
return () => observer.disconnect();
}, [loadMoreWines, hasMore]);

useEffect(() => {
setWines([]);
setNextCursor(null);
setHasMore(true);
}, [filters, searchQuery]);

useEffect(() => {
if (wines.length === 0 && hasMore) {
loadMoreWines();
}
}, [wines, hasMore, loadMoreWines]);

useEffect(() => {
if (isFilterModalOpen) {
Expand Down Expand Up @@ -55,29 +113,6 @@ export default function WineListContainer() {
setFilterModalOpen(false);
};

useEffect(() => {
const fetchWines = async () => {
const params = new URLSearchParams();
if (filters.type) params.append('type', filters.type.toUpperCase());
if (filters.minPrice) params.append('minPrice', filters.minPrice.toString());
if (filters.maxPrice) params.append('maxPrice', filters.maxPrice.toString());
if (filters.rating) params.append('rating', filters.rating.toString());
if (searchQuery) params.append('name', searchQuery);

const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/wines?limit=20&${params.toString()}`);

if (!res.ok) {
console.error('Failed to fetch wines');
return;
}

const data = await res.json();
setWines(data.list);
};

fetchWines();
}, [filters, searchQuery]);

return (
<div className='mt-10 grid grid-cols-[284px,1fr] grid-rows-[48px,1fr] gap-x-[60px] gap-y-[62px] tablet:flex tablet:flex-col mobile:mt-5 mobile:gap-[30px]'>
<div className='hidden pc:block' />
Expand Down Expand Up @@ -109,10 +144,13 @@ export default function WineListContainer() {
)}
</div>
<WineFilterModal isOpen={isFilterModalOpen} onClose={() => setFilterModalOpen(false)} onApply={applyFilters} initialFilters={pendingFilters} onFilterChange={setPendingFilters} />
<div className='flex flex-1 flex-col gap-[62px]'>
{wines.map((wine) => (
<WineCard key={wine.id} wine={wine} />
))}
<div className='flex min-h-0 flex-col'>
<div className='scrollbar-hidden flex h-[650px] max-h-screen flex-col gap-[62px] tablet:h-[550px]'>
{wines.map((wine, index) => (
<WineCard key={`${wine.id}-${index}`} ref={index === wines.length - 1 ? lastWineRef : null} wine={wine} />
))}
{isLoading && <LoadingSpinner />}
</div>
</div>
</div>
);
Expand Down
6 changes: 6 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,10 @@ body {
-webkit-box-orient: vertical;
overflow: hidden;
}
.scrollbar-hidden {
overflow-y: auto;
}
.scrollbar-hidden::-webkit-scrollbar {
width: 0px;
}
}
30 changes: 30 additions & 0 deletions src/lib/fetchWines.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { WineListResponse } from '@/types/wine';

interface FetchWinesParams {
limit: number;
cursor?: number;
type?: string;
minPrice?: number;
maxPrice?: number;
rating?: number;
name?: string;
}

export async function fetchWines({ limit, cursor, type, minPrice, maxPrice, rating, name }: FetchWinesParams): Promise<WineListResponse> {
const params = new URLSearchParams();
params.append('limit', limit.toString());
if (cursor) params.append('cursor', cursor.toString());
if (type) params.append('type', type.toUpperCase());
if (minPrice) params.append('minPrice', minPrice.toString());
if (maxPrice) params.append('maxPrice', maxPrice.toString());
if (rating) params.append('rating', rating.toString());
if (name) params.append('name', name);

const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/wines?${params.toString()}`);

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

return res.json();
}
4 changes: 2 additions & 2 deletions src/types/wine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ export interface Wine {
avgRating: number;
}

export interface WineListResponse {
export interface RecommendWineListResponse {
totalCount: number;
nextCursor: number | null;
list: Wine[];
}

export interface MyWineListResponse {
export interface WineListResponse {
totalCount: number;
nextCursor: number | null;
list: WineDetails[];
Expand Down