From 248f26050f95a04626f8225e44ddbfa16cd972fd Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Tue, 14 Oct 2025 17:19:21 +0900 Subject: [PATCH 01/11] =?UTF-8?q?[refactor]=20=EB=AC=B4=ED=95=9C=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20tanstack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/recipe/api/fetchRecipe.ts | 186 ++++++++++++++++++ .../recipe/components/main/Accordion.tsx | 4 +- .../recipe/components/main/CocktailFilter.tsx | 8 +- .../recipe/components/main/CocktailList.tsx | 29 +-- .../components/main/CocktailSearchBar.tsx | 22 ++- .../recipe/components/main/Cocktails.tsx | 98 +++------ 6 files changed, 242 insertions(+), 105 deletions(-) create mode 100644 src/domains/recipe/api/fetchRecipe.ts diff --git a/src/domains/recipe/api/fetchRecipe.ts b/src/domains/recipe/api/fetchRecipe.ts new file mode 100644 index 0000000..2156c5a --- /dev/null +++ b/src/domains/recipe/api/fetchRecipe.ts @@ -0,0 +1,186 @@ +import { getApi } from "@/app/api/config/appConfig"; +import { useAuthStore } from "@/domains/shared/store/auth"; +import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; +import { Cocktail } from "../types/types"; + + + +interface CocktailResponse { + data: Cocktail[] +} + +interface KeepResponse{ + data: Array<{cocktailId:number}> +} + +interface SearchFilters { + keyword?: string; + alcoholStrengths: string[]; + cocktailTypes: string[]; + alcoholBaseTypes: string[]; +} + +type Sort = 'recent' | 'keeps' | 'comments' + +interface CocktailFilter extends SearchFilters{ + sortBy?:Sort +} + + +const fetchKeep = async (): Promise> => { + const res = await fetch(`${getApi}/me/bar`, { + method: 'GET', + credentials: 'include', + }); + + if (!res.ok) return new Set(); + + const json: KeepResponse = await res.json(); + const myKeep = json.data ?? []; + return new Set(myKeep.map((v: { cocktailId: number }) => v.cocktailId)); +}; + + + const fetchRecipe = async ( + lastId: number | null, + size: number, + sortBy?: Sort + ): Promise => { + + const url = new URL(`${getApi}/cocktails`) + url.searchParams.set('SIZE',String(size)) + url.searchParams.set('lastId', String(lastId)) + url.searchParams.set('lastValue', String(lastId)); + + + if (sortBy) { + url.searchParams.set('sortBy',sortBy) + } + + + const res = await fetch(url.toString(), { + method:'GET' + }) + + if (!res.ok) throw new Error('레시피 패치 실패') + + const json:CocktailResponse = await res.json() + + return json.data ?? [] + } + + + +const searchCocktails = async (filters: SearchFilters): Promise => { + + const body = { + keyword: filters.keyword?.trim() ?? '', + alcoholStrengths: filters.alcoholStrengths, + cocktailTypes: filters.cocktailTypes, + alcoholBaseTypes: filters.alcoholBaseTypes, + page: 0, + size: 20, + }; + + const res = await fetch(`${getApi}/cocktails/search`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body:JSON.stringify(body) + }) + + if(!res.ok) throw new Error('검색 POST 실패') + + const json:CocktailResponse = await res.json() + return json.data ?? [] +} + + +const hasActiveFilters = (filters: SearchFilters): boolean => { + return !!( + filters.keyword?.trim() || + filters.alcoholStrengths.length > 0 || + filters.cocktailTypes.length > 0 || + filters.alcoholBaseTypes.length > 0 + ); +}; + + +export const useCocktailsInfiniteQuery = ( + size: number = 20, + sortBy?: Sort +) => { + const user = useAuthStore((state) => state.user); + + return useInfiniteQuery({ + queryKey: ['cocktails','infinite',sortBy, size, user?.id], + queryFn: async ({ pageParam }) => { + const cocktails = await fetchRecipe(pageParam,size,sortBy) + + if (user) { + const keepId = await fetchKeep() + return cocktails.map((item) => ({ + ...item, + isKeep: keepId.has(item.cocktailId) + })) + } + + return cocktails + }, + getNextPageParam: (lastpage) => { + if(lastpage.length < size) return undefined + return lastpage[lastpage.length - 1]?.cocktailId ?? undefined + }, + initialPageParam: 345 + }) +} + + +export const useCocktailsSearchQuery = (filters:SearchFilters) => { + const user = useAuthStore(state => state.user) + const isActive = hasActiveFilters(filters) + + return useQuery({ + queryKey: ['cocktails', 'search', filters, user?.id], + queryFn: async () => { + const cocktails = await searchCocktails(filters) + if (user && cocktails.length > 0) { + const keepId = await fetchKeep() + return cocktails.map((item) => ({ + ...item, + isKeep: keepId.has(item.cocktailId) + })) + } + return cocktails + }, + enabled: isActive, + refetchOnMount:false, + }) +} + +export const useCocktails = (filters: CocktailFilter, infiniteScrollSize: number = 20) => { + const isSearchMode = hasActiveFilters(filters); + const infiniteQuery = useCocktailsInfiniteQuery(infiniteScrollSize, filters.sortBy); + const searchQuery = useCocktailsSearchQuery(filters); + + if (isSearchMode) { + return { + data: searchQuery.data ?? [], + noResults: searchQuery.data?.length === 0, + isSearchMode: true, + fetchNextPage: undefined, + hasNextPage: false, + isFetchingNextPage: false, + }; + } + + const allCocktails = infiniteQuery.data?.pages.flatMap((page) => page) ?? []; + + return { + data: allCocktails, + noResults: false, + isSearchMode: false, + fetchNextPage: infiniteQuery.fetchNextPage, + hasNextPage: infiniteQuery.hasNextPage, + isFetchingNextPage: infiniteQuery.isFetchingNextPage, + }; +}; diff --git a/src/domains/recipe/components/main/Accordion.tsx b/src/domains/recipe/components/main/Accordion.tsx index 771c5a1..1b35825 100644 --- a/src/domains/recipe/components/main/Accordion.tsx +++ b/src/domains/recipe/components/main/Accordion.tsx @@ -93,13 +93,13 @@ function Accordion({ setAlcoholBaseTypes, setCocktailTypes, setAlcoholStrengths }; // URL 파라미터에서 현재 선택된 값 가져오기 아코디언 UI에 적용 - const currentValues = useMemo(() => { + const currentValues = () => { return { abv: getDisplayValue('abv', searchParams.get('abv')), base: getDisplayValue('base', searchParams.get('base')), glass: getDisplayValue('glass', searchParams.get('glass')), }; - }, [searchParams]); + } const handleSelect = (id: string, value: string) => { const optionGroup = SELECT_OPTIONS.find((opt) => opt.id === id); diff --git a/src/domains/recipe/components/main/CocktailFilter.tsx b/src/domains/recipe/components/main/CocktailFilter.tsx index 589823c..0e2fc87 100644 --- a/src/domains/recipe/components/main/CocktailFilter.tsx +++ b/src/domains/recipe/components/main/CocktailFilter.tsx @@ -5,11 +5,11 @@ import { Dispatch, SetStateAction } from 'react'; import { Cocktail } from '../../types/types'; interface Props { - cocktailsEA: string; - setData: Dispatch>; + cocktailsEA: number; + } -function CocktailFilter({ cocktailsEA, setData }: Props) { +function CocktailFilter({ cocktailsEA }: Props) { const sortMap = { 최신순: 'recent', 인기순: 'keeps', @@ -23,7 +23,7 @@ function CocktailFilter({ cocktailsEA, setData }: Props) { try { const res = await fetch(`${getApi}/cocktails`); const json = await res.json(); - setData(json.data); + } catch { console.error(); console.log(selectTitle); diff --git a/src/domains/recipe/components/main/CocktailList.tsx b/src/domains/recipe/components/main/CocktailList.tsx index 0b2f7e6..daa519a 100644 --- a/src/domains/recipe/components/main/CocktailList.tsx +++ b/src/domains/recipe/components/main/CocktailList.tsx @@ -1,35 +1,15 @@ 'use client'; -import { useRef } from 'react'; + import Link from 'next/link'; -import { useIntersectionObserver } from '@/domains/shared/hook/useIntersectionObserver'; import { Cocktail } from '../../types/types'; import CocktailCard from '@/domains/shared/components/cocktail-card/CocktailCard'; -import { useScrollRestore } from '@/domains/shared/hook/useMemoScroll'; interface Props { cocktails: Cocktail[]; - RecipeFetch?: (cursor?: string | undefined) => Promise; - hasNextPage: boolean; - lastId: number | null; } -function CocktailList({ cocktails, RecipeFetch, hasNextPage, lastId }: Props) { - const cocktailRef = useRef(null); - const onIntersect: IntersectionObserverCallback = ([entry]) => { - if (!RecipeFetch) return; - if (!lastId) return; - if (entry.isIntersecting && lastId > 1) { - RecipeFetch(); - } - }; - - useIntersectionObserver(cocktailRef, onIntersect, hasNextPage); - const saveScroll = useScrollRestore({ - lastId, - fetchData: RecipeFetch!, - hasNextPage, - currentDataLength: cocktails.length, - }); +function CocktailList({ cocktails}: Props) { + return (
    +
  • ) )} -
); } diff --git a/src/domains/recipe/components/main/CocktailSearchBar.tsx b/src/domains/recipe/components/main/CocktailSearchBar.tsx index 5850054..b8dfb49 100644 --- a/src/domains/recipe/components/main/CocktailSearchBar.tsx +++ b/src/domains/recipe/components/main/CocktailSearchBar.tsx @@ -1,17 +1,29 @@ import Input from '@/shared/components/Input-box/Input'; +import { debounce } from '@/shared/utills/debounce'; +import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react'; interface Props { - value: string; - onChange: (v: string) => void; + keyword: string; + setKeyword: Dispatch>; } -function CocktailSearchBar({ value, onChange }: Props) { +function CocktailSearchBar({ keyword, setKeyword }: Props) { + const[input,setInput] = useState('') + const debounceKeyword = useMemo(() => debounce((v: string) => { setKeyword(v)},300),[setKeyword]) + + useEffect(() => { + debounceKeyword(keyword) + }, [keyword, debounceKeyword]) + return ( onChange(e.target.value)} + value={input} + onChange={(e) => { + setKeyword(e.target.value) + setInput(e.target.value) + }} variant="search" className="w-full md:max-w-80" /> diff --git a/src/domains/recipe/components/main/Cocktails.tsx b/src/domains/recipe/components/main/Cocktails.tsx index a63dc93..3adf51d 100644 --- a/src/domains/recipe/components/main/Cocktails.tsx +++ b/src/domains/recipe/components/main/Cocktails.tsx @@ -3,74 +3,42 @@ import { useEffect, useState } from 'react'; import CocktailFilter from './CocktailFilter'; import CocktailList from './CocktailList'; -import { Cocktail } from '../../types/types'; import Accordion from './Accordion'; -import { RecipeFetch } from '../../api/RecipeFetch'; import CocktailSearchBar from './CocktailSearchBar'; -import useSearchControl from '../../hook/useSearchControl'; -import CocktailSearch from '../../api/CocktailSearch'; -import { useAuthStore } from '@/domains/shared/store/auth'; +import { useCocktails} from '../../api/fetchRecipe'; +import { useInView } from 'react-intersection-observer'; -function Cocktails() { - const user = useAuthStore((state) => state.user); - - const [data, setData] = useState([]); - const [lastId, setLastId] = useState(null); - const [hasNextPage, setHasNextPage] = useState(true); - const { inputValue, keyword, isSearching, onInputChange, noResults, setNoResults } = - useSearchControl({ delay: 300, storageKey: 'cocktails_scoll_state' }); - const { fetchData } = RecipeFetch({ setData, lastId, setLastId, hasNextPage, setHasNextPage }); +function Cocktails() { +const [keyword,setKeyword] = useState('') +const [alcoholStrengths,setAlcoholStrengths] = useState([]) +const [alcoholBaseTypes,setAlcoholBaseTypes] = useState([]) +const [cocktailTypes,setCocktailTypes] = useState([]) const { - searchApi, - setAlcoholBaseTypes, - setAlcoholStrengths, - setCocktailTypes, + data, + fetchNextPage, + hasNextPage, + noResults, + isSearchMode, + isFetchingNextPage + } = useCocktails({ + keyword, alcoholBaseTypes, - cocktailTypes, alcoholStrengths, - } = CocktailSearch({ - setData, - setNoResults, - }); - - const countLabel = isSearching - ? hasNextPage - ? `검색결과 현재 ${data.length}+` - : `검색결과 총 ${data.length}` - : hasNextPage - ? `전체 ${data.length}+` - : `전체 ${data.length}`; + cocktailTypes + },20) - // 초기 로드 시 검색어가 있으면 검색 실행 - // useEffect(() => { - // const readyForFirstLoad = !isSearching && hasNextPage && lastId == null && data.length === 0; + const { ref, inView } = useInView({ + threshold:0.1 + }) - // if (readyForFirstLoad) { - // fetchData(); - // } - // }, [hasNextPage, lastId]); - - // 검색어 변경 시 useEffect(() => { - if (isSearching && keyword.trim()) { - setLastId(null); - setHasNextPage(false); - searchApi(keyword.trim()); - } else if (!isSearching) { - // 검색어를 지웠을 때만 초기화 - setData([]); - setLastId(null); - setHasNextPage(true); - } - }, [keyword, isSearching, alcoholBaseTypes, alcoholStrengths, cocktailTypes]); + if (!isSearchMode && inView && hasNextPage) { + fetchNextPage?.() + } +},[inView,hasNextPage,fetchNextPage,isSearchMode]) - // 일반 fetch - useEffect(() => { - if (isSearching) return; - fetchData(); - }, [isSearching, alcoholBaseTypes, alcoholStrengths, cocktailTypes]); return (
@@ -80,23 +48,15 @@ function Cocktails() { setAlcoholStrengths={setAlcoholStrengths} setCocktailTypes={setCocktailTypes} /> - + - - + +
- {isSearching && noResults ? ( -
검색결과가 없습니다.
- ) : ( - - )} +
+
); } From e7b7e3b6d13b82d29e51dfb4cb9c6340395ef59e Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Wed, 15 Oct 2025 00:43:58 +0900 Subject: [PATCH 02/11] =?UTF-8?q?[refactor]=20=EB=A0=88=EC=8B=9C=ED=94=BC?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/recipe/api/CocktailSearch.tsx | 58 ------ src/domains/recipe/api/RecipeFetch.tsx | 74 ------- src/domains/recipe/api/fetchRecipeComment.ts | 57 ----- src/domains/recipe/api/useRecipeComment.ts | 195 ++++++++++-------- src/domains/recipe/api/useRecipeDetails.ts | 43 ++++ .../recipe/components/details/DetailMain.tsx | 66 ++---- .../components/details/DetailsHeader.tsx | 3 +- .../components/details/RecipeComment.tsx | 72 +++---- .../recipe/components/main/CocktailFilter.tsx | 2 +- .../recipe/components/main/CocktailList.tsx | 4 +- .../components/main/CocktailSearchBar.tsx | 19 +- .../recipe/components/main/Cocktails.tsx | 21 +- src/domains/recipe/types/types.ts | 2 +- .../components/cocktail-card/CocktailCard.tsx | 1 - 14 files changed, 207 insertions(+), 410 deletions(-) delete mode 100644 src/domains/recipe/api/CocktailSearch.tsx delete mode 100644 src/domains/recipe/api/RecipeFetch.tsx delete mode 100644 src/domains/recipe/api/fetchRecipeComment.ts create mode 100644 src/domains/recipe/api/useRecipeDetails.ts diff --git a/src/domains/recipe/api/CocktailSearch.tsx b/src/domains/recipe/api/CocktailSearch.tsx deleted file mode 100644 index aacd03f..0000000 --- a/src/domains/recipe/api/CocktailSearch.tsx +++ /dev/null @@ -1,58 +0,0 @@ -'use client'; -import { getApi } from '@/app/api/config/appConfig'; -import { Cocktail } from '../types/types'; -import { Dispatch, SetStateAction, useEffect, useState } from 'react'; - -interface Props { - setData: Dispatch>; - setNoResults: Dispatch>; -} - -function CocktailSearch({ setData, setNoResults }: Props) { - const [alcoholStrengths, setAlcoholStrengths] = useState([]); - const [cocktailTypes, setCocktailTypes] = useState([]); - const [alcoholBaseTypes, setAlcoholBaseTypes] = useState([]); - - const searchApi = async (v?: string) => { - const keyword = v?.trim() ?? ''; - const body = { - keyword, - alcoholStrengths, - cocktailTypes, - alcoholBaseTypes, - page: 0, - size: 100, - }; - - if (!keyword && !alcoholStrengths.length && !cocktailTypes.length && !alcoholBaseTypes.length) { - setData([]); - setNoResults(false); - return null; - } - - const res = await fetch(`${getApi}/cocktails/search`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - const json = await res.json(); - - setData(json.data); - setNoResults(json.data.length === 0); - }; - - useEffect(() => { - searchApi(); - }, [alcoholStrengths, cocktailTypes, alcoholBaseTypes]); - - return { - searchApi, - setAlcoholBaseTypes, - setAlcoholStrengths, - setCocktailTypes, - alcoholBaseTypes, - alcoholStrengths, - cocktailTypes, - }; -} -export default CocktailSearch; diff --git a/src/domains/recipe/api/RecipeFetch.tsx b/src/domains/recipe/api/RecipeFetch.tsx deleted file mode 100644 index 39dbe35..0000000 --- a/src/domains/recipe/api/RecipeFetch.tsx +++ /dev/null @@ -1,74 +0,0 @@ -'use client'; - -import { getApi } from '@/app/api/config/appConfig'; -import { Cocktail } from '../types/types'; -import { Dispatch, SetStateAction, useCallback } from 'react'; -import { useAuthStore } from '@/domains/shared/store/auth'; - -interface Props { - setData: React.Dispatch>; - lastId: number | null; - setLastId: Dispatch>; - hasNextPage: boolean; - setHasNextPage: Dispatch>; - SIZE?: number; -} - -// api/cocktais fetch용 -export const RecipeFetch = ({ - setData, - lastId, - setLastId, - hasNextPage, - setHasNextPage, - SIZE = 20, -}: Props) => { - const user = useAuthStore((state) => state.user); - const fetchData = useCallback(async () => { - // 쿼리파라미터에 값 넣기 - if (!hasNextPage) return; - const url = new URL(`${getApi}/cocktails`); - url.searchParams.set('size', String(SIZE)); - if (typeof lastId === 'number') { - url.searchParams.set('lastId', String(lastId)); - } - url.searchParams.set('LastValue', String(lastId)); - - const recipeRes = await fetch(url.toString(), { - method: 'GET', - }); - if (!recipeRes.ok) throw new Error('데이터 요청 실패'); - const recipeJson = await recipeRes.json(); - const list: Cocktail[] = recipeJson.data ?? []; - - if (user) { - const keepRes = await fetch(`${getApi}/me/bar`, { - method: 'GET', - credentials: 'include', - }); - const bars = keepRes.ok ? ((await keepRes.json()).data ?? []) : []; - const favoriteIds = new Set(bars.map((m: { cocktailId: number }) => m.cocktailId)); - const merged = list.map((item) => ({ - ...item, - isFavorited: favoriteIds.has(item.cocktailId), - })); - setData((prev) => - Array.from( - new Map([...prev, ...merged].map((i) => [i.cocktailId, i])).values() - ) - ); - } else { - setData((prev) => - Array.from( - new Map([...prev, ...list].map((i) => [i.cocktailId, i])).values() - ) - ); - } - - if (list.length > 0) { - setLastId(list[list.length - 1].cocktailId); - } - setHasNextPage(list.length === SIZE); - }, [hasNextPage, lastId, setData, setLastId, setHasNextPage, SIZE]); - return { fetchData }; -}; diff --git a/src/domains/recipe/api/fetchRecipeComment.ts b/src/domains/recipe/api/fetchRecipeComment.ts deleted file mode 100644 index 80f758a..0000000 --- a/src/domains/recipe/api/fetchRecipeComment.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { getApi } from '@/app/api/config/appConfig'; -import { CommentType } from '@/domains/community/types/post'; - -export const getRecipeComment = async (cocktailId: number): Promise => { - try { - const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments`, { - method: 'GET', - cache: 'no-store', // 캐시 비활성화 - }); - const data = await res.json(); - - //삭제된 댓글은 제외 - const filteredComments = data.data.filter( - (comment: CommentType) => comment.status !== 'DELETED' - ); - - return filteredComments; - } catch (err) { - console.error('해당 글의 댓글 조회 실패', err); - return null; - } -}; - -export async function updateComment( - postId: number, - commentId: number, - content: string -): Promise { - console.log(postId, typeof postId); - const response = await fetch(`${getApi}/cocktails/${postId}/comments/${commentId}`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - body: JSON.stringify({ content }), - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error('서버 응답 에러:', errorText); - throw new Error(`댓글 수정 실패: ${response.status}`); - } -} - -export async function deleteRecipeComment(cocktailId: number, commentId: number): Promise { - const response = await fetch(`${getApi}/cocktails/${cocktailId}/comments/${commentId}`, { - method: 'DELETE', - credentials: 'include', - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error('서버 응답 에러:', errorText); - throw new Error(`댓글 수정 실패: ${response.status}`); - } -} diff --git a/src/domains/recipe/api/useRecipeComment.ts b/src/domains/recipe/api/useRecipeComment.ts index 3fa8e6d..b9392b5 100644 --- a/src/domains/recipe/api/useRecipeComment.ts +++ b/src/domains/recipe/api/useRecipeComment.ts @@ -1,102 +1,115 @@ -import { useState, useEffect, useCallback } from 'react'; import { getApi } from '@/app/api/config/appConfig'; -import { User } from '@/domains/shared/store/auth'; import { CommentType } from '@/domains/community/types/post'; -import { deleteRecipeComment, getRecipeComment, updateComment } from './fetchRecipeComment'; +import { useAuthStore } from '@/domains/shared/store/auth'; import { useToast } from '@/shared/hook/useToast'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -export function useRecipeComments(cocktailId: number, user: User | null) { - const [comments, setComments] = useState(null); - const [isEnd, setIsEnd] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [deleteTarget, setDeleteTarget] = useState<{ - commentId: number; - cocktailId: number; - } | null>(null); - const { toastError } = useToast(); - - const fetchData = useCallback(async () => { - const data = await getRecipeComment(cocktailId); - if (!data) return; - setComments(data); - setIsEnd(false); - }, [cocktailId]); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - const handleUpdateComment = async (commentId: number, content: string) => { - if (!user) { - toastError('로그인이 필요합니다'); - return; - } - try { - await updateComment(cocktailId, commentId, content); - setComments((prev) => - prev - ? prev.map((comment) => - comment.commentId === commentId ? { ...comment, content } : comment - ) - : prev - ); - } catch (err) { - console.error(err); - toastError('댓글 수정 중 오류가 발생했습니다.'); - } - }; - const handleAskDeleteComment = (commentId: number) => { - setDeleteTarget({ commentId, cocktailId }); - }; +export const postRecipeComment = async (cocktailId: number, content: string) => { + const body = { + cocktailId, + content, + status:'PUBLIC' + } - const handleConfirmDelete = async () => { - if (!user) { - toastError('로그인이 필요합니다'); - return; - } - if (!deleteTarget) return; - - try { - await deleteRecipeComment(deleteTarget.cocktailId, deleteTarget.commentId); - setComments((prev) => - prev ? prev.filter((c) => c.commentId !== deleteTarget.commentId) : prev - ); - } catch (err) { - console.error(err); - } finally { - setDeleteTarget(null); - } - }; + const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments`, { + method: 'POST', + headers: { 'Content-type': 'application/json' }, + credentials: 'include', + body:JSON.stringify(body) + }) + + const text = await res.text() + const data = JSON.parse(text) + return data +} + + +export const getRecipeComment = async (cocktailId: number) => { + const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments`, { + method: 'GET', + cache: 'no-store', + credentials:'include' + }); + const data = await res.json(); + if (res.status === 401) return []; + if(!res.ok) throw new Error('댓글 조회 실패') + const filteredComments = data.data.filter((comment: CommentType) => comment.status !== 'DELETED'); - const loadMoreComments = async (lastCommentId: number) => { - if (isEnd || isLoading) return; + return filteredComments +}; - setIsLoading(true); - try { - const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments?lastId=${lastCommentId}`); - const newComments = await res.json(); +export const updateRecipeComment = async(postId: number, commentId: number, content: string) => { + const res = await fetch(`${getApi}/cocktails/${postId}/comments/${commentId}`, { + method: 'PATCH', + headers: { + 'Content-Type':'application/json' + }, + credentials: 'include', + body:JSON.stringify({content}) + }) + + if(!res.ok) throw new Error('댓글 수정 실패') +} - if (newComments.data.length === 0) { - setIsEnd(true); - } else { - setComments((prev) => [...(prev ?? []), ...newComments.data]); +export const deleteRecipeComment = async (cocktailId: number, commentId: number) => { + const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments/${commentId}`, { + method: 'DELETE', + credentials:'include' + }) + if(!res.ok) throw new Error('댓글 삭제 실패') +} + +export function useRecipeComment({cocktailId}:{cocktailId:number}) { + const queryClient = useQueryClient() + const user = useAuthStore(state => state.user) + const {toastInfo,toastError} = useToast() + + const { data:comments=[],refetch,isLoading } = useQuery({ + queryKey: ['comments', cocktailId], + queryFn: () => getRecipeComment(cocktailId), + staleTime: 30_000 + + }) + + const createMut = useMutation({ + mutationFn: (content: string) => { + if (!user?.id) { + toastInfo('로그인 후 이용 가능합니다.'); + return Promise.reject(new Error('unauth')) } - } finally { - setIsLoading(false); + return postRecipeComment(cocktailId, content); + }, + onSuccess: () => refetch(), + onError: (e) => { + if (e.message !== 'unauth') { + toastInfo('댓글은 한개만 작성 가능합니다') + } } - }; - - return { - comments, - isEnd, - isLoading, - deleteTarget, - setDeleteTarget, - fetchData, - handleUpdateComment, - handleAskDeleteComment, - handleConfirmDelete, - loadMoreComments, - }; -} + }); + + const updateMut = useMutation({ + mutationFn: ({ commentId, content }: { + commentId: number, + content: string + }) => updateRecipeComment(cocktailId, commentId, content), + onSuccess: (_, vars) => { + queryClient.setQueryData(['comments',cocktailId], + prev => prev?.map(c => c.commentId === vars.commentId ? + { ...c, content:vars.content } : c + ) ?? prev) + }, + onError: () => toastError('수정 중 에러가 발생했습니다.') + + }); + + const deleteMut = useMutation({ + mutationFn: (commentId: number) => deleteRecipeComment(cocktailId, commentId), + onSuccess: (_res, commentId) => { + queryClient.setQueryData(['comments', cocktailId], + prev => prev?.filter(c=>c.commentId !== commentId) ?? prev + ) + } + }) + return {createMut,updateMut,deleteMut,comments,refetch,user,isLoading} +} \ No newline at end of file diff --git a/src/domains/recipe/api/useRecipeDetails.ts b/src/domains/recipe/api/useRecipeDetails.ts new file mode 100644 index 0000000..e1cf013 --- /dev/null +++ b/src/domains/recipe/api/useRecipeDetails.ts @@ -0,0 +1,43 @@ +import { getApi } from "@/app/api/config/appConfig" +import { useAuthStore } from "@/domains/shared/store/auth" +import { useQuery } from "@tanstack/react-query" + +const fetchKeep = async () => { + const res = await fetch(`${getApi}/me/bar`, { + method: 'GET', + credentials:'include' + }) + + if (!res.ok) return new Set() + + const json = await res.json() + const mykeep = json.data + + return new Set(mykeep.map((v:{cocktailId:number}) => v.cocktailId)) +} + +const fetchRecipe = async (id:number) => { + const res = await fetch(`${getApi}/cocktails/${id}`, { + method:'GET' + }) + + if(!res.ok) throw new Error ('상세페이지 fetch 실패') + const json = await res.json() + return json.data ?? [] + +} + +export const useDetailRecipe = (id:number) => { + const user = useAuthStore(state => state.user) + + return useQuery({ + queryKey: ['detail',id, user?.id], + queryFn: async () => { + const recipe = await fetchRecipe(id) + const keep = user ? await fetchKeep() : null + const iskept = keep ? keep.has(Number(id)) : false + return { recipe,iskept} + } + }) + +} \ No newline at end of file diff --git a/src/domains/recipe/components/details/DetailMain.tsx b/src/domains/recipe/components/details/DetailMain.tsx index 3ee858b..15c36f6 100644 --- a/src/domains/recipe/components/details/DetailMain.tsx +++ b/src/domains/recipe/components/details/DetailMain.tsx @@ -7,79 +7,37 @@ import SsuryShake from '@/shared/assets/ssury/ssury_make.webp'; import SsuryDrink from '@/shared/assets/ssury/ssury_drink.webp'; import Image from 'next/image'; import DetailList from './DetailList'; -import { Suspense, useEffect, useState } from 'react'; -import { getApi } from '@/app/api/config/appConfig'; -import { useAuthStore } from '@/domains/shared/store/auth'; +import { Suspense } from 'react'; import SkeletonDetail from '../../skeleton/SkeletonDetail'; import RecipeComment from './RecipeComment'; +import { useDetailRecipe } from '../../api/useRecipeDetails'; -interface Kept { - cocktailId: number; - id: number; - keptAt: Date; -} function DetailMain({ id }: { id: number }) { - const user = useAuthStore(); - const [cocktail, setCocktail] = useState(); - const [isKept, setIsKept] = useState(null); - - const fetchData = async () => { - const res = await fetch(`${getApi}/cocktails/${id}`); - const json = await res.json(); - if (!res.ok) throw new Error('데이터 요청 실패'); - setCocktail(json.data); - - if (!user) { - setIsKept(false); - return; - } else { - const keepRes = await fetch(`${getApi}/me/bar`, { - method: 'GET', - credentials: 'include', - }); - const keepjson = await keepRes.json(); - const keepIds = keepjson.data.map((a: Kept) => String(a.cocktailId)); - setIsKept(keepIds.includes(String(id))); - } - }; - - useEffect(() => { - fetchData(); - }, []); - useEffect(() => { - window.scrollTo(0, 0); - return () => { - // 레시피 페이지로 돌아가지 않는 경우 (헤더 탭 클릭 등) - // 네비게이션 플래그를 제거하여 스크롤 복원 방지 - const currentPath = window.location.pathname; - - // 디테일 페이지를 벗어나는 경우 - if (!currentPath.includes('/recipe')) { - sessionStorage.removeItem('cocktails_scroll_state_nav_flag'); - } - }; - }, []); + const { + data, + } = useDetailRecipe(id) - if (!cocktail) return; + if(!data?.recipe) return null + const { - cocktailId, - cocktailImgUrl, cocktailName, cocktailNameKo, cocktailStory, + cocktailImgUrl, alcoholStrength, cocktailType, ingredient, recipe, - } = cocktail; + cocktailId + } =data?.recipe return ( }> -

${cocktailNameKo} 상세정보

+

{`${cocktailNameKo} 상세정보`}

- +
diff --git a/src/domains/recipe/components/details/DetailsHeader.tsx b/src/domains/recipe/components/details/DetailsHeader.tsx index d15c053..79e68cb 100644 --- a/src/domains/recipe/components/details/DetailsHeader.tsx +++ b/src/domains/recipe/components/details/DetailsHeader.tsx @@ -13,7 +13,8 @@ interface Meta { url: string; } -function DetailsHeader({ id, favor }: { id: number; favor: boolean | null }) { +function DetailsHeader({ id, favor }: { id: number; favor: boolean | undefined }) { + const [isShare, setIsShare] = useState(false); const [meta, setMeta] = useState(null); diff --git a/src/domains/recipe/components/details/RecipeComment.tsx b/src/domains/recipe/components/details/RecipeComment.tsx index 901a43c..b2cb424 100644 --- a/src/domains/recipe/components/details/RecipeComment.tsx +++ b/src/domains/recipe/components/details/RecipeComment.tsx @@ -1,11 +1,9 @@ import CommentHeader from '@/domains/shared/components/comment/CommentHeader'; import CommentList from '@/domains/shared/components/comment/CommentList'; -import { useAuthStore } from '@/domains/shared/store/auth'; -import { useShallow } from 'zustand/shallow'; import ConfirmModal from '@/shared/components/modal-pop/ConfirmModal'; -import { useRecipeComments } from '../../api/useRecipeComment'; -import { getApi } from '@/app/api/config/appConfig'; -import { useToast } from '@/shared/hook/useToast'; +import { useRecipeComment } from '../../api/useRecipeComment'; +import { useState } from 'react'; +import { CommentType } from '@/domains/community/types/post'; import { ParamValue } from 'next/dist/server/request/params'; interface Props { @@ -13,53 +11,31 @@ interface Props { } function RecipeComment({ cocktailId }: Props) { - const { user } = useAuthStore( - useShallow((state) => ({ - user: state.user, - })) - ); - - const { toastInfo } = useToast(); + + const [deleteTarget, setDeleteTarget] = useState<{ commentId: number, cocktailId: number } | null>(null) + + const { refetch,createMut,deleteMut,updateMut,user,comments,isLoading } = useRecipeComment({ cocktailId }) - const postRecipeComment = async (cocktailId: number | ParamValue, content: string) => { - if (!user?.id) { - toastInfo('로그인 후 이용 가능합니다'); - return; - } - const body = { - cocktailId, - content: content, + const postRecipeComment = async (postId: number | ParamValue, content: string): Promise => { + if (typeof postId !== 'number') return null; + await createMut.mutateAsync(content) + const referesh = await refetch() + return (referesh.data) ?? null + } + const handleUpdateComment = (commentId: number, content:string) => updateMut.mutateAsync({ commentId, content }); + + const handleConfirmDelete = async () => { + if (!deleteTarget) return; + await deleteMut.mutateAsync(deleteTarget.commentId); + setDeleteTarget(null); }; + + const fetchData = () => refetch + const loadMoreComments = () => { } + const isEnd = true + const handleAskDeleteComment = (commentId: number) => setDeleteTarget({ commentId, cocktailId }) - const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', - body: JSON.stringify(body), - }); - - const text = await res.text(); - if (!res.ok) { - toastInfo('댓글은 한 개만 작성가능합니다'); - return; - } - - const data = JSON.parse(text); - return data; - }; - const { - comments, - fetchData, - handleAskDeleteComment, - handleUpdateComment, - loadMoreComments, - isEnd, - isLoading, - deleteTarget, - handleConfirmDelete, - setDeleteTarget, - } = useRecipeComments(cocktailId, user); return (
diff --git a/src/domains/recipe/components/main/CocktailFilter.tsx b/src/domains/recipe/components/main/CocktailFilter.tsx index 0e2fc87..b687157 100644 --- a/src/domains/recipe/components/main/CocktailFilter.tsx +++ b/src/domains/recipe/components/main/CocktailFilter.tsx @@ -32,7 +32,7 @@ function CocktailFilter({ cocktailsEA }: Props) { return (
-

{cocktailsEA}개

+

{cocktailsEA}개+

(
  • >; + onChange: (v:string) =>void } -function CocktailSearchBar({ keyword, setKeyword }: Props) { - const[input,setInput] = useState('') - const debounceKeyword = useMemo(() => debounce((v: string) => { setKeyword(v)},300),[setKeyword]) +function CocktailSearchBar({ keyword, onChange}: Props) { - useEffect(() => { - debounceKeyword(keyword) - }, [keyword, debounceKeyword]) - return ( { - setKeyword(e.target.value) - setInput(e.target.value) - }} + value={keyword} + onChange={(e)=>onChange(e.target.value)} variant="search" className="w-full md:max-w-80" /> diff --git a/src/domains/recipe/components/main/Cocktails.tsx b/src/domains/recipe/components/main/Cocktails.tsx index 3adf51d..5a0f1c7 100644 --- a/src/domains/recipe/components/main/Cocktails.tsx +++ b/src/domains/recipe/components/main/Cocktails.tsx @@ -1,16 +1,19 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import CocktailFilter from './CocktailFilter'; import CocktailList from './CocktailList'; import Accordion from './Accordion'; import CocktailSearchBar from './CocktailSearchBar'; import { useCocktails} from '../../api/fetchRecipe'; import { useInView } from 'react-intersection-observer'; +import { debounce } from '@/shared/utills/debounce'; function Cocktails() { const [keyword,setKeyword] = useState('') +const [input, setInput] = useState(''); + const [alcoholStrengths,setAlcoholStrengths] = useState([]) const [alcoholBaseTypes,setAlcoholBaseTypes] = useState([]) const [cocktailTypes,setCocktailTypes] = useState([]) @@ -20,14 +23,13 @@ const [cocktailTypes,setCocktailTypes] = useState([]) fetchNextPage, hasNextPage, noResults, - isSearchMode, - isFetchingNextPage + isSearchMode } = useCocktails({ keyword, alcoholBaseTypes, alcoholStrengths, - cocktailTypes - },20) + cocktailTypes, + }, 20) const { ref, inView } = useInView({ threshold:0.1 @@ -37,8 +39,13 @@ const [cocktailTypes,setCocktailTypes] = useState([]) if (!isSearchMode && inView && hasNextPage) { fetchNextPage?.() } -},[inView,hasNextPage,fetchNextPage,isSearchMode]) + },[inView,hasNextPage,fetchNextPage]) +const debounceKeyword = useMemo(()=> debounce((v:string)=> setKeyword(v),300),[]) + const handleSearch = (v: string) => { + setInput(v) + debounceKeyword(v) +} return (
    @@ -48,7 +55,7 @@ const [cocktailTypes,setCocktailTypes] = useState([]) setAlcoholStrengths={setAlcoholStrengths} setCocktailTypes={setCocktailTypes} /> - +
  • diff --git a/src/domains/recipe/types/types.ts b/src/domains/recipe/types/types.ts index b3a176f..caa66f3 100644 --- a/src/domains/recipe/types/types.ts +++ b/src/domains/recipe/types/types.ts @@ -4,7 +4,7 @@ export interface Cocktail { cocktailName: string; cocktailImgUrl: string; cocktailNameKo: string; - isFavorited: boolean; + isKeep: boolean; } export interface RecommendCocktail { diff --git a/src/domains/shared/components/cocktail-card/CocktailCard.tsx b/src/domains/shared/components/cocktail-card/CocktailCard.tsx index b113c4f..593e3fa 100644 --- a/src/domains/shared/components/cocktail-card/CocktailCard.tsx +++ b/src/domains/shared/components/cocktail-card/CocktailCard.tsx @@ -33,7 +33,6 @@ function CocktailCard({ favor, }: Props) { const alcoholTitle = labelTitle(alcohol); - return (
    Date: Wed, 15 Oct 2025 01:46:17 +0900 Subject: [PATCH 03/11] =?UTF-8?q?[refactor]=20=EC=95=84=EC=BD=94=EB=94=94?= =?UTF-8?q?=EC=96=B8=EB=B0=95=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mypage/components/EditNickName.tsx | 2 +- src/domains/recipe/api/fetchRecipe.ts | 46 +++++++++---------- src/domains/recipe/api/useRecipeComment.ts | 3 +- .../recipe/components/details/BackBtn.tsx | 4 +- .../recipe/components/main/Accordion.tsx | 4 +- .../recipe/components/main/CocktailFilter.tsx | 33 +++++-------- .../recipe/components/main/CocktailList.tsx | 12 +++-- .../recipe/components/main/Cocktails.tsx | 18 ++++++-- src/domains/recipe/types/types.ts | 1 + 9 files changed, 63 insertions(+), 60 deletions(-) diff --git a/src/domains/mypage/components/EditNickName.tsx b/src/domains/mypage/components/EditNickName.tsx index dcee62c..c355c8b 100644 --- a/src/domains/mypage/components/EditNickName.tsx +++ b/src/domains/mypage/components/EditNickName.tsx @@ -80,7 +80,7 @@ function EditNickName({ value={editNickName} className="w-full" /> - 전 닉네임으로 돌아가기 + 초기화
    ); diff --git a/src/domains/recipe/api/fetchRecipe.ts b/src/domains/recipe/api/fetchRecipe.ts index 2156c5a..4d9d37e 100644 --- a/src/domains/recipe/api/fetchRecipe.ts +++ b/src/domains/recipe/api/fetchRecipe.ts @@ -1,7 +1,7 @@ import { getApi } from "@/app/api/config/appConfig"; import { useAuthStore } from "@/domains/shared/store/auth"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; -import { Cocktail } from "../types/types"; +import { Cocktail, Sort } from "../types/types"; @@ -20,7 +20,6 @@ interface SearchFilters { alcoholBaseTypes: string[]; } -type Sort = 'recent' | 'keeps' | 'comments' interface CocktailFilter extends SearchFilters{ sortBy?:Sort @@ -44,20 +43,20 @@ const fetchKeep = async (): Promise> => { const fetchRecipe = async ( lastId: number | null, size: number, - sortBy?: Sort + sortBy?: Sort ): Promise => { const url = new URL(`${getApi}/cocktails`) - url.searchParams.set('SIZE',String(size)) - url.searchParams.set('lastId', String(lastId)) - url.searchParams.set('lastValue', String(lastId)); - - + url.searchParams.set('size',String(size)) + if (lastId !== null) { + url.searchParams.set('lastId', String(lastId)); + url.searchParams.set('lastValue', String(lastId)); + } + if (sortBy) { - url.searchParams.set('sortBy',sortBy) + url.searchParams.set('sortBy',String(sortBy)) } - const res = await fetch(url.toString(), { method:'GET' }) @@ -110,28 +109,27 @@ export const useCocktailsInfiniteQuery = ( sortBy?: Sort ) => { const user = useAuthStore((state) => state.user); - return useInfiniteQuery({ - queryKey: ['cocktails','infinite',sortBy, size, user?.id], + queryKey: ['cocktails', 'infinite', sortBy, size, user?.id], queryFn: async ({ pageParam }) => { - const cocktails = await fetchRecipe(pageParam,size,sortBy) + const cocktails = await fetchRecipe(pageParam, size, sortBy); if (user) { - const keepId = await fetchKeep() + const keepId = await fetchKeep(); return cocktails.map((item) => ({ ...item, - isKeep: keepId.has(item.cocktailId) - })) + isKeep: keepId.has(item.cocktailId), + })); } - return cocktails + return cocktails; }, getNextPageParam: (lastpage) => { - if(lastpage.length < size) return undefined - return lastpage[lastpage.length - 1]?.cocktailId ?? undefined + if (lastpage.length < size) return undefined; + return lastpage[lastpage.length - 1]?.cocktailId ?? undefined; }, - initialPageParam: 345 - }) + initialPageParam: null as number | null, + }); } @@ -140,7 +138,7 @@ export const useCocktailsSearchQuery = (filters:SearchFilters) => { const isActive = hasActiveFilters(filters) return useQuery({ - queryKey: ['cocktails', 'search', filters, user?.id], + queryKey: ['cocktails', 'search', filters, user?.id], queryFn: async () => { const cocktails = await searchCocktails(filters) if (user && cocktails.length > 0) { @@ -157,9 +155,9 @@ export const useCocktailsSearchQuery = (filters:SearchFilters) => { }) } -export const useCocktails = (filters: CocktailFilter, infiniteScrollSize: number = 20) => { +export const useCocktails = (filters: CocktailFilter, infiniteScrollSize: number = 20,sortBy?:Sort) => { const isSearchMode = hasActiveFilters(filters); - const infiniteQuery = useCocktailsInfiniteQuery(infiniteScrollSize, filters.sortBy); + const infiniteQuery = useCocktailsInfiniteQuery(infiniteScrollSize, sortBy); const searchQuery = useCocktailsSearchQuery(filters); if (isSearchMode) { diff --git a/src/domains/recipe/api/useRecipeComment.ts b/src/domains/recipe/api/useRecipeComment.ts index b9392b5..67ffcfa 100644 --- a/src/domains/recipe/api/useRecipeComment.ts +++ b/src/domains/recipe/api/useRecipeComment.ts @@ -68,8 +68,7 @@ export function useRecipeComment({cocktailId}:{cocktailId:number}) { const { data:comments=[],refetch,isLoading } = useQuery({ queryKey: ['comments', cocktailId], queryFn: () => getRecipeComment(cocktailId), - staleTime: 30_000 - + staleTime: 30_000, }) const createMut = useMutation({ diff --git a/src/domains/recipe/components/details/BackBtn.tsx b/src/domains/recipe/components/details/BackBtn.tsx index 069522a..499c108 100644 --- a/src/domains/recipe/components/details/BackBtn.tsx +++ b/src/domains/recipe/components/details/BackBtn.tsx @@ -6,11 +6,11 @@ import Back from '@/shared/assets/icons/back_36.svg'; function BackButton() { const router = useRouter(); + const handleClick = () => { const url = sessionStorage.getItem('saveUrl'); if (!url) return; - router.push(url); - sessionStorage.removeItem('listScrollY'); + router.replace(url,{scroll:false}); }; return ( diff --git a/src/domains/recipe/components/main/Accordion.tsx b/src/domains/recipe/components/main/Accordion.tsx index 1b35825..71549e2 100644 --- a/src/domains/recipe/components/main/Accordion.tsx +++ b/src/domains/recipe/components/main/Accordion.tsx @@ -1,7 +1,7 @@ 'use client'; import SelectBox from '@/shared/components/select-box/SelectBox'; -import { Dispatch, SetStateAction, useEffect, useMemo } from 'react'; +import { Dispatch, SetStateAction, useEffect} from 'react'; import { useSearchParams, usePathname, useRouter } from 'next/navigation'; interface Props { @@ -140,7 +140,7 @@ function Accordion({ setAlcoholBaseTypes, setCocktailTypes, setAlcoholStrengths return (
      {SELECT_OPTIONS.map(({ id, option, title }) => { - const currentValue = currentValues[id as keyof typeof currentValues]; + const currentValue = currentValues()[id as keyof typeof currentValues]; return (
    • diff --git a/src/domains/recipe/components/main/CocktailFilter.tsx b/src/domains/recipe/components/main/CocktailFilter.tsx index b687157..d9a6286 100644 --- a/src/domains/recipe/components/main/CocktailFilter.tsx +++ b/src/domains/recipe/components/main/CocktailFilter.tsx @@ -1,12 +1,11 @@ -import { getApi } from '@/app/api/config/appConfig'; + import SelectBox from '@/shared/components/select-box/SelectBox'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { Dispatch, SetStateAction } from 'react'; -import { Cocktail } from '../../types/types'; +import { useQueryClient } from '@tanstack/react-query'; +import { useRouter} from 'next/navigation'; + interface Props { cocktailsEA: number; - } function CocktailFilter({ cocktailsEA }: Props) { @@ -15,19 +14,15 @@ function CocktailFilter({ cocktailsEA }: Props) { 인기순: 'keeps', 댓글순: 'comments', }; - const searchParams = useSearchParams(); - const query = searchParams.get('sortBy'); + const queryClient = useQueryClient(); const router = useRouter(); const handleChange = async (selectTitle: string) => { - if (!query) return; - try { - const res = await fetch(`${getApi}/cocktails`); - const json = await res.json(); - - } catch { - console.error(); - console.log(selectTitle); - } + const sortValue = sortMap[selectTitle as keyof typeof sortMap] + queryClient.removeQueries({ + queryKey: ['cocktails', 'infinite'], + exact:false + }) + router.push(`?sortBy=${sortValue}`); }; return ( @@ -36,11 +31,7 @@ function CocktailFilter({ cocktailsEA }: Props) { { - const sortValue = sortMap[value as keyof typeof sortMap]; - handleChange(value); - router.push(`?sortBy=${sortValue}`); - }} + onChange={handleChange} />
    ); diff --git a/src/domains/recipe/components/main/CocktailList.tsx b/src/domains/recipe/components/main/CocktailList.tsx index ebd9fad..7c16636 100644 --- a/src/domains/recipe/components/main/CocktailList.tsx +++ b/src/domains/recipe/components/main/CocktailList.tsx @@ -8,8 +8,12 @@ interface Props { cocktails: Cocktail[]; } -function CocktailList({ cocktails}: Props) { +function CocktailList({cocktails}: Props) { + const handleClick = () => { + sessionStorage.setItem('saveUrl',location.href) + } + return (
      - + },i) => ( +
    • + ([]) alcoholBaseTypes, alcoholStrengths, cocktailTypes, - }, 20) + }, 20,sortBy) const { ref, inView } = useInView({ threshold:0.1 @@ -45,7 +49,7 @@ const debounceKeyword = useMemo(()=> debounce((v:string)=> setKeyword(v),300),[] const handleSearch = (v: string) => { setInput(v) debounceKeyword(v) -} + } return (
      @@ -61,9 +65,15 @@ const debounceKeyword = useMemo(()=> debounce((v:string)=> setKeyword(v),300),[]
      - + { + noResults ? ( +
      검색 결과가 없습니다.
      + ): ( + + ) + }
      -
      +
      ); } diff --git a/src/domains/recipe/types/types.ts b/src/domains/recipe/types/types.ts index caa66f3..2d53040 100644 --- a/src/domains/recipe/types/types.ts +++ b/src/domains/recipe/types/types.ts @@ -36,3 +36,4 @@ export type TagType = { cocktailName: string; cocktailNameKo: string; }; +export type Sort = 'recent' | 'keeps' | 'comments'; From 3e5306e63d3e7385f5a5e4790a3c86951504dfc5 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Wed, 15 Oct 2025 10:35:55 +0900 Subject: [PATCH 04/11] =?UTF-8?q?[feat]=ED=95=84=ED=84=B0=EB=A7=81=20?= =?UTF-8?q?=EB=92=A4=EB=A1=9C=EA=B0=80=EA=B8=B0=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recipe/components/details/BackBtn.tsx | 23 ++- .../recipe/components/main/Accordion.tsx | 2 +- .../recipe/components/main/CocktailList.tsx | 15 +- src/domains/recipe/hook/useSaveScroll.ts | 100 +++++++++++ src/domains/recipe/hook/useSearchControl.tsx | 67 -------- .../shared/hook/useIntersectionObserver.ts | 25 --- src/domains/shared/hook/useMemoScroll.ts | 156 ------------------ 7 files changed, 126 insertions(+), 262 deletions(-) create mode 100644 src/domains/recipe/hook/useSaveScroll.ts delete mode 100644 src/domains/recipe/hook/useSearchControl.tsx delete mode 100644 src/domains/shared/hook/useIntersectionObserver.ts delete mode 100644 src/domains/shared/hook/useMemoScroll.ts diff --git a/src/domains/recipe/components/details/BackBtn.tsx b/src/domains/recipe/components/details/BackBtn.tsx index 499c108..cabca42 100644 --- a/src/domains/recipe/components/details/BackBtn.tsx +++ b/src/domains/recipe/components/details/BackBtn.tsx @@ -1,20 +1,25 @@ 'use client'; -import { useRouter } from 'next/navigation'; import Back from '@/shared/assets/icons/back_36.svg'; +import { useSaveScroll } from '../../hook/useSaveScroll'; function BackButton() { - const router = useRouter(); + + const { restoreAndGoBack } = useSaveScroll({ + storageKey: 'cocktail_list_scroll', + }); - - const handleClick = () => { - const url = sessionStorage.getItem('saveUrl'); - if (!url) return; - router.replace(url,{scroll:false}); - }; + const handleBack = () => { + console.log('뒤로가기 클릭'); + console.log('저장된 스크롤:', sessionStorage.getItem('cocktail_list_scroll')); + console.log('저장된 URL:', sessionStorage.getItem('cocktail_list_scroll_url')); + console.log('복원 플래그:', sessionStorage.getItem('cocktail_list_scroll_restore')); + restoreAndGoBack() + } + return ( - ); diff --git a/src/domains/recipe/components/main/Accordion.tsx b/src/domains/recipe/components/main/Accordion.tsx index 71549e2..45c8280 100644 --- a/src/domains/recipe/components/main/Accordion.tsx +++ b/src/domains/recipe/components/main/Accordion.tsx @@ -149,7 +149,7 @@ function Accordion({ setAlcoholBaseTypes, setCocktailTypes, setAlcoholStrengths title={title} id={id} groupKey="filter" - value={currentValue} // 현재 선택된 값 전달 + value={currentValue} onChange={(value) => handleSelect(id, value)} />
    • diff --git a/src/domains/recipe/components/main/CocktailList.tsx b/src/domains/recipe/components/main/CocktailList.tsx index 7c16636..ba64e3c 100644 --- a/src/domains/recipe/components/main/CocktailList.tsx +++ b/src/domains/recipe/components/main/CocktailList.tsx @@ -3,6 +3,7 @@ import Link from 'next/link'; import { Cocktail } from '../../types/types'; import CocktailCard from '@/domains/shared/components/cocktail-card/CocktailCard'; +import { useSaveScroll } from '../../hook/useSaveScroll'; interface Props { cocktails: Cocktail[]; @@ -10,9 +11,15 @@ interface Props { function CocktailList({cocktails}: Props) { - const handleClick = () => { - sessionStorage.setItem('saveUrl',location.href) - } + const { saveAndNavigate } = useSaveScroll({ + storageKey: 'cocktail_list_scroll', + }); + +const handleClick = (cocktailId: number) => (e: React.MouseEvent) => { + e.preventDefault(); + + saveAndNavigate(`/recipe/${cocktailId}`); +}; return (
        (
      • - + { + const { + storageKey = 'cocktail_scroll', + enabled = true, + pageType = 'list', // 기본값은 리스트 + } = opt; + const router = useRouter(); + const hasRestore = useRef(false); + + useEffect(() => { + console.log('=== useEffect 실행 ==='); + console.log('pageType:', pageType); + + // 상세 페이지에서는 스크롤 복원 로직 실행 안함 + if (pageType === 'detail') { + console.log('상세 페이지 - 복원 스킵'); + return; + } + + if (!enabled || hasRestore.current) { + console.log('조건 불만족'); + return; + } + + const savedPosition = sessionStorage.getItem(storageKey); + const shouldRestore = sessionStorage.getItem(`${storageKey}_should_restore`); + + console.log('savedPosition:', savedPosition); + console.log('shouldRestore:', shouldRestore); + + // 리스트 페이지이고, 복원 플래그가 있을 때만 복원 + if (savedPosition && shouldRestore === 'true') { + const position = parseInt(savedPosition, 10); + + console.log('스크롤 복원 시도:', position); + + const restoreScroll = () => { + console.log('restoreScroll 함수 실행'); + window.scrollTo(0, position); + console.log('현재 스크롤 위치:', window.scrollY); + hasRestore.current = true; + }; + + // 여러 타이밍에 시도 + requestAnimationFrame(restoreScroll); + setTimeout(restoreScroll, 0); + setTimeout(restoreScroll, 100); + + // 복원 플래그 제거 + sessionStorage.removeItem(`${storageKey}_should_restore`); + } else { + console.log('복원 조건 불만족'); + } + }, [storageKey, enabled, pageType]); + + const saveScroll = () => { + if (!enabled) return; + const currentScroll = window.scrollY; + sessionStorage.setItem(storageKey, currentScroll.toString()); + console.log('스크롤 저장:', currentScroll); + }; + + // 상세 페이지로 이동 (스크롤 위치만 저장, 복원 플래그는 설정 안함) + const saveAndNavigate = (href: string) => { + saveScroll(); + sessionStorage.setItem(`${storageKey}_url`, location.href); + console.log('상세 페이지로 이동 - 스크롤만 저장'); + router.push(href); + }; + + // 뒤로가기 (복원 플래그 설정) + const restoreAndGoBack = () => { + const saveUrl = sessionStorage.getItem(`${storageKey}_url`); + console.log('뒤로가기 - saveUrl:', saveUrl); + + if (!saveUrl) return; + + // 뒤로가기할 때만 복원 플래그 설정 + sessionStorage.setItem(`${storageKey}_should_restore`, 'true'); + console.log('복원 플래그 설정 완료'); + + router.replace(saveUrl, { scroll: false }); + }; + + return { + saveScroll, + saveAndNavigate, + restoreAndGoBack, + }; +}; diff --git a/src/domains/recipe/hook/useSearchControl.tsx b/src/domains/recipe/hook/useSearchControl.tsx deleted file mode 100644 index a00e40f..0000000 --- a/src/domains/recipe/hook/useSearchControl.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { debounce } from '@/shared/utills/debounce'; -import { useEffect, useMemo, useState } from 'react'; - -interface UseSearchControlProps { - delay?: number; - storageKey?: string; // 검색 상태 저장용 키 -} - -function useSearchControl({ delay = 300, storageKey }: UseSearchControlProps) { - // 초기값을 sessionStorage에서 복원 - const [inputValue, setInputValue] = useState(() => { - if (typeof window === 'undefined' || !storageKey) return ''; - const saved = sessionStorage.getItem(`${storageKey}_search`); - return saved ? JSON.parse(saved).inputValue : ''; - }); - - const [keyword, setKeyword] = useState(() => { - if (typeof window === 'undefined' || !storageKey) return ''; - const saved = sessionStorage.getItem(`${storageKey}_search`); - return saved ? JSON.parse(saved).keyword : ''; - }); - - const [noResults, setNoResults] = useState(false); - - const isSearching = keyword.trim().length > 0; - - // 검색 상태를 sessionStorage에 저장 - useEffect(() => { - if (!storageKey) return; - sessionStorage.setItem( - `${storageKey}_search`, - JSON.stringify({ - inputValue, - keyword, - }) - ); - }, [inputValue, keyword, storageKey]); - - const debouncedKeyword = useMemo(() => debounce((v: string) => setKeyword(v), delay), [delay]); - - const onInputChange = (v: string) => { - setInputValue(v); - debouncedKeyword(v); - }; - - // 검색 상태 초기화 함수 - const resetSearch = () => { - setInputValue(''); - setKeyword(''); - setNoResults(false); - if (storageKey) { - sessionStorage.removeItem(`${storageKey}_search`); - } - }; - - return { - inputValue, - keyword, - isSearching, - onInputChange, - noResults, - setNoResults, - resetSearch, - }; -} - -export default useSearchControl; diff --git a/src/domains/shared/hook/useIntersectionObserver.ts b/src/domains/shared/hook/useIntersectionObserver.ts deleted file mode 100644 index 85ddc67..0000000 --- a/src/domains/shared/hook/useIntersectionObserver.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { RefObject, useEffect, useRef } from 'react'; - -export const useIntersectionObserver = ( - targetRef: RefObject, // 관찰하는 요소 - onIntersect: IntersectionObserverCallback, // 관찰 될 때 실행할 함수 - hasNextPage: boolean | undefined // 무한스크롤로 더 불러올 요소가 있는지 -) => { - const observer = useRef(null); - - useEffect(() => { - if (targetRef && targetRef.current) { - observer.current = new IntersectionObserver(onIntersect, { - root: null, - rootMargin: '200px', - threshold: 1.0, - }); - if (!hasNextPage) { - observer.current?.unobserve(targetRef.current); - return; - } - observer.current.observe(targetRef.current); - } - return () => observer && observer.current?.disconnect(); - }, [targetRef, onIntersect]); -}; diff --git a/src/domains/shared/hook/useMemoScroll.ts b/src/domains/shared/hook/useMemoScroll.ts deleted file mode 100644 index 87bd13f..0000000 --- a/src/domains/shared/hook/useMemoScroll.ts +++ /dev/null @@ -1,156 +0,0 @@ -// useScrollRestore.ts -import { usePathname } from 'next/navigation'; -import { useCallback, useEffect, useLayoutEffect, useRef } from 'react'; - -interface UseScrollRestoreProps { - lastId: number | null; // 현재까지 로드한 "최소 id"(내림차순에서 커서) - fetchData: (cursor?: string) => Promise; - currentDataLength: number; - hasNextPage?: boolean; // 선택: 있으면 조기 종료에 사용 -} - -type SavedShape = { targetId: number | null; scrollY: number }; - -export function useScrollRestore({ - lastId, - fetchData, - currentDataLength, - hasNextPage, -}: UseScrollRestoreProps) { - const pathname = usePathname(); - const KEY = `scroll-${pathname}`; - - const isRestoringRef = useRef(false); - const hasRestoredRef = useRef(false); - const lastIdRef = useRef(lastId); - const lenRef = useRef(currentDataLength); - - useEffect(() => { - lastIdRef.current = lastId; - }, [lastId]); - useEffect(() => { - lenRef.current = currentDataLength; - }, [currentDataLength]); - - // 브라우저 기본 복원 비활성화 - useLayoutEffect(() => { - if ('scrollRestoration' in history) { - try { - history.scrollRestoration = 'manual'; - } catch {} - } - }, []); - - const jumpOnce = useCallback( - (y: number) => { - const el = document.scrollingElement || document.documentElement; - const enough = () => document.body.scrollHeight >= y + window.innerHeight; - let done = false; - - const finish = () => { - if (done) return; - done = true; - el.scrollTo({ top: y, behavior: 'auto' }); - isRestoringRef.current = false; - hasRestoredRef.current = true; - sessionStorage.removeItem(KEY); - }; - - if (enough()) { - requestAnimationFrame(finish); - return; - } - - const ro = new ResizeObserver(() => { - if (enough()) { - ro.disconnect(); - finish(); - } - }); - ro.observe(document.body); - window.addEventListener( - 'load', - () => { - if (enough()) finish(); - }, - { once: true } - ); - setTimeout(() => finish(), 1000); - }, - [KEY] - ); - - // 복원 - useEffect(() => { - if (hasRestoredRef.current) return; - - const raw = sessionStorage.getItem(KEY); - if (!raw) { - hasRestoredRef.current = true; - return; - } - - let saved: SavedShape | null = null; - try { - saved = JSON.parse(raw) as SavedShape; - } catch { - sessionStorage.removeItem(KEY); - return; - } - if (!saved) { - sessionStorage.removeItem(KEY); - return; - } - - const { targetId, scrollY } = saved; - isRestoringRef.current = true; - - const MAX_FETCH = 50; - - const restore = async () => { - let tries = 0; - let lastProgressLen = lenRef.current; - let lastProgressId = lastIdRef.current; - - // 내림차순 전제: - // 더 불러올수록 현재 최소 id(=lastIdRef.current)가 "작아진다" - // 목표 지점 도달 조건: currentMinId <= targetId 또는 targetId==null - while ( - targetId != null && - (lastIdRef.current == null || (lastIdRef.current as number) > targetId) - ) { - if (hasNextPage === false) break; // 더 없음 - if (tries++ >= MAX_FETCH) break; // 안전망 - - await fetchData(); - - // 진행 없음(길이와 lastId 모두 동일) → 중단 - const noLenChange = lenRef.current === lastProgressLen; - const noIdChange = lastIdRef.current === lastProgressId; - if (noLenChange && noIdChange) break; - - lastProgressLen = lenRef.current; - lastProgressId = lastIdRef.current; - - // 다음 렌더로 넘겨 레이아웃 안정화 - await new Promise((r) => setTimeout(r, 0)); - } - - requestAnimationFrame(() => jumpOnce(scrollY)); - }; - - restore(); - }, [KEY, fetchData, hasNextPage, jumpOnce]); - - // 저장 - const saveScroll = useCallback(() => { - const payload: SavedShape = { - targetId: lastIdRef.current, - scrollY: window.scrollY, - }; - sessionStorage.setItem(KEY, JSON.stringify(payload)); - sessionStorage.setItem('saveUrl', location.href); - }, [KEY]); - - return saveScroll; -} From 311548b12eda6967034f0ed4538cf67de8feefae Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Wed, 15 Oct 2025 11:01:56 +0900 Subject: [PATCH 05/11] =?UTF-8?q?[chore]=20=EB=A8=B8=EC=A7=80=20=EC=A0=84?= =?UTF-8?q?=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/pages/my-alarm/MyAlarm.tsx | 6 +++++ .../mypage/components/pages/my-bar/MyBar.tsx | 6 +++++ src/domains/recipe/hook/useSaveScroll.ts | 27 ++++--------------- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/domains/mypage/components/pages/my-alarm/MyAlarm.tsx b/src/domains/mypage/components/pages/my-alarm/MyAlarm.tsx index 0124b1e..ba3b78d 100644 --- a/src/domains/mypage/components/pages/my-alarm/MyAlarm.tsx +++ b/src/domains/mypage/components/pages/my-alarm/MyAlarm.tsx @@ -7,6 +7,7 @@ import Link from 'next/link'; import useFetchAlarm from '@/domains/mypage/api/fetchAlarm'; import { useQuery } from '@tanstack/react-query'; import DeleteAllModal from '../../DeleteAllModal'; +import { useToast } from '@/shared/hook/useToast'; interface MyAlarm { createdAt: Date; @@ -21,6 +22,7 @@ interface MyAlarm { } function MyAlarm() { + const {toastInfo} =useToast() const [isModal, setIsModal] = useState(false); const { fetchAlarm } = useFetchAlarm(); const { data } = useQuery({ @@ -29,6 +31,10 @@ function MyAlarm() { }); const handleDelete = () => { + if (data.items.length == 0) { + toastInfo('아직 알림이 없습니다.'); + return; + } setIsModal(!isModal); }; diff --git a/src/domains/mypage/components/pages/my-bar/MyBar.tsx b/src/domains/mypage/components/pages/my-bar/MyBar.tsx index 9ebd921..6f24669 100644 --- a/src/domains/mypage/components/pages/my-bar/MyBar.tsx +++ b/src/domains/mypage/components/pages/my-bar/MyBar.tsx @@ -8,6 +8,7 @@ import { useState } from 'react'; import DeleteAllModal from '../../DeleteAllModal'; import useFetchMyBar from '@/domains/mypage/api/fetchMyBar'; import { useQuery } from '@tanstack/react-query'; +import { useToast } from '@/shared/hook/useToast'; interface MyCocktail { cocktailId: number; @@ -19,6 +20,7 @@ interface MyCocktail { } function MyBar() { + const {toastInfo} =useToast() const [isModal, setIsModal] = useState(false); const { fetchMyBar } = useFetchMyBar(); const { data } = useQuery({ @@ -28,6 +30,10 @@ function MyBar() { }); const handleDelete = () => { + if (data.items.length == 0) { + toastInfo('저장한 칵테일이 없습니다.') + return + } setIsModal(!isModal); }; diff --git a/src/domains/recipe/hook/useSaveScroll.ts b/src/domains/recipe/hook/useSaveScroll.ts index 34078ec..fa12a5e 100644 --- a/src/domains/recipe/hook/useSaveScroll.ts +++ b/src/domains/recipe/hook/useSaveScroll.ts @@ -4,7 +4,6 @@ import { useEffect, useRef } from 'react'; interface Scroll { storageKey?: string; enabled?: boolean; - // 이 페이지가 리스트 페이지인지 상세 페이지인지 구분 pageType?: 'list' | 'detail'; } @@ -12,7 +11,7 @@ export const useSaveScroll = (opt: Scroll = {}) => { const { storageKey = 'cocktail_scroll', enabled = true, - pageType = 'list', // 기본값은 리스트 + pageType = 'list', } = opt; const router = useRouter(); const hasRestore = useRef(false); @@ -21,29 +20,18 @@ export const useSaveScroll = (opt: Scroll = {}) => { console.log('=== useEffect 실행 ==='); console.log('pageType:', pageType); - // 상세 페이지에서는 스크롤 복원 로직 실행 안함 - if (pageType === 'detail') { - console.log('상세 페이지 - 복원 스킵'); - return; - } - if (!enabled || hasRestore.current) { - console.log('조건 불만족'); - return; - } + if (pageType === 'detail') return + + if (!enabled || hasRestore.current) return const savedPosition = sessionStorage.getItem(storageKey); const shouldRestore = sessionStorage.getItem(`${storageKey}_should_restore`); - console.log('savedPosition:', savedPosition); - console.log('shouldRestore:', shouldRestore); - // 리스트 페이지이고, 복원 플래그가 있을 때만 복원 if (savedPosition && shouldRestore === 'true') { const position = parseInt(savedPosition, 10); - console.log('스크롤 복원 시도:', position); - const restoreScroll = () => { console.log('restoreScroll 함수 실행'); window.scrollTo(0, position); @@ -51,12 +39,11 @@ export const useSaveScroll = (opt: Scroll = {}) => { hasRestore.current = true; }; - // 여러 타이밍에 시도 + requestAnimationFrame(restoreScroll); setTimeout(restoreScroll, 0); setTimeout(restoreScroll, 100); - // 복원 플래그 제거 sessionStorage.removeItem(`${storageKey}_should_restore`); } else { console.log('복원 조건 불만족'); @@ -67,27 +54,23 @@ export const useSaveScroll = (opt: Scroll = {}) => { if (!enabled) return; const currentScroll = window.scrollY; sessionStorage.setItem(storageKey, currentScroll.toString()); - console.log('스크롤 저장:', currentScroll); }; // 상세 페이지로 이동 (스크롤 위치만 저장, 복원 플래그는 설정 안함) const saveAndNavigate = (href: string) => { saveScroll(); sessionStorage.setItem(`${storageKey}_url`, location.href); - console.log('상세 페이지로 이동 - 스크롤만 저장'); router.push(href); }; // 뒤로가기 (복원 플래그 설정) const restoreAndGoBack = () => { const saveUrl = sessionStorage.getItem(`${storageKey}_url`); - console.log('뒤로가기 - saveUrl:', saveUrl); if (!saveUrl) return; // 뒤로가기할 때만 복원 플래그 설정 sessionStorage.setItem(`${storageKey}_should_restore`, 'true'); - console.log('복원 플래그 설정 완료'); router.replace(saveUrl, { scroll: false }); }; From 0e5ac992a3a305caa92b37e685f2a29842db35f0 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Wed, 15 Oct 2025 11:04:43 +0900 Subject: [PATCH 06/11] =?UTF-8?q?[chore]=EB=A8=B8=EC=A7=80=20=EC=A0=84=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B=20=EB=88=84=EB=9D=BD=20=EB=82=B4=EC=9A=A9=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/mypage/components/DeleteAllModal.tsx | 12 ++++++++++-- src/domains/recipe/hook/useSaveScroll.ts | 11 ++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/domains/mypage/components/DeleteAllModal.tsx b/src/domains/mypage/components/DeleteAllModal.tsx index c7720a5..da33a50 100644 --- a/src/domains/mypage/components/DeleteAllModal.tsx +++ b/src/domains/mypage/components/DeleteAllModal.tsx @@ -2,6 +2,7 @@ import ConfirmModal from '@/shared/components/modal-pop/ConfirmModal'; import { Dispatch, SetStateAction } from 'react'; import useFetchMyBar from '../api/fetchMyBar'; import useFetchAlarm from '../api/fetchAlarm'; +import { useToast } from '@/shared/hook/useToast'; interface Props { open: boolean; @@ -11,16 +12,23 @@ interface Props { } function DeleteAllModal({ open, onClose, setIsModal, type }: Props) { + const { toastSuccess } =useToast() const { deleteMyBar } = useFetchMyBar(); const { deleteAlarm } = useFetchAlarm(); const handleBarDelete = () => { deleteMyBar.mutate(undefined, { - onSuccess: () => setIsModal(false), + onSuccess: () => { + toastSuccess('성공적으로 삭제 되었습니다.'); + setIsModal(false); + } }); }; const handleAlarmDelete = () => { deleteAlarm.mutate(undefined, { - onSuccess: () => setIsModal(false), + onSuccess: () => { + setIsModal(false); + toastSuccess('성공적으로 삭제 되었습니다.'); + }, }); }; diff --git a/src/domains/recipe/hook/useSaveScroll.ts b/src/domains/recipe/hook/useSaveScroll.ts index fa12a5e..eaca2c4 100644 --- a/src/domains/recipe/hook/useSaveScroll.ts +++ b/src/domains/recipe/hook/useSaveScroll.ts @@ -17,10 +17,7 @@ export const useSaveScroll = (opt: Scroll = {}) => { const hasRestore = useRef(false); useEffect(() => { - console.log('=== useEffect 실행 ==='); - console.log('pageType:', pageType); - - + if (pageType === 'detail') return if (!enabled || hasRestore.current) return @@ -33,9 +30,7 @@ export const useSaveScroll = (opt: Scroll = {}) => { const position = parseInt(savedPosition, 10); const restoreScroll = () => { - console.log('restoreScroll 함수 실행'); window.scrollTo(0, position); - console.log('현재 스크롤 위치:', window.scrollY); hasRestore.current = true; }; @@ -45,9 +40,7 @@ export const useSaveScroll = (opt: Scroll = {}) => { setTimeout(restoreScroll, 100); sessionStorage.removeItem(`${storageKey}_should_restore`); - } else { - console.log('복원 조건 불만족'); - } + } }, [storageKey, enabled, pageType]); const saveScroll = () => { From b38afe64a9fe717558ded7fa1bd3a761d581f61b Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Wed, 15 Oct 2025 11:16:35 +0900 Subject: [PATCH 07/11] =?UTF-8?q?[refactor]=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mypage/components/DeleteAllModal.tsx | 4 +- .../components/pages/my-alarm/MyAlarm.tsx | 10 +- .../mypage/components/pages/my-bar/MyBar.tsx | 6 +- src/domains/recipe/api/fetchRecipe.ts | 155 ++++++++---------- src/domains/recipe/api/useRecipeComment.ts | 105 ++++++------ src/domains/recipe/api/useRecipeDetails.ts | 58 ++++--- .../recipe/components/details/BackBtn.tsx | 14 +- .../recipe/components/details/DetailMain.tsx | 12 +- .../components/details/DetailsHeader.tsx | 1 - .../components/details/RecipeComment.tsx | 48 +++--- .../recipe/components/main/Accordion.tsx | 6 +- .../recipe/components/main/CocktailFilter.tsx | 20 +-- .../recipe/components/main/CocktailList.tsx | 29 ++-- .../components/main/CocktailSearchBar.tsx | 7 +- .../recipe/components/main/Cocktails.tsx | 69 ++++---- src/domains/recipe/hook/useSaveScroll.ts | 15 +- 16 files changed, 260 insertions(+), 299 deletions(-) diff --git a/src/domains/mypage/components/DeleteAllModal.tsx b/src/domains/mypage/components/DeleteAllModal.tsx index da33a50..44e16dc 100644 --- a/src/domains/mypage/components/DeleteAllModal.tsx +++ b/src/domains/mypage/components/DeleteAllModal.tsx @@ -12,7 +12,7 @@ interface Props { } function DeleteAllModal({ open, onClose, setIsModal, type }: Props) { - const { toastSuccess } =useToast() + const { toastSuccess } = useToast(); const { deleteMyBar } = useFetchMyBar(); const { deleteAlarm } = useFetchAlarm(); const handleBarDelete = () => { @@ -20,7 +20,7 @@ function DeleteAllModal({ open, onClose, setIsModal, type }: Props) { onSuccess: () => { toastSuccess('성공적으로 삭제 되었습니다.'); setIsModal(false); - } + }, }); }; const handleAlarmDelete = () => { diff --git a/src/domains/mypage/components/pages/my-alarm/MyAlarm.tsx b/src/domains/mypage/components/pages/my-alarm/MyAlarm.tsx index ba3b78d..3b95536 100644 --- a/src/domains/mypage/components/pages/my-alarm/MyAlarm.tsx +++ b/src/domains/mypage/components/pages/my-alarm/MyAlarm.tsx @@ -22,7 +22,7 @@ interface MyAlarm { } function MyAlarm() { - const {toastInfo} =useToast() + const { toastInfo } = useToast(); const [isModal, setIsModal] = useState(false); const { fetchAlarm } = useFetchAlarm(); const { data } = useQuery({ @@ -31,10 +31,10 @@ function MyAlarm() { }); const handleDelete = () => { - if (data.items.length == 0) { - toastInfo('아직 알림이 없습니다.'); - return; - } + if (data.items.length == 0) { + toastInfo('아직 알림이 없습니다.'); + return; + } setIsModal(!isModal); }; diff --git a/src/domains/mypage/components/pages/my-bar/MyBar.tsx b/src/domains/mypage/components/pages/my-bar/MyBar.tsx index 6f24669..521cfa3 100644 --- a/src/domains/mypage/components/pages/my-bar/MyBar.tsx +++ b/src/domains/mypage/components/pages/my-bar/MyBar.tsx @@ -20,7 +20,7 @@ interface MyCocktail { } function MyBar() { - const {toastInfo} =useToast() + const { toastInfo } = useToast(); const [isModal, setIsModal] = useState(false); const { fetchMyBar } = useFetchMyBar(); const { data } = useQuery({ @@ -31,8 +31,8 @@ function MyBar() { const handleDelete = () => { if (data.items.length == 0) { - toastInfo('저장한 칵테일이 없습니다.') - return + toastInfo('저장한 칵테일이 없습니다.'); + return; } setIsModal(!isModal); }; diff --git a/src/domains/recipe/api/fetchRecipe.ts b/src/domains/recipe/api/fetchRecipe.ts index 4d9d37e..91d2092 100644 --- a/src/domains/recipe/api/fetchRecipe.ts +++ b/src/domains/recipe/api/fetchRecipe.ts @@ -1,16 +1,14 @@ -import { getApi } from "@/app/api/config/appConfig"; -import { useAuthStore } from "@/domains/shared/store/auth"; -import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; -import { Cocktail, Sort } from "../types/types"; - - +import { getApi } from '@/app/api/config/appConfig'; +import { useAuthStore } from '@/domains/shared/store/auth'; +import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; +import { Cocktail, Sort } from '../types/types'; interface CocktailResponse { - data: Cocktail[] + data: Cocktail[]; } -interface KeepResponse{ - data: Array<{cocktailId:number}> +interface KeepResponse { + data: Array<{ cocktailId: number }>; } interface SearchFilters { @@ -20,12 +18,10 @@ interface SearchFilters { alcoholBaseTypes: string[]; } - -interface CocktailFilter extends SearchFilters{ - sortBy?:Sort +interface CocktailFilter extends SearchFilters { + sortBy?: Sort; } - const fetchKeep = async (): Promise> => { const res = await fetch(`${getApi}/me/bar`, { method: 'GET', @@ -39,39 +35,34 @@ const fetchKeep = async (): Promise> => { return new Set(myKeep.map((v: { cocktailId: number }) => v.cocktailId)); }; - - const fetchRecipe = async ( +const fetchRecipe = async ( lastId: number | null, size: number, sortBy?: Sort - ): Promise => { - - const url = new URL(`${getApi}/cocktails`) - url.searchParams.set('size',String(size)) - if (lastId !== null) { - url.searchParams.set('lastId', String(lastId)); - url.searchParams.set('lastValue', String(lastId)); - } - - if (sortBy) { - url.searchParams.set('sortBy',String(sortBy)) - } +): Promise => { + const url = new URL(`${getApi}/cocktails`); + url.searchParams.set('size', String(size)); + if (lastId !== null) { + url.searchParams.set('lastId', String(lastId)); + url.searchParams.set('lastValue', String(lastId)); + } + + if (sortBy) { + url.searchParams.set('sortBy', String(sortBy)); + } - const res = await fetch(url.toString(), { - method:'GET' - }) + const res = await fetch(url.toString(), { + method: 'GET', + }); - if (!res.ok) throw new Error('레시피 패치 실패') - - const json:CocktailResponse = await res.json() + if (!res.ok) throw new Error('레시피 패치 실패'); - return json.data ?? [] - } + const json: CocktailResponse = await res.json(); - + return json.data ?? []; +}; const searchCocktails = async (filters: SearchFilters): Promise => { - const body = { keyword: filters.keyword?.trim() ?? '', alcoholStrengths: filters.alcoholStrengths, @@ -84,15 +75,14 @@ const searchCocktails = async (filters: SearchFilters): Promise => { const res = await fetch(`${getApi}/cocktails/search`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body:JSON.stringify(body) - }) + body: JSON.stringify(body), + }); - if(!res.ok) throw new Error('검색 POST 실패') + if (!res.ok) throw new Error('검색 POST 실패'); - const json:CocktailResponse = await res.json() - return json.data ?? [] -} - + const json: CocktailResponse = await res.json(); + return json.data ?? []; +}; const hasActiveFilters = (filters: SearchFilters): boolean => { return !!( @@ -103,59 +93,58 @@ const hasActiveFilters = (filters: SearchFilters): boolean => { ); }; +export const useCocktailsInfiniteQuery = (size: number = 20, sortBy?: Sort) => { + const user = useAuthStore((state) => state.user); + return useInfiniteQuery({ + queryKey: ['cocktails', 'infinite', sortBy, size, user?.id], + queryFn: async ({ pageParam }) => { + const cocktails = await fetchRecipe(pageParam, size, sortBy); -export const useCocktailsInfiniteQuery = ( - size: number = 20, - sortBy?: Sort -) => { - const user = useAuthStore((state) => state.user); - return useInfiniteQuery({ - queryKey: ['cocktails', 'infinite', sortBy, size, user?.id], - queryFn: async ({ pageParam }) => { - const cocktails = await fetchRecipe(pageParam, size, sortBy); - - if (user) { - const keepId = await fetchKeep(); - return cocktails.map((item) => ({ - ...item, - isKeep: keepId.has(item.cocktailId), - })); - } - - return cocktails; - }, - getNextPageParam: (lastpage) => { - if (lastpage.length < size) return undefined; - return lastpage[lastpage.length - 1]?.cocktailId ?? undefined; - }, - initialPageParam: null as number | null, - }); -} + if (user) { + const keepId = await fetchKeep(); + return cocktails.map((item) => ({ + ...item, + isKeep: keepId.has(item.cocktailId), + })); + } + return cocktails; + }, + getNextPageParam: (lastpage) => { + if (lastpage.length < size) return undefined; + return lastpage[lastpage.length - 1]?.cocktailId ?? undefined; + }, + initialPageParam: null as number | null, + }); +}; -export const useCocktailsSearchQuery = (filters:SearchFilters) => { - const user = useAuthStore(state => state.user) - const isActive = hasActiveFilters(filters) +export const useCocktailsSearchQuery = (filters: SearchFilters) => { + const user = useAuthStore((state) => state.user); + const isActive = hasActiveFilters(filters); return useQuery({ queryKey: ['cocktails', 'search', filters, user?.id], queryFn: async () => { - const cocktails = await searchCocktails(filters) + const cocktails = await searchCocktails(filters); if (user && cocktails.length > 0) { - const keepId = await fetchKeep() + const keepId = await fetchKeep(); return cocktails.map((item) => ({ ...item, - isKeep: keepId.has(item.cocktailId) - })) + isKeep: keepId.has(item.cocktailId), + })); } - return cocktails + return cocktails; }, enabled: isActive, - refetchOnMount:false, - }) -} + refetchOnMount: false, + }); +}; -export const useCocktails = (filters: CocktailFilter, infiniteScrollSize: number = 20,sortBy?:Sort) => { +export const useCocktails = ( + filters: CocktailFilter, + infiniteScrollSize: number = 20, + sortBy?: Sort +) => { const isSearchMode = hasActiveFilters(filters); const infiniteQuery = useCocktailsInfiniteQuery(infiniteScrollSize, sortBy); const searchQuery = useCocktailsSearchQuery(filters); diff --git a/src/domains/recipe/api/useRecipeComment.ts b/src/domains/recipe/api/useRecipeComment.ts index 67ffcfa..ce74336 100644 --- a/src/domains/recipe/api/useRecipeComment.ts +++ b/src/domains/recipe/api/useRecipeComment.ts @@ -4,111 +4,114 @@ import { useAuthStore } from '@/domains/shared/store/auth'; import { useToast } from '@/shared/hook/useToast'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; - export const postRecipeComment = async (cocktailId: number, content: string) => { const body = { cocktailId, content, - status:'PUBLIC' - } + status: 'PUBLIC', + }; const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments`, { method: 'POST', headers: { 'Content-type': 'application/json' }, credentials: 'include', - body:JSON.stringify(body) - }) - - const text = await res.text() - const data = JSON.parse(text) - return data -} + body: JSON.stringify(body), + }); + const text = await res.text(); + const data = JSON.parse(text); + return data; +}; export const getRecipeComment = async (cocktailId: number) => { const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments`, { method: 'GET', cache: 'no-store', - credentials:'include' + credentials: 'include', }); const data = await res.json(); if (res.status === 401) return []; - if(!res.ok) throw new Error('댓글 조회 실패') + if (!res.ok) throw new Error('댓글 조회 실패'); const filteredComments = data.data.filter((comment: CommentType) => comment.status !== 'DELETED'); - return filteredComments + return filteredComments; }; -export const updateRecipeComment = async(postId: number, commentId: number, content: string) => { +export const updateRecipeComment = async (postId: number, commentId: number, content: string) => { const res = await fetch(`${getApi}/cocktails/${postId}/comments/${commentId}`, { method: 'PATCH', headers: { - 'Content-Type':'application/json' + 'Content-Type': 'application/json', }, credentials: 'include', - body:JSON.stringify({content}) - }) + body: JSON.stringify({ content }), + }); - if(!res.ok) throw new Error('댓글 수정 실패') -} + if (!res.ok) throw new Error('댓글 수정 실패'); +}; export const deleteRecipeComment = async (cocktailId: number, commentId: number) => { const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments/${commentId}`, { method: 'DELETE', - credentials:'include' - }) - if(!res.ok) throw new Error('댓글 삭제 실패') -} + credentials: 'include', + }); + if (!res.ok) throw new Error('댓글 삭제 실패'); +}; -export function useRecipeComment({cocktailId}:{cocktailId:number}) { - const queryClient = useQueryClient() - const user = useAuthStore(state => state.user) - const {toastInfo,toastError} = useToast() +export function useRecipeComment({ cocktailId }: { cocktailId: number }) { + const queryClient = useQueryClient(); + const user = useAuthStore((state) => state.user); + const { toastInfo, toastError } = useToast(); - const { data:comments=[],refetch,isLoading } = useQuery({ + const { + data: comments = [], + refetch, + isLoading, + } = useQuery({ queryKey: ['comments', cocktailId], queryFn: () => getRecipeComment(cocktailId), staleTime: 30_000, - }) + }); const createMut = useMutation({ mutationFn: (content: string) => { if (!user?.id) { toastInfo('로그인 후 이용 가능합니다.'); - return Promise.reject(new Error('unauth')) + return Promise.reject(new Error('unauth')); } return postRecipeComment(cocktailId, content); }, onSuccess: () => refetch(), onError: (e) => { if (e.message !== 'unauth') { - toastInfo('댓글은 한개만 작성 가능합니다') - } - } + toastInfo('댓글은 한개만 작성 가능합니다'); + } + }, }); const updateMut = useMutation({ - mutationFn: ({ commentId, content }: { - commentId: number, - content: string - }) => updateRecipeComment(cocktailId, commentId, content), - onSuccess: (_, vars) => { - queryClient.setQueryData(['comments',cocktailId], - prev => prev?.map(c => c.commentId === vars.commentId ? - { ...c, content:vars.content } : c - ) ?? prev) + mutationFn: ({ commentId, content }: { commentId: number; content: string }) => + updateRecipeComment(cocktailId, commentId, content), + onSuccess: (_, vars) => { + queryClient.setQueryData( + ['comments', cocktailId], + (prev) => + prev?.map((c) => + c.commentId === vars.commentId ? { ...c, content: vars.content } : c + ) ?? prev + ); }, - onError: () => toastError('수정 중 에러가 발생했습니다.') - + onError: () => toastError('수정 중 에러가 발생했습니다.'), }); const deleteMut = useMutation({ mutationFn: (commentId: number) => deleteRecipeComment(cocktailId, commentId), onSuccess: (_res, commentId) => { - queryClient.setQueryData(['comments', cocktailId], - prev => prev?.filter(c=>c.commentId !== commentId) ?? prev - ) - } - }) - return {createMut,updateMut,deleteMut,comments,refetch,user,isLoading} -} \ No newline at end of file + queryClient.setQueryData( + ['comments', cocktailId], + (prev) => prev?.filter((c) => c.commentId !== commentId) ?? prev + ); + }, + }); + return { createMut, updateMut, deleteMut, comments, refetch, user, isLoading }; +} diff --git a/src/domains/recipe/api/useRecipeDetails.ts b/src/domains/recipe/api/useRecipeDetails.ts index e1cf013..7f43cd9 100644 --- a/src/domains/recipe/api/useRecipeDetails.ts +++ b/src/domains/recipe/api/useRecipeDetails.ts @@ -1,43 +1,41 @@ -import { getApi } from "@/app/api/config/appConfig" -import { useAuthStore } from "@/domains/shared/store/auth" -import { useQuery } from "@tanstack/react-query" +import { getApi } from '@/app/api/config/appConfig'; +import { useAuthStore } from '@/domains/shared/store/auth'; +import { useQuery } from '@tanstack/react-query'; const fetchKeep = async () => { const res = await fetch(`${getApi}/me/bar`, { method: 'GET', - credentials:'include' - }) + credentials: 'include', + }); - if (!res.ok) return new Set() - - const json = await res.json() - const mykeep = json.data + if (!res.ok) return new Set(); - return new Set(mykeep.map((v:{cocktailId:number}) => v.cocktailId)) -} + const json = await res.json(); + const mykeep = json.data; -const fetchRecipe = async (id:number) => { + return new Set(mykeep.map((v: { cocktailId: number }) => v.cocktailId)); +}; + +const fetchRecipe = async (id: number) => { const res = await fetch(`${getApi}/cocktails/${id}`, { - method:'GET' - }) - - if(!res.ok) throw new Error ('상세페이지 fetch 실패') - const json = await res.json() - return json.data ?? [] + method: 'GET', + }); -} + if (!res.ok) throw new Error('상세페이지 fetch 실패'); + const json = await res.json(); + return json.data ?? []; +}; -export const useDetailRecipe = (id:number) => { - const user = useAuthStore(state => state.user) +export const useDetailRecipe = (id: number) => { + const user = useAuthStore((state) => state.user); return useQuery({ - queryKey: ['detail',id, user?.id], + queryKey: ['detail', id, user?.id], queryFn: async () => { - const recipe = await fetchRecipe(id) - const keep = user ? await fetchKeep() : null - const iskept = keep ? keep.has(Number(id)) : false - return { recipe,iskept} - } - }) - -} \ No newline at end of file + const recipe = await fetchRecipe(id); + const keep = user ? await fetchKeep() : null; + const iskept = keep ? keep.has(Number(id)) : false; + return { recipe, iskept }; + }, + }); +}; diff --git a/src/domains/recipe/components/details/BackBtn.tsx b/src/domains/recipe/components/details/BackBtn.tsx index cabca42..af3bc26 100644 --- a/src/domains/recipe/components/details/BackBtn.tsx +++ b/src/domains/recipe/components/details/BackBtn.tsx @@ -4,19 +4,17 @@ import Back from '@/shared/assets/icons/back_36.svg'; import { useSaveScroll } from '../../hook/useSaveScroll'; function BackButton() { - const { restoreAndGoBack } = useSaveScroll({ storageKey: 'cocktail_list_scroll', }); const handleBack = () => { - console.log('뒤로가기 클릭'); - console.log('저장된 스크롤:', sessionStorage.getItem('cocktail_list_scroll')); - console.log('저장된 URL:', sessionStorage.getItem('cocktail_list_scroll_url')); - console.log('복원 플래그:', sessionStorage.getItem('cocktail_list_scroll_restore')); - restoreAndGoBack() - } - + console.log('뒤로가기 클릭'); + console.log('저장된 스크롤:', sessionStorage.getItem('cocktail_list_scroll')); + console.log('저장된 URL:', sessionStorage.getItem('cocktail_list_scroll_url')); + console.log('복원 플래그:', sessionStorage.getItem('cocktail_list_scroll_restore')); + restoreAndGoBack(); + }; return (
      • diff --git a/src/domains/recipe/components/main/CocktailFilter.tsx b/src/domains/recipe/components/main/CocktailFilter.tsx index d9a6286..209b94a 100644 --- a/src/domains/recipe/components/main/CocktailFilter.tsx +++ b/src/domains/recipe/components/main/CocktailFilter.tsx @@ -1,8 +1,6 @@ - import SelectBox from '@/shared/components/select-box/SelectBox'; import { useQueryClient } from '@tanstack/react-query'; -import { useRouter} from 'next/navigation'; - +import { useRouter } from 'next/navigation'; interface Props { cocktailsEA: number; @@ -14,25 +12,21 @@ function CocktailFilter({ cocktailsEA }: Props) { 인기순: 'keeps', 댓글순: 'comments', }; - const queryClient = useQueryClient(); + const queryClient = useQueryClient(); const router = useRouter(); const handleChange = async (selectTitle: string) => { - const sortValue = sortMap[selectTitle as keyof typeof sortMap] + const sortValue = sortMap[selectTitle as keyof typeof sortMap]; queryClient.removeQueries({ queryKey: ['cocktails', 'infinite'], - exact:false - }) - router.push(`?sortBy=${sortValue}`); + exact: false, + }); + router.push(`?sortBy=${sortValue}`); }; return (

        {cocktailsEA}개+

        - +
        ); } diff --git a/src/domains/recipe/components/main/CocktailList.tsx b/src/domains/recipe/components/main/CocktailList.tsx index ba64e3c..3c43da7 100644 --- a/src/domains/recipe/components/main/CocktailList.tsx +++ b/src/domains/recipe/components/main/CocktailList.tsx @@ -9,17 +9,16 @@ interface Props { cocktails: Cocktail[]; } -function CocktailList({cocktails}: Props) { - +function CocktailList({ cocktails }: Props) { const { saveAndNavigate } = useSaveScroll({ storageKey: 'cocktail_list_scroll', }); - -const handleClick = (cocktailId: number) => (e: React.MouseEvent) => { - e.preventDefault(); - saveAndNavigate(`/recipe/${cocktailId}`); -}; + const handleClick = (cocktailId: number) => (e: React.MouseEvent) => { + e.preventDefault(); + + saveAndNavigate(`/recipe/${cocktailId}`); + }; return (
          (e: React.MouseEvent {cocktails.map( - ({ - cocktailImgUrl, - cocktailId, - cocktailName, - cocktailNameKo, - alcoholStrength, - isKeep, - },i) => ( -
        • - + ( + { cocktailImgUrl, cocktailId, cocktailName, cocktailNameKo, alcoholStrength, isKeep }, + i + ) => ( +
        • + void + onChange: (v: string) => void; } -function CocktailSearchBar({ keyword, onChange}: Props) { - +function CocktailSearchBar({ keyword, onChange }: Props) { return ( onChange(e.target.value)} + onChange={(e) => onChange(e.target.value)} variant="search" className="w-full md:max-w-80" /> diff --git a/src/domains/recipe/components/main/Cocktails.tsx b/src/domains/recipe/components/main/Cocktails.tsx index 5b217a9..1a29861 100644 --- a/src/domains/recipe/components/main/Cocktails.tsx +++ b/src/domains/recipe/components/main/Cocktails.tsx @@ -5,51 +5,48 @@ import CocktailFilter from './CocktailFilter'; import CocktailList from './CocktailList'; import Accordion from './Accordion'; import CocktailSearchBar from './CocktailSearchBar'; -import { useCocktails} from '../../api/fetchRecipe'; +import { useCocktails } from '../../api/fetchRecipe'; import { useInView } from 'react-intersection-observer'; import { debounce } from '@/shared/utills/debounce'; import { useSearchParams } from 'next/navigation'; import { Sort } from '../../types/types'; - function Cocktails() { - const searchParams = useSearchParams(); - const sortBy = searchParams.get('sortBy') as Sort -const [keyword,setKeyword] = useState('') -const [input, setInput] = useState(''); + const searchParams = useSearchParams(); + const sortBy = searchParams.get('sortBy') as Sort; + const [keyword, setKeyword] = useState(''); + const [input, setInput] = useState(''); -const [alcoholStrengths,setAlcoholStrengths] = useState([]) -const [alcoholBaseTypes,setAlcoholBaseTypes] = useState([]) -const [cocktailTypes,setCocktailTypes] = useState([]) + const [alcoholStrengths, setAlcoholStrengths] = useState([]); + const [alcoholBaseTypes, setAlcoholBaseTypes] = useState([]); + const [cocktailTypes, setCocktailTypes] = useState([]); - const { - data, - fetchNextPage, - hasNextPage, - noResults, - isSearchMode - } = useCocktails({ - keyword, - alcoholBaseTypes, - alcoholStrengths, - cocktailTypes, - }, 20,sortBy) + const { data, fetchNextPage, hasNextPage, noResults, isSearchMode } = useCocktails( + { + keyword, + alcoholBaseTypes, + alcoholStrengths, + cocktailTypes, + }, + 20, + sortBy + ); const { ref, inView } = useInView({ - threshold:0.1 - }) + threshold: 0.1, + }); useEffect(() => { if (!isSearchMode && inView && hasNextPage) { - fetchNextPage?.() - } - },[inView,hasNextPage,fetchNextPage]) + fetchNextPage?.(); + } + }, [inView, hasNextPage, fetchNextPage]); -const debounceKeyword = useMemo(()=> debounce((v:string)=> setKeyword(v),300),[]) + const debounceKeyword = useMemo(() => debounce((v: string) => setKeyword(v), 300), []); const handleSearch = (v: string) => { - setInput(v) - debounceKeyword(v) - } + setInput(v); + debounceKeyword(v); + }; return (
          @@ -63,17 +60,11 @@ const debounceKeyword = useMemo(()=> debounce((v:string)=> setKeyword(v),300),[]
    - +
    - { - noResults ? ( -
    검색 결과가 없습니다.
    - ): ( - - ) - } + {noResults ?
    검색 결과가 없습니다.
    : }
    -
    +
    ); } diff --git a/src/domains/recipe/hook/useSaveScroll.ts b/src/domains/recipe/hook/useSaveScroll.ts index eaca2c4..8971021 100644 --- a/src/domains/recipe/hook/useSaveScroll.ts +++ b/src/domains/recipe/hook/useSaveScroll.ts @@ -8,24 +8,18 @@ interface Scroll { } export const useSaveScroll = (opt: Scroll = {}) => { - const { - storageKey = 'cocktail_scroll', - enabled = true, - pageType = 'list', - } = opt; + const { storageKey = 'cocktail_scroll', enabled = true, pageType = 'list' } = opt; const router = useRouter(); const hasRestore = useRef(false); useEffect(() => { - - if (pageType === 'detail') return + if (pageType === 'detail') return; - if (!enabled || hasRestore.current) return + if (!enabled || hasRestore.current) return; const savedPosition = sessionStorage.getItem(storageKey); const shouldRestore = sessionStorage.getItem(`${storageKey}_should_restore`); - if (savedPosition && shouldRestore === 'true') { const position = parseInt(savedPosition, 10); @@ -34,13 +28,12 @@ export const useSaveScroll = (opt: Scroll = {}) => { hasRestore.current = true; }; - requestAnimationFrame(restoreScroll); setTimeout(restoreScroll, 0); setTimeout(restoreScroll, 100); sessionStorage.removeItem(`${storageKey}_should_restore`); - } + } }, [storageKey, enabled, pageType]); const saveScroll = () => { From f02f5559f25e03b1a941fb96d266e9328a0a7012 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Wed, 15 Oct 2025 15:07:46 +0900 Subject: [PATCH 08/11] =?UTF-8?q?[refactor]=20=EC=A0=95=EB=A0=AC=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=EC=95=84=EC=9D=B4=ED=85=9C=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/recipe/api/fetchRecipe.ts | 28 ++++++++++++++++--- .../recipe/components/main/CocktailFilter.tsx | 12 ++++---- .../recipe/components/main/Cocktails.tsx | 12 ++++++-- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/domains/recipe/api/fetchRecipe.ts b/src/domains/recipe/api/fetchRecipe.ts index 91d2092..58c510f 100644 --- a/src/domains/recipe/api/fetchRecipe.ts +++ b/src/domains/recipe/api/fetchRecipe.ts @@ -1,7 +1,8 @@ import { getApi } from '@/app/api/config/appConfig'; import { useAuthStore } from '@/domains/shared/store/auth'; -import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; +import { useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query'; import { Cocktail, Sort } from '../types/types'; +import { useEffect, useRef } from 'react'; interface CocktailResponse { data: Cocktail[]; @@ -95,6 +96,18 @@ const hasActiveFilters = (filters: SearchFilters): boolean => { export const useCocktailsInfiniteQuery = (size: number = 20, sortBy?: Sort) => { const user = useAuthStore((state) => state.user); + const queryClient = useQueryClient() + const prevSortBy = useRef(sortBy) + + useEffect(() => { + if (prevSortBy.current !== undefined && prevSortBy.current !== sortBy) { + queryClient.removeQueries({ + queryKey: ['cocktails', 'infinite'], + }); + prevSortBy.current = sortBy; + } + }, [sortBy, queryClient]); + return useInfiniteQuery({ queryKey: ['cocktails', 'infinite', sortBy, size, user?.id], queryFn: async ({ pageParam }) => { @@ -115,6 +128,9 @@ export const useCocktailsInfiniteQuery = (size: number = 20, sortBy?: Sort) => { return lastpage[lastpage.length - 1]?.cocktailId ?? undefined; }, initialPageParam: null as number | null, + refetchOnMount: true, // 마운트 시 재요청 (기본값: true) + refetchOnWindowFocus: true, // 윈도우 포커스 시 재요청 (기본값: true) + refetchOnReconnect: true, // 재연결 시 재요청 }); }; @@ -161,13 +177,17 @@ export const useCocktails = ( } const allCocktails = infiniteQuery.data?.pages.flatMap((page) => page) ?? []; - + const uniqueCocktails = allCocktails.filter( + (cocktail, index, self) => index === self.findIndex((c) => c.cocktailId === cocktail.cocktailId) + ); + + const hasDuplicates = allCocktails.length !== uniqueCocktails.length; return { - data: allCocktails, + data: uniqueCocktails, noResults: false, isSearchMode: false, fetchNextPage: infiniteQuery.fetchNextPage, - hasNextPage: infiniteQuery.hasNextPage, + hasNextPage: hasDuplicates ? false: infiniteQuery.hasNextPage, isFetchingNextPage: infiniteQuery.isFetchingNextPage, }; }; diff --git a/src/domains/recipe/components/main/CocktailFilter.tsx b/src/domains/recipe/components/main/CocktailFilter.tsx index 209b94a..6907d1b 100644 --- a/src/domains/recipe/components/main/CocktailFilter.tsx +++ b/src/domains/recipe/components/main/CocktailFilter.tsx @@ -1,9 +1,9 @@ import SelectBox from '@/shared/components/select-box/SelectBox'; -import { useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; interface Props { cocktailsEA: number; + } function CocktailFilter({ cocktailsEA }: Props) { @@ -12,14 +12,12 @@ function CocktailFilter({ cocktailsEA }: Props) { 인기순: 'keeps', 댓글순: 'comments', }; - const queryClient = useQueryClient(); + const router = useRouter(); - const handleChange = async (selectTitle: string) => { + + const handleChange = (selectTitle: string) => { const sortValue = sortMap[selectTitle as keyof typeof sortMap]; - queryClient.removeQueries({ - queryKey: ['cocktails', 'infinite'], - exact: false, - }); + router.push(`?sortBy=${sortValue}`); }; diff --git a/src/domains/recipe/components/main/Cocktails.tsx b/src/domains/recipe/components/main/Cocktails.tsx index 1a29861..2d09f82 100644 --- a/src/domains/recipe/components/main/Cocktails.tsx +++ b/src/domains/recipe/components/main/Cocktails.tsx @@ -13,14 +13,17 @@ import { Sort } from '../../types/types'; function Cocktails() { const searchParams = useSearchParams(); - const sortBy = searchParams.get('sortBy') as Sort; + const sortByParam = searchParams.get('sortBy') || 'recent' const [keyword, setKeyword] = useState(''); const [input, setInput] = useState(''); + const [sortBy,setSortBy] = useState(sortByParam as Sort) const [alcoholStrengths, setAlcoholStrengths] = useState([]); const [alcoholBaseTypes, setAlcoholBaseTypes] = useState([]); const [cocktailTypes, setCocktailTypes] = useState([]); + + const { data, fetchNextPage, hasNextPage, noResults, isSearchMode } = useCocktails( { keyword, @@ -42,12 +45,17 @@ function Cocktails() { } }, [inView, hasNextPage, fetchNextPage]); + useEffect(() => { + setSortBy(sortByParam as Sort); + }, [sortByParam]); + const debounceKeyword = useMemo(() => debounce((v: string) => setKeyword(v), 300), []); const handleSearch = (v: string) => { setInput(v); debounceKeyword(v); }; + return (
    @@ -59,7 +67,7 @@ function Cocktails() {
    - +
    {noResults ?
    검색 결과가 없습니다.
    : } From 2b825cbff680f19283ed873aa23dcd1c3ab796a1 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Wed, 15 Oct 2025 15:38:40 +0900 Subject: [PATCH 09/11] =?UTF-8?q?[feat]=20=EC=B9=B5=ED=85=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/recipe/api/fetchRecipe.ts | 78 ++++++++++++++++++--------- src/domains/recipe/types/types.ts | 2 + 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/src/domains/recipe/api/fetchRecipe.ts b/src/domains/recipe/api/fetchRecipe.ts index 58c510f..b0d4639 100644 --- a/src/domains/recipe/api/fetchRecipe.ts +++ b/src/domains/recipe/api/fetchRecipe.ts @@ -4,6 +4,7 @@ import { useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-quer import { Cocktail, Sort } from '../types/types'; import { useEffect, useRef } from 'react'; + interface CocktailResponse { data: Cocktail[]; } @@ -23,6 +24,11 @@ interface CocktailFilter extends SearchFilters { sortBy?: Sort; } +interface PageParam { + lastId: number; + lastValue: number | string; +} + const fetchKeep = async (): Promise> => { const res = await fetch(`${getApi}/me/bar`, { method: 'GET', @@ -37,15 +43,15 @@ const fetchKeep = async (): Promise> => { }; const fetchRecipe = async ( - lastId: number | null, + pageParam:PageParam|null, size: number, - sortBy?: Sort + sortBy?: Sort, ): Promise => { const url = new URL(`${getApi}/cocktails`); url.searchParams.set('size', String(size)); - if (lastId !== null) { - url.searchParams.set('lastId', String(lastId)); - url.searchParams.set('lastValue', String(lastId)); + if (pageParam) { + url.searchParams.set('lastId', String(pageParam.lastId)); + url.searchParams.set('lastValue', String(pageParam.lastValue)); } if (sortBy) { @@ -96,18 +102,18 @@ const hasActiveFilters = (filters: SearchFilters): boolean => { export const useCocktailsInfiniteQuery = (size: number = 20, sortBy?: Sort) => { const user = useAuthStore((state) => state.user); - const queryClient = useQueryClient() - const prevSortBy = useRef(sortBy) - - useEffect(() => { - if (prevSortBy.current !== undefined && prevSortBy.current !== sortBy) { - queryClient.removeQueries({ - queryKey: ['cocktails', 'infinite'], - }); - prevSortBy.current = sortBy; - } - }, [sortBy, queryClient]); - + const queryClient = useQueryClient(); + const prevSortBy = useRef(sortBy); + + useEffect(() => { + if (prevSortBy.current !== undefined && prevSortBy.current !== sortBy) { + queryClient.removeQueries({ + queryKey: ['cocktails', 'infinite', prevSortBy.current], + }); + } + prevSortBy.current = sortBy; + }, [sortBy, queryClient]); + return useInfiniteQuery({ queryKey: ['cocktails', 'infinite', sortBy, size, user?.id], queryFn: async ({ pageParam }) => { @@ -123,14 +129,38 @@ export const useCocktailsInfiniteQuery = (size: number = 20, sortBy?: Sort) => { return cocktails; }, - getNextPageParam: (lastpage) => { - if (lastpage.length < size) return undefined; - return lastpage[lastpage.length - 1]?.cocktailId ?? undefined; + getNextPageParam: (lastPage) => { + if (lastPage.length < size) { + return undefined; + } + + const lastItem = lastPage[lastPage.length - 1]; + if (!lastItem) return undefined; + + + let lastValue: number | string; + + switch (sortBy) { + case 'keeps': + lastValue = lastItem.keepCount ?? lastItem.cocktailId; + break; + case 'comments': + lastValue = lastItem.commentCount ?? lastItem.cocktailId; + break; + case 'recent': + default: + lastValue = lastItem.cocktailId; + break; + } + + return { + lastId: lastItem.cocktailId, + lastValue: lastValue, + }; }, - initialPageParam: null as number | null, - refetchOnMount: true, // 마운트 시 재요청 (기본값: true) - refetchOnWindowFocus: true, // 윈도우 포커스 시 재요청 (기본값: true) - refetchOnReconnect: true, // 재연결 시 재요청 + initialPageParam: null as PageParam | null, + refetchOnMount: false, + refetchOnWindowFocus: false, }); }; diff --git a/src/domains/recipe/types/types.ts b/src/domains/recipe/types/types.ts index 2d53040..ea588c7 100644 --- a/src/domains/recipe/types/types.ts +++ b/src/domains/recipe/types/types.ts @@ -5,6 +5,8 @@ export interface Cocktail { cocktailImgUrl: string; cocktailNameKo: string; isKeep: boolean; + keepCount?: number; + commentCount?: number; } export interface RecommendCocktail { From d8cacccba407b3f8722b7292b65744d6ef1340e6 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Wed, 15 Oct 2025 15:58:10 +0900 Subject: [PATCH 10/11] =?UTF-8?q?[fix]=EB=8C=93=EA=B8=80=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/recipe/api/useRecipeComment.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/domains/recipe/api/useRecipeComment.ts b/src/domains/recipe/api/useRecipeComment.ts index ce74336..8ad93b7 100644 --- a/src/domains/recipe/api/useRecipeComment.ts +++ b/src/domains/recipe/api/useRecipeComment.ts @@ -4,6 +4,11 @@ import { useAuthStore } from '@/domains/shared/store/auth'; import { useToast } from '@/shared/hook/useToast'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +interface Comment { + + userNickName: string +} + export const postRecipeComment = async (cocktailId: number, content: string) => { const body = { cocktailId, @@ -18,6 +23,9 @@ export const postRecipeComment = async (cocktailId: number, content: string) => body: JSON.stringify(body), }); + + if (res.status === 401) throw new Error('unauth') + const text = await res.text(); const data = JSON.parse(text); return data; @@ -72,21 +80,20 @@ export function useRecipeComment({ cocktailId }: { cocktailId: number }) { queryFn: () => getRecipeComment(cocktailId), staleTime: 30_000, }); - + console.log(comments) + console.log(user) + const hasComment = comments.some((c:Comment) => c.userNickName === user?.nickname); const createMut = useMutation({ mutationFn: (content: string) => { if (!user?.id) { toastInfo('로그인 후 이용 가능합니다.'); - return Promise.reject(new Error('unauth')); + return Promise.resolve(null) + } else if (hasComment) { + toastInfo('댓글은 한 개만 작성 가능합니다.') } return postRecipeComment(cocktailId, content); }, onSuccess: () => refetch(), - onError: (e) => { - if (e.message !== 'unauth') { - toastInfo('댓글은 한개만 작성 가능합니다'); - } - }, }); const updateMut = useMutation({ From ff02efabf080c807d8d7d77e49b046975b7e32a8 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Wed, 15 Oct 2025 16:00:12 +0900 Subject: [PATCH 11/11] =?UTF-8?q?[chore]=20=EC=B6=A9=EB=8F=8C=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/recipe/api/fetchRecipe.ts | 18 ++++++++---------- src/domains/recipe/api/useRecipeComment.ts | 14 ++++++-------- .../recipe/components/main/Cocktails.tsx | 15 ++++++--------- src/domains/recipe/types/types.ts | 2 +- 4 files changed, 21 insertions(+), 28 deletions(-) diff --git a/src/domains/recipe/api/fetchRecipe.ts b/src/domains/recipe/api/fetchRecipe.ts index b0d4639..d4f0869 100644 --- a/src/domains/recipe/api/fetchRecipe.ts +++ b/src/domains/recipe/api/fetchRecipe.ts @@ -4,7 +4,6 @@ import { useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-quer import { Cocktail, Sort } from '../types/types'; import { useEffect, useRef } from 'react'; - interface CocktailResponse { data: Cocktail[]; } @@ -43,9 +42,9 @@ const fetchKeep = async (): Promise> => { }; const fetchRecipe = async ( - pageParam:PageParam|null, + pageParam: PageParam | null, size: number, - sortBy?: Sort, + sortBy?: Sort ): Promise => { const url = new URL(`${getApi}/cocktails`); url.searchParams.set('size', String(size)); @@ -137,7 +136,6 @@ export const useCocktailsInfiniteQuery = (size: number = 20, sortBy?: Sort) => { const lastItem = lastPage[lastPage.length - 1]; if (!lastItem) return undefined; - let lastValue: number | string; switch (sortBy) { @@ -207,17 +205,17 @@ export const useCocktails = ( } const allCocktails = infiniteQuery.data?.pages.flatMap((page) => page) ?? []; - const uniqueCocktails = allCocktails.filter( - (cocktail, index, self) => index === self.findIndex((c) => c.cocktailId === cocktail.cocktailId) - ); - - const hasDuplicates = allCocktails.length !== uniqueCocktails.length; + const uniqueCocktails = allCocktails.filter( + (cocktail, index, self) => index === self.findIndex((c) => c.cocktailId === cocktail.cocktailId) + ); + + const hasDuplicates = allCocktails.length !== uniqueCocktails.length; return { data: uniqueCocktails, noResults: false, isSearchMode: false, fetchNextPage: infiniteQuery.fetchNextPage, - hasNextPage: hasDuplicates ? false: infiniteQuery.hasNextPage, + hasNextPage: hasDuplicates ? false : infiniteQuery.hasNextPage, isFetchingNextPage: infiniteQuery.isFetchingNextPage, }; }; diff --git a/src/domains/recipe/api/useRecipeComment.ts b/src/domains/recipe/api/useRecipeComment.ts index 7044db8..550c2cf 100644 --- a/src/domains/recipe/api/useRecipeComment.ts +++ b/src/domains/recipe/api/useRecipeComment.ts @@ -5,8 +5,7 @@ import { useToast } from '@/shared/hook/useToast'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; interface Comment { - - userNickName: string + userNickName: string; } export const postRecipeComment = async (cocktailId: number, content: string) => { @@ -23,8 +22,7 @@ export const postRecipeComment = async (cocktailId: number, content: string) => body: JSON.stringify(body), }); - - if (res.status === 401) throw new Error('unauth') + if (res.status === 401) throw new Error('unauth'); const text = await res.text(); const data = JSON.parse(text); @@ -80,15 +78,15 @@ export function useRecipeComment({ cocktailId }: { cocktailId: number }) { queryFn: () => getRecipeComment(cocktailId), staleTime: 30_000, }); - - const hasComment = comments.some((c:Comment) => c.userNickName === user?.nickname); + + const hasComment = comments.some((c: Comment) => c.userNickName === user?.nickname); const createMut = useMutation({ mutationFn: (content: string) => { if (!user?.id) { toastInfo('로그인 후 이용 가능합니다.'); - return Promise.resolve(null) + return Promise.resolve(null); } else if (hasComment) { - toastInfo('댓글은 한 개만 작성 가능합니다.') + toastInfo('댓글은 한 개만 작성 가능합니다.'); } return postRecipeComment(cocktailId, content); }, diff --git a/src/domains/recipe/components/main/Cocktails.tsx b/src/domains/recipe/components/main/Cocktails.tsx index 2d09f82..f3d22ee 100644 --- a/src/domains/recipe/components/main/Cocktails.tsx +++ b/src/domains/recipe/components/main/Cocktails.tsx @@ -13,17 +13,15 @@ import { Sort } from '../../types/types'; function Cocktails() { const searchParams = useSearchParams(); - const sortByParam = searchParams.get('sortBy') || 'recent' + const sortByParam = searchParams.get('sortBy') || 'recent'; const [keyword, setKeyword] = useState(''); const [input, setInput] = useState(''); - const [sortBy,setSortBy] = useState(sortByParam as Sort) + const [sortBy, setSortBy] = useState(sortByParam as Sort); const [alcoholStrengths, setAlcoholStrengths] = useState([]); const [alcoholBaseTypes, setAlcoholBaseTypes] = useState([]); const [cocktailTypes, setCocktailTypes] = useState([]); - - const { data, fetchNextPage, hasNextPage, noResults, isSearchMode } = useCocktails( { keyword, @@ -45,9 +43,9 @@ function Cocktails() { } }, [inView, hasNextPage, fetchNextPage]); - useEffect(() => { - setSortBy(sortByParam as Sort); - }, [sortByParam]); + useEffect(() => { + setSortBy(sortByParam as Sort); + }, [sortByParam]); const debounceKeyword = useMemo(() => debounce((v: string) => setKeyword(v), 300), []); const handleSearch = (v: string) => { @@ -55,7 +53,6 @@ function Cocktails() { debounceKeyword(v); }; - return (
    @@ -67,7 +64,7 @@ function Cocktails() {
    - +
    {noResults ?
    검색 결과가 없습니다.
    : } diff --git a/src/domains/recipe/types/types.ts b/src/domains/recipe/types/types.ts index 0cdf384..22e0354 100644 --- a/src/domains/recipe/types/types.ts +++ b/src/domains/recipe/types/types.ts @@ -6,7 +6,7 @@ export interface Cocktail { cocktailNameKo: string; isKeep: boolean; keepCount?: number; - commentCount?: number; + commentCount?: number; } export interface RecommendCocktail {