From 597470087bdd7534ddda5ab1620d74d08ac568fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B4=80=ED=98=84=5CLuganic?= Date: Fri, 1 Aug 2025 20:04:12 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20api=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=EB=B8=8C=EB=9E=9C=EC=B9=98=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/wines/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/wines/index.tsx b/src/pages/wines/index.tsx index 4ea9ec66..4461d227 100644 --- a/src/pages/wines/index.tsx +++ b/src/pages/wines/index.tsx @@ -2,7 +2,6 @@ import WineFilter from '@/components/common/winelist/WineFilter'; import WineListCard from '@/components/common/winelist/WineListCard'; import WineSlider from '@/components/common/winelist/WineSlider'; -//// export default function Wines() { return (
From af6aef5d3bbb6d675eb12aba0d91eaa0af33444a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B4=80=ED=98=84=5CLuganic?= Date: Fri, 1 Aug 2025 21:07:55 +0900 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20useWineListQuery.ts=20api=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useWineListQuery.ts | 23 +++++++++-- src/lib/winelist.ts | 75 +++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 src/lib/winelist.ts diff --git a/src/hooks/useWineListQuery.ts b/src/hooks/useWineListQuery.ts index 9809f8bf..e37e326f 100644 --- a/src/hooks/useWineListQuery.ts +++ b/src/hooks/useWineListQuery.ts @@ -1,26 +1,43 @@ import { useInfiniteQuery } from '@tanstack/react-query'; -import { getWines } from '@/lib/getWines'; +import { getWines } from '@/lib/winelist'; import useFilterStore from '@/stores/filterStore'; import useWineSearchKeywordStore from '@/stores/searchStore'; const PAGE_LIMIT = 8; +const getMinRatingFromFilter = (rating: string): number | undefined => { + const ratingMap: Record = { + '4.6': 4.5, + '4.1': 4.0, + '3.6': 3.5, + '3.1': 3.0, + }; + return ratingMap[rating]; +}; + export function useWineListQuery() { - /* 각 필터 상태 */ const type = useFilterStore((state) => state.type); const minPrice = useFilterStore((state) => state.minPrice); const maxPrice = useFilterStore((state) => state.maxPrice); const rating = useFilterStore((state) => state.rating); const searchTerm = useWineSearchKeywordStore((state) => state.searchTerm); + const minRating = getMinRatingFromFilter(rating); + return useInfiniteQuery({ queryKey: ['wines', { type, minPrice, maxPrice, rating, searchTerm }], queryFn: ({ pageParam = 0 }) => getWines({ cursor: pageParam, limit: PAGE_LIMIT, - filters: { type, minPrice, maxPrice, rating, searchTerm }, + filters: { + type, + minPrice, + maxPrice, + rating: minRating, + searchTerm, + }, }), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, diff --git a/src/lib/winelist.ts b/src/lib/winelist.ts new file mode 100644 index 00000000..f63b03f0 --- /dev/null +++ b/src/lib/winelist.ts @@ -0,0 +1,75 @@ +const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL; +const TEAM_ID = process.env.NEXT_PUBLIC_TEAM; + +export interface Wine { + id: number; + name: string; + region: string; + image: string; + price: number; + type: 'RED' | 'WHITE' | 'SPARKLING'; + avgRating: number; + reviewCount: number; + recentReview: { + id: number; + content: string; + createdAt: string; + updatedAt: string; + } | null; + userId: number; +} + +export interface RecommendedWine { + id: number; + name: string; + region: string; + image: string; + price: number; + type: 'RED' | 'WHITE' | 'SPARKLING'; + avgRating: number; +} + +export interface GetWinesParams { + cursor: number; + limit: number; + filters?: { + type?: string; + minPrice?: number; + maxPrice?: number; + rating?: number; + searchTerm?: string; + }; +} + +export interface WinesResponse { + list: Wine[]; + totalCount: number; + nextCursor: number | null; +} +/* 추천 와인 API */ +export async function getRecommendedWines(): Promise { + const res = await fetch(`${BASE_URL}/${TEAM_ID}/wines/recommended`); + if (!res.ok) throw new Error('추천 와인 불러오기 실패'); + return res.json(); +} + +/* 전체 와인 목록 API (필터 + 페이징) */ +export async function getWines({ + cursor, + limit, + filters = {}, +}: GetWinesParams): Promise { + const query = new URLSearchParams({ + cursor: String(cursor), + limit: String(limit), + ...(filters.type && { type: filters.type }), + ...(filters.minPrice && { minPrice: String(filters.minPrice) }), + ...(filters.maxPrice && { maxPrice: String(filters.maxPrice) }), + ...(filters.rating && { rating: String(filters.rating) }), + ...(filters.searchTerm && { searchTerm: filters.searchTerm }), + }); + + const res = await fetch(`${BASE_URL}/${TEAM_ID}/wines?${query.toString()}`); + if (!res.ok) throw new Error('와인 목록 불러오기 실패'); + return res.json(); +} From 62ec1523df698eec38a01dfe76c191de38de4f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B4=80=ED=98=84=5CLuganic?= Date: Fri, 1 Aug 2025 21:20:09 +0900 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20wineSlider=20api=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/winelist/WineSlider.tsx | 88 +++++++++---------- 1 file changed, 42 insertions(+), 46 deletions(-) diff --git a/src/components/common/winelist/WineSlider.tsx b/src/components/common/winelist/WineSlider.tsx index 54b6c340..27b367d1 100644 --- a/src/components/common/winelist/WineSlider.tsx +++ b/src/components/common/winelist/WineSlider.tsx @@ -1,3 +1,5 @@ +import { useEffect, useState } from 'react'; + import { Carousel, CarouselContent, @@ -5,45 +7,29 @@ import { CarouselPrevious, CarouselNext, } from '@/components/ui/carousel'; +import { getRecommendedWines, RecommendedWine } from '@/lib/winelist'; import WineCard from './WineCard'; -const baseWines = [ - { - id: 1, - name: 'Sentinel Carbernet Sauvignon 2016', - image: '/images/image1.svg', - rating: 4.8, - }, - { - id: 2, - name: 'Palazzo della Torre 2017', - image: '/images/image3.svg', - rating: 4.6, - }, - { - id: 3, - name: 'Sentinel Carbernet Sauvignon 2016', - image: '/images/image2.svg', - rating: 4.6, - }, - { - id: 4, - name: 'Palazzo della Torre 2017', - image: '/images/image4.svg', - rating: 3.1, - }, -]; +export default function WineSlider() { + const [wines, setWines] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [hasError, setHasError] = useState(false); -const mockWines = Array.from({ length: 20 }).map((_, i) => { - const wine = baseWines[i % baseWines.length]; - return { - ...wine, - id: i + 1, - }; -}); + useEffect(() => { + const fetchWines = async () => { + try { + const data = await getRecommendedWines(); + setWines(data); + setIsLoading(false); + } catch (e) { + setHasError(true); + setIsLoading(false); + } + }; + fetchWines(); + }, []); -export default function WineSlider() { return (
@@ -51,19 +37,29 @@ export default function WineSlider() { 이번 달 추천 와인 - {/* 캐러셀 영역 */}
- - - {mockWines.map((wine) => ( - - - - ))} - - - - + {isLoading ? ( +

불러오는 중...

+ ) : hasError ? ( +

추천 와인을 불러올 수 없습니다.

+ ) : ( + + + {wines.map((wine) => ( + + + + ))} + + + + + )}
From 7c135edd5de9aef605e8a2033d51b029c937dfef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B4=80=ED=98=84=5CLuganic?= Date: Sat, 2 Aug 2025 05:30:37 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat=20API=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/images/image1.svg | 14 -- public/images/image2.svg | 14 -- public/images/image3.svg | 14 -- public/images/image4.svg | 14 -- src/components/common/winelist/WineCard.tsx | 4 - .../common/winelist/WineListCard.tsx | 22 +- src/components/common/winelist/WineSlider.tsx | 74 +++---- src/hooks/useWineListQuery.ts | 53 +++-- src/lib/getWines.ts | 61 ----- src/lib/wineApi.ts | 55 +++++ src/lib/winelist.ts | 75 ------- src/stores/wineAddStore.ts | 209 ------------------ src/types/wineListType.ts | 48 ++++ 13 files changed, 184 insertions(+), 473 deletions(-) delete mode 100644 public/images/image1.svg delete mode 100644 public/images/image2.svg delete mode 100644 public/images/image3.svg delete mode 100644 public/images/image4.svg delete mode 100644 src/lib/getWines.ts create mode 100644 src/lib/wineApi.ts delete mode 100644 src/lib/winelist.ts create mode 100644 src/types/wineListType.ts diff --git a/public/images/image1.svg b/public/images/image1.svg deleted file mode 100644 index 92a835f4..00000000 --- a/public/images/image1.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/public/images/image2.svg b/public/images/image2.svg deleted file mode 100644 index 3985ef9d..00000000 --- a/public/images/image2.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/public/images/image3.svg b/public/images/image3.svg deleted file mode 100644 index beebe1c6..00000000 --- a/public/images/image3.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/public/images/image4.svg b/public/images/image4.svg deleted file mode 100644 index 1777f343..00000000 --- a/public/images/image4.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/components/common/winelist/WineCard.tsx b/src/components/common/winelist/WineCard.tsx index 01b28aca..50a65b1e 100644 --- a/src/components/common/winelist/WineCard.tsx +++ b/src/components/common/winelist/WineCard.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import Link from 'next/link'; import StarIcon from '@/assets/icons/star.svg'; @@ -24,7 +22,6 @@ export default function WineCard({ id, image, name, rating }: WineCardProps) { 'md:w-[232px] md:h-[185px]', )} > - {/* 왼쪽: 이미지 카드 */}
- {/* 오른쪽: 평점 + 별점 + 이름 */}
{rating.toFixed(1)} diff --git a/src/components/common/winelist/WineListCard.tsx b/src/components/common/winelist/WineListCard.tsx index d5bf850f..52621467 100644 --- a/src/components/common/winelist/WineListCard.tsx +++ b/src/components/common/winelist/WineListCard.tsx @@ -9,6 +9,7 @@ import { Button } from '@/components/ui/button'; import { useInfiniteScroll } from '@/hooks/useInfiniteScroll'; import { useWineListQuery } from '@/hooks/useWineListQuery'; import { cn } from '@/lib/utils'; +import { GetWinesResponse } from '@/types/wineListType'; export default function WineListCard() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError } = @@ -27,8 +28,10 @@ export default function WineListCard() { if (isLoading) return

불러오는 중...

; if (isError || !data) return

와인 데이터를 불러올 수 없습니다.

; - /* 전체 와인 리스트 조합 */ - const wineList = data.pages.flatMap((page) => page.list); + const wineList = data.pages.flatMap( + (page) => + (page as GetWinesResponse)?.list?.filter((wine) => !wine.image.includes('example.com')) ?? [], + ); return (
@@ -68,7 +71,7 @@ export default function WineListCard() {
- {wine.rating.toFixed(1)} + {wine.avgRating?.toFixed(1)}
@@ -76,14 +79,14 @@ export default function WineListCard() { = i + 1 ? 'fill-purpleDark' : 'fill-gray-300', + wine.avgRating >= i + 1 ? 'fill-purpleDark' : 'fill-gray-300', 'w-3 h-3', )} /> ))}
- 47개의 후기 + {wine.reviewCount}개의 후기
@@ -98,7 +101,7 @@ export default function WineListCard() {
- {wine.rating.toFixed(1)} + {wine.avgRating?.toFixed(1)}
@@ -106,14 +109,14 @@ export default function WineListCard() { = i + 1 ? 'fill-purpleDark' : 'fill-gray-300', + wine.avgRating >= i + 1 ? 'fill-purpleDark' : 'fill-gray-300', 'w-[16px] h-[16px]', )} /> ))}
- 47개의 후기 + {wine.reviewCount}개의 후기
@@ -132,13 +135,12 @@ export default function WineListCard() { 최신 후기
- {wine.review} + {wine.recentReview ? wine.recentReview.content : '아직 후기가 없습니다.'}
))} - {/* 무한 스크롤 감지 */}
); diff --git a/src/components/common/winelist/WineSlider.tsx b/src/components/common/winelist/WineSlider.tsx index 27b367d1..73503f54 100644 --- a/src/components/common/winelist/WineSlider.tsx +++ b/src/components/common/winelist/WineSlider.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; import { Carousel, @@ -7,28 +7,19 @@ import { CarouselPrevious, CarouselNext, } from '@/components/ui/carousel'; -import { getRecommendedWines, RecommendedWine } from '@/lib/winelist'; +import { getRecommendedWines } from '@/lib/wineApi'; +import { RecommendedWineResponse } from '@/types/wineListType'; import WineCard from './WineCard'; -export default function WineSlider() { - const [wines, setWines] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [hasError, setHasError] = useState(false); +const TEAM_ID = process.env.NEXT_PUBLIC_TEAM; +const RECOMMENDED_WINES_LIMIT = 4; - useEffect(() => { - const fetchWines = async () => { - try { - const data = await getRecommendedWines(); - setWines(data); - setIsLoading(false); - } catch (e) { - setHasError(true); - setIsLoading(false); - } - }; - fetchWines(); - }, []); +export default function WineSlider() { + const { data, isLoading, isError } = useQuery({ + queryKey: ['recommendedWines'], + queryFn: () => getRecommendedWines({ teamId: TEAM_ID!, limit: RECOMMENDED_WINES_LIMIT }), + }); return (
@@ -38,27 +29,36 @@ export default function WineSlider() {
- {isLoading ? ( + {isLoading && !data ? (

불러오는 중...

- ) : hasError ? ( + ) : isError || !data ? (

추천 와인을 불러올 수 없습니다.

) : ( - - - {wines.map((wine) => ( - - - - ))} - - - - + (() => { + const filteredWines = data.filter((wine) => !wine.image.includes('example.com')); + if (filteredWines.length === 0) { + return

추천 와인 목록이 없습니다.

; + } + + return ( + + + {filteredWines.map((wine) => ( + + + + ))} + + + + + ); + })() )}
diff --git a/src/hooks/useWineListQuery.ts b/src/hooks/useWineListQuery.ts index e37e326f..4554d1e4 100644 --- a/src/hooks/useWineListQuery.ts +++ b/src/hooks/useWineListQuery.ts @@ -1,23 +1,24 @@ -import { useInfiniteQuery } from '@tanstack/react-query'; +import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query'; -import { getWines } from '@/lib/winelist'; +import { getWines } from '@/lib/wineApi'; import useFilterStore from '@/stores/filterStore'; import useWineSearchKeywordStore from '@/stores/searchStore'; +import { GetWinesResponse } from '@/types/wineListType'; const PAGE_LIMIT = 8; +const TEAM_ID = process.env.NEXT_PUBLIC_TEAM; const getMinRatingFromFilter = (rating: string): number | undefined => { const ratingMap: Record = { - '4.6': 4.5, - '4.1': 4.0, - '3.6': 3.5, - '3.1': 3.0, + '4.5 - 5.0': 4.5, + '4.0 - 4.5': 4.0, + '3.5 - 4.0': 3.5, + '3.0 - 3.5': 3.0, }; return ratingMap[rating]; }; export function useWineListQuery() { - const type = useFilterStore((state) => state.type); const minPrice = useFilterStore((state) => state.minPrice); const maxPrice = useFilterStore((state) => state.maxPrice); const rating = useFilterStore((state) => state.rating); @@ -25,21 +26,31 @@ export function useWineListQuery() { const minRating = getMinRatingFromFilter(rating); - return useInfiniteQuery({ - queryKey: ['wines', { type, minPrice, maxPrice, rating, searchTerm }], - queryFn: ({ pageParam = 0 }) => - getWines({ - cursor: pageParam, + return useInfiniteQuery>({ + queryKey: ['wines', { minPrice, maxPrice, rating: minRating, searchTerm }], + queryFn: ({ pageParam = 0 }) => { + const filters = { + minPrice, + maxPrice, + rating: minRating, + searchTerm, + }; + + const filteredParams = Object.fromEntries( + Object.entries(filters).filter( + ([, value]) => value !== null && value !== undefined && value !== '', + ), + ); + + return getWines({ + teamId: TEAM_ID!, + cursor: pageParam as number, limit: PAGE_LIMIT, - filters: { - type, - minPrice, - maxPrice, - rating: minRating, - searchTerm, - }, - }), + filters: filteredParams, + }); + }, + initialPageParam: 0, - getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, + getNextPageParam: (lastPage) => lastPage?.nextCursor ?? undefined, }); } diff --git a/src/lib/getWines.ts b/src/lib/getWines.ts deleted file mode 100644 index 630fc350..00000000 --- a/src/lib/getWines.ts +++ /dev/null @@ -1,61 +0,0 @@ -import useWineAddStore, { Wine } from '@/stores/wineAddStore'; - -interface GetWinesParams { - cursor: number; - limit: number; - filters: { - type?: string; - minPrice?: number; - maxPrice?: number; - rating?: string; - searchTerm?: string; - }; -} - -interface GetWinesResponse { - list: Wine[]; - nextCursor: number | null; - totalCount: number; -} - -/* 평점 필터링 */ -const ratingRangeMap: Record = { - all: [0, 5], - '4.6': [4.5, 5], - '4.1': [4.0, 4.5], - '3.6': [3.5, 4.0], - '3.1': [3.0, 3.5], -}; - -/* 필터링 + 페이징 */ -export function getWines({ cursor, limit, filters }: GetWinesParams): GetWinesResponse { - const allWines = useWineAddStore.getState().wines; - - const { type, minPrice = 0, maxPrice = Infinity, rating = 'all', searchTerm = '' } = filters; - - const [minRating, maxRating] = ratingRangeMap[rating] ?? [0, 5]; - const keyword = searchTerm.toLowerCase(); - - const filtered = allWines.filter((wine) => { - if (type && wine.type !== type) return false; - if (wine.price < minPrice || wine.price > maxPrice) return false; - if (wine.rating < minRating || wine.rating > maxRating) return false; - if ( - keyword && - !wine.name.toLowerCase().includes(keyword) && - !wine.region.toLowerCase().includes(keyword) - ) - return false; - return true; - }); - - const totalCount = filtered.length; - const paged = filtered.slice(cursor, cursor + limit); - const nextCursor = cursor + limit < totalCount ? cursor + limit : null; - - return { - list: paged, - nextCursor, - totalCount, - }; -} diff --git a/src/lib/wineApi.ts b/src/lib/wineApi.ts new file mode 100644 index 00000000..69988bdc --- /dev/null +++ b/src/lib/wineApi.ts @@ -0,0 +1,55 @@ +import axios from 'axios'; + +import { GetWinesParams, GetWinesResponse, RecommendedWineResponse } from '@/types/wineListType'; + +const API_BASE_URL = 'https://winereview-api.vercel.app'; + +interface GetRecommendedWinesParams { + teamId: string; + limit: number; +} + +export const getWines = async ({ + teamId, + cursor, + limit, + filters, +}: GetWinesParams): Promise => { + try { + const params = new URLSearchParams(); + if (cursor !== undefined && cursor !== null) { + params.append('cursor', String(cursor)); + } + if (limit !== undefined) { + params.append('limit', String(limit)); + } + + // 필터 객체를 순회하며 유효한 필터만 파라미터에 추가 + if (filters) { + Object.entries(filters).forEach(([key, value]) => { + if (value !== null && value !== undefined && value !== '') { + params.append(key, String(value)); + } + }); + } + + const response = await axios.get(`${API_BASE_URL}/${teamId}/wines?${params.toString()}`); + return response.data; + } catch (error) { + console.error('Failed to fetch wines:', error); + throw error; + } +}; + +export const getRecommendedWines = async ({ + teamId, + limit, +}: GetRecommendedWinesParams): Promise => { + try { + const response = await axios.get(`${API_BASE_URL}/${teamId}/wines/recommended?limit=${limit}`); + return response.data; + } catch (error) { + console.error('Failed to fetch recommended wines:', error); + throw error; + } +}; diff --git a/src/lib/winelist.ts b/src/lib/winelist.ts deleted file mode 100644 index f63b03f0..00000000 --- a/src/lib/winelist.ts +++ /dev/null @@ -1,75 +0,0 @@ -const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL; -const TEAM_ID = process.env.NEXT_PUBLIC_TEAM; - -export interface Wine { - id: number; - name: string; - region: string; - image: string; - price: number; - type: 'RED' | 'WHITE' | 'SPARKLING'; - avgRating: number; - reviewCount: number; - recentReview: { - id: number; - content: string; - createdAt: string; - updatedAt: string; - } | null; - userId: number; -} - -export interface RecommendedWine { - id: number; - name: string; - region: string; - image: string; - price: number; - type: 'RED' | 'WHITE' | 'SPARKLING'; - avgRating: number; -} - -export interface GetWinesParams { - cursor: number; - limit: number; - filters?: { - type?: string; - minPrice?: number; - maxPrice?: number; - rating?: number; - searchTerm?: string; - }; -} - -export interface WinesResponse { - list: Wine[]; - totalCount: number; - nextCursor: number | null; -} -/* 추천 와인 API */ -export async function getRecommendedWines(): Promise { - const res = await fetch(`${BASE_URL}/${TEAM_ID}/wines/recommended`); - if (!res.ok) throw new Error('추천 와인 불러오기 실패'); - return res.json(); -} - -/* 전체 와인 목록 API (필터 + 페이징) */ -export async function getWines({ - cursor, - limit, - filters = {}, -}: GetWinesParams): Promise { - const query = new URLSearchParams({ - cursor: String(cursor), - limit: String(limit), - ...(filters.type && { type: filters.type }), - ...(filters.minPrice && { minPrice: String(filters.minPrice) }), - ...(filters.maxPrice && { maxPrice: String(filters.maxPrice) }), - ...(filters.rating && { rating: String(filters.rating) }), - ...(filters.searchTerm && { searchTerm: filters.searchTerm }), - }); - - const res = await fetch(`${BASE_URL}/${TEAM_ID}/wines?${query.toString()}`); - if (!res.ok) throw new Error('와인 목록 불러오기 실패'); - return res.json(); -} diff --git a/src/stores/wineAddStore.ts b/src/stores/wineAddStore.ts index 108f8326..1a0373ea 100644 --- a/src/stores/wineAddStore.ts +++ b/src/stores/wineAddStore.ts @@ -29,215 +29,6 @@ const useWineAddStore = create((set) => ({ review: 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', }, - { - id: 2, - name: 'Palazzo della Torre 2017', - region: 'Western Cape, South Africa', - image: '/images/image3.svg', - price: 64900, - rating: 4.6, - type: 'Sparkling', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 3, - name: 'Sentinel Carbernet Sauvignon 2016', - region: 'Western Cape, South Africa', - image: '/images/image2.svg', - price: 59900, - rating: 3.6, - type: 'White', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 4, - name: 'Palazzo della Torre 2017', - region: 'Western Cape, South Africa', - image: '/images/image4.svg', - price: 74000, - rating: 2.1, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 5, - name: 'Sentinel Carbernet Sauvignon 2016', - region: 'Western Cape, South Africa', - image: '/images/image1.svg', - price: 64900, - rating: 4.5, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 6, - name: 'Palazzo della Torre 2017', - region: 'Western Cape, South Africa', - image: '/images/image3.svg', - price: 64900, - rating: 4.6, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 7, - name: 'Sentinel Carbernet Sauvignon 2016', - region: 'Western Cape, South Africa', - image: '/images/image2.svg', - price: 59900, - rating: 3.6, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 8, - name: 'Palazzo della Torre 2017', - region: 'Western Cape, South Africa', - image: '/images/image4.svg', - price: 74000, - rating: 2.1, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 9, - name: 'Sentinel Carbernet Sauvignon 2016', - region: 'Western Cape, South Africa', - image: '/images/image1.svg', - price: 64900, - rating: 4.5, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 10, - name: 'Palazzo della Torre 2017', - region: 'Western Cape, South Africa', - image: '/images/image3.svg', - price: 64900, - rating: 4.6, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 11, - name: 'Sentinel Carbernet Sauvignon 2016', - region: 'Western Cape, South Africa', - image: '/images/image2.svg', - price: 59900, - rating: 3.6, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 12, - name: 'Palazzo della Torre 2017', - region: 'Western Cape, South Africa', - image: '/images/image4.svg', - price: 74000, - rating: 2.1, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 13, - name: 'Sentinel Carbernet Sauvignon 2016', - region: 'Western Cape, South Africa', - image: '/images/image1.svg', - price: 64900, - rating: 4.5, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 14, - name: 'Palazzo della Torre 2017', - region: 'Western Cape, South Africa', - image: '/images/image3.svg', - price: 64900, - rating: 4.6, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 15, - name: 'Sentinel Carbernet Sauvignon 2016', - region: 'Western Cape, South Africa', - image: '/images/image2.svg', - price: 59900, - rating: 3.6, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 16, - name: 'Palazzo della Torre 2017', - region: 'Western Cape, South Africa', - image: '/images/image4.svg', - price: 74000, - rating: 2.1, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 17, - name: 'Sentinel Carbernet Sauvignon 2016', - region: 'Western Cape, South Africa', - image: '/images/image1.svg', - price: 64900, - rating: 4.5, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 18, - name: 'Palazzo della Torre 2017', - region: 'Western Cape, South Africa', - image: '/images/image3.svg', - price: 64900, - rating: 4.6, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 19, - name: 'Sentinel Carbernet Sauvignon 2016', - region: 'Western Cape, South Africa', - image: '/images/image2.svg', - price: 59900, - rating: 3.6, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, - { - id: 20, - name: 'Palazzo della Torre 2017', - region: 'Western Cape, South Africa', - image: '/images/image4.svg', - price: 74000, - rating: 2.1, - type: 'Red', - review: - 'Cherry, cocoa, vanilla and clove - beautiful red fruit driven Amarone. Low acidity and medium tannins. Nice long velvety finish.', - }, ], addWine: (newWine) => diff --git a/src/types/wineListType.ts b/src/types/wineListType.ts new file mode 100644 index 00000000..164cbf29 --- /dev/null +++ b/src/types/wineListType.ts @@ -0,0 +1,48 @@ +export interface Wine { + id: number; + name: string; + region: string; + image: string; + price: number; + type: 'RED' | 'WHITE' | 'SPARKLING'; + avgRating: number; + reviewCount: number; + recentReview: { + id: number; + content: string; + createdAt: string; + updatedAt: string; + } | null; + userId: number; +} + +export interface RecommendedWine { + id: number; + name: string; + region: string; + image: string; + price: number; + type: 'RED' | 'WHITE' | 'SPARKLING'; + avgRating: number; +} + +export interface GetWinesParams { + teamId: string; + cursor?: number | null; + limit?: number; + filters: { + type?: string; + minPrice?: number; + maxPrice?: number; + rating?: number; + searchTerm?: string; + }; +} + +export interface GetWinesResponse { + list: Wine[]; + totalCount: number; + nextCursor: number | null; +} + +export type RecommendedWineResponse = RecommendedWine[]; From fca7b68a3b8acc0fa4d577433f87de4b828dfc4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B4=80=ED=98=84=5CLuganic?= Date: Sat, 2 Aug 2025 10:12:10 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20=ED=95=84=ED=84=B0=EB=A7=81=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/Filter/WineTypeFilter.tsx | 10 +++++----- src/hooks/useWineListQuery.ts | 20 ++++++------------- src/stores/filterStore.ts | 2 +- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/components/common/Filter/WineTypeFilter.tsx b/src/components/common/Filter/WineTypeFilter.tsx index a1e56256..6d895ef9 100644 --- a/src/components/common/Filter/WineTypeFilter.tsx +++ b/src/components/common/Filter/WineTypeFilter.tsx @@ -19,7 +19,7 @@ const WineTypeFilter = ({ }: WineTypeFilterProps) => { const { type, setType, minPrice, maxPrice, setPriceRange, rating, setRating } = useFilterStore(); - const wineTypeOptions: WineType[] = ['Red', 'White', 'Sparkling']; + const wineTypeOptions: WineType[] = ['RED', 'WHITE', 'SPARKLING']; const priceRange: [number, number] = [minPrice, maxPrice]; const borderClass = 'border-b border-gray-100'; @@ -65,19 +65,19 @@ const WineTypeFilter = ({
- +
- +
- +
- +
diff --git a/src/hooks/useWineListQuery.ts b/src/hooks/useWineListQuery.ts index 4554d1e4..e0aa51c1 100644 --- a/src/hooks/useWineListQuery.ts +++ b/src/hooks/useWineListQuery.ts @@ -8,32 +8,24 @@ import { GetWinesResponse } from '@/types/wineListType'; const PAGE_LIMIT = 8; const TEAM_ID = process.env.NEXT_PUBLIC_TEAM; -const getMinRatingFromFilter = (rating: string): number | undefined => { - const ratingMap: Record = { - '4.5 - 5.0': 4.5, - '4.0 - 4.5': 4.0, - '3.5 - 4.0': 3.5, - '3.0 - 3.5': 3.0, - }; - return ratingMap[rating]; -}; - export function useWineListQuery() { + const type = useFilterStore((state) => state.type); const minPrice = useFilterStore((state) => state.minPrice); const maxPrice = useFilterStore((state) => state.maxPrice); const rating = useFilterStore((state) => state.rating); const searchTerm = useWineSearchKeywordStore((state) => state.searchTerm); - const minRating = getMinRatingFromFilter(rating); + const apiRating = rating === 'all' ? undefined : Number(rating); return useInfiniteQuery>({ - queryKey: ['wines', { minPrice, maxPrice, rating: minRating, searchTerm }], + queryKey: ['wines', { type, minPrice, maxPrice, rating: apiRating, name: searchTerm }], queryFn: ({ pageParam = 0 }) => { const filters = { + type: type.toUpperCase(), minPrice, maxPrice, - rating: minRating, - searchTerm, + rating: apiRating, + name: searchTerm, }; const filteredParams = Object.fromEntries( diff --git a/src/stores/filterStore.ts b/src/stores/filterStore.ts index a59a8fd5..e5122aed 100644 --- a/src/stores/filterStore.ts +++ b/src/stores/filterStore.ts @@ -1,6 +1,6 @@ import { create } from 'zustand'; -export type WineType = 'Red' | 'White' | 'Sparkling'; +export type WineType = 'RED' | 'WHITE' | 'SPARKLING'; type FilterState = { type: WineType; From 696206a151317955fa1b263749d1a19217ccab23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B4=80=ED=98=84=5CLuganic?= Date: Sat, 2 Aug 2025 16:53:37 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modal/WineModal/AddWineModal.tsx | 31 +++---------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/src/components/Modal/WineModal/AddWineModal.tsx b/src/components/Modal/WineModal/AddWineModal.tsx index 9e6eb032..4d686f06 100644 --- a/src/components/Modal/WineModal/AddWineModal.tsx +++ b/src/components/Modal/WineModal/AddWineModal.tsx @@ -13,7 +13,6 @@ import SelectDropdown from '../../common/dropdown/SelectDropdown'; import Input from '../../common/Input'; import BasicModal from '../../common/Modal/BasicModal'; import { Button } from '../../ui/button'; - interface WineForm { wineName: string; winePrice: number; @@ -21,23 +20,19 @@ interface WineForm { wineImage: FileList; wineType: string; } - interface AddWineModalProps { showRegisterModal: boolean; setShowRegisterModal: (state: boolean) => void; } - const AddWineModal = ({ showRegisterModal, setShowRegisterModal }: AddWineModalProps) => { const [category, setCategory] = useState(''); const fileInputRef = useRef(null); const [previewImage, setPreviewImage] = useState(null); const queryClient = useQueryClient(); const isDesktop = useMediaQuery('(min-width: 640px)'); - const triggerFileSelect = () => { fileInputRef.current?.click(); }; - const handleImageChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { @@ -47,7 +42,6 @@ const AddWineModal = ({ showRegisterModal, setShowRegisterModal }: AddWineModalP setPreviewImage(null); } }; - const { register, handleSubmit, @@ -59,7 +53,6 @@ const AddWineModal = ({ showRegisterModal, setShowRegisterModal }: AddWineModalP } = useForm({ mode: 'onBlur', }); - const resetForm = () => { reset({ wineName: '', @@ -72,10 +65,11 @@ const AddWineModal = ({ showRegisterModal, setShowRegisterModal }: AddWineModalP setPreviewImage(null); if (fileInputRef.current) fileInputRef.current.value = ''; }; - const handlePostWine = async (form: WineForm) => { const file = form.wineImage[0]; - const imageUrl = await uploadImage(file); + const newFileName = file.name.replace(/\s/g, '-'); + const newFile = new File([file], newFileName, { type: file.type }); + const imageUrl = await uploadImage(newFile); const requestData: PostWineRequest = { name: form.wineName, region: form.wineOrigin, @@ -85,7 +79,6 @@ const AddWineModal = ({ showRegisterModal, setShowRegisterModal }: AddWineModalP }; return postWine(requestData); }; - const postWineMutation = useMutation({ mutationFn: handlePostWine, onSuccess: () => { @@ -98,20 +91,15 @@ const AddWineModal = ({ showRegisterModal, setShowRegisterModal }: AddWineModalP console.log('와인 등록 실패', error); }, }); - const onSubmit = async (form: WineForm) => { postWineMutation.mutate(form); }; - const categoryOptions = [ { label: 'Red', value: 'Red' }, { label: 'White', value: 'White' }, { label: 'Sparkling', value: 'Sparkling' }, ]; - const selectedCategoryLabel = categoryOptions.find((opt) => opt.value === category)?.label; - - //모달창 끄면 리셋되게 const closeModalReset = (isOpen: boolean) => { setShowRegisterModal(isOpen); if (!isOpen) { @@ -120,11 +108,8 @@ const AddWineModal = ({ showRegisterModal, setShowRegisterModal }: AddWineModalP }, 50); } }; - //// - const renderForm = () => (
- {/* 이름 */}

와인 이름

@@ -140,7 +125,6 @@ const AddWineModal = ({ showRegisterModal, setShowRegisterModal }: AddWineModalP placeholder='와인 이름 입력' className='custom-text-md-regular md:custom-text-lg-regular' /> - {/* 가격 */}

가격

- {/* 원산지 */}

원산지

- {/* 타입 */}

타입

{errors.wineType.message}

- )}{' '} + )} - {/* 사진 */}

와인 사진

{previewImage ? ( - // eslint-disable-next-line @next/next/no-img-element 미리보기 ) : (
@@ -245,7 +225,6 @@ const AddWineModal = ({ showRegisterModal, setShowRegisterModal }: AddWineModalP
); - const renderButton = (
); - return isDesktop ? ( ); }; - export default AddWineModal;