diff --git a/src/domains/recipe/components/main/Accordion.tsx b/src/domains/recipe/components/main/Accordion.tsx
index 771c5a1b..286a782d 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 {
@@ -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);
@@ -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 (
-
@@ -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/CocktailFilter.tsx b/src/domains/recipe/components/main/CocktailFilter.tsx
index 589823c6..209b94aa 100644
--- a/src/domains/recipe/components/main/CocktailFilter.tsx
+++ b/src/domains/recipe/components/main/CocktailFilter.tsx
@@ -1,47 +1,32 @@
-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: string;
- setData: Dispatch>;
+ cocktailsEA: number;
}
-function CocktailFilter({ cocktailsEA, setData }: Props) {
+function CocktailFilter({ cocktailsEA }: Props) {
const sortMap = {
최신순: 'recent',
인기순: '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();
- setData(json.data);
- } 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 (
-
{cocktailsEA}개
-
{
- const sortValue = sortMap[value as keyof typeof sortMap];
- handleChange(value);
- router.push(`?sortBy=${sortValue}`);
- }}
- />
+ {cocktailsEA}개+
+
);
}
diff --git a/src/domains/recipe/components/main/CocktailList.tsx b/src/domains/recipe/components/main/CocktailList.tsx
index 0b2f7e63..3c43da7b 100644
--- a/src/domains/recipe/components/main/CocktailList.tsx
+++ b/src/domains/recipe/components/main/CocktailList.tsx
@@ -1,35 +1,25 @@
'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';
+import { useSaveScroll } from '../../hook/useSaveScroll';
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();
- }
+function CocktailList({ cocktails }: Props) {
+ const { saveAndNavigate } = useSaveScroll({
+ storageKey: 'cocktail_list_scroll',
+ });
+
+ const handleClick = (cocktailId: number) => (e: React.MouseEvent) => {
+ e.preventDefault();
+
+ saveAndNavigate(`/recipe/${cocktailId}`);
};
- useIntersectionObserver(cocktailRef, onIntersect, hasNextPage);
- const saveScroll = useScrollRestore({
- lastId,
- fetchData: RecipeFetch!,
- hasNextPage,
- currentDataLength: cocktails.length,
- });
return (
{cocktails.map(
- ({
- cocktailImgUrl,
- cocktailId,
- cocktailName,
- cocktailNameKo,
- alcoholStrength,
- isFavorited,
- }) => (
- -
-
+ (
+ { cocktailImgUrl, cocktailId, cocktailName, cocktailNameKo, alcoholStrength, isKeep },
+ i
+ ) => (
+
-
+
)
)}
-
);
}
diff --git a/src/domains/recipe/components/main/CocktailSearchBar.tsx b/src/domains/recipe/components/main/CocktailSearchBar.tsx
index 5850054e..96247815 100644
--- a/src/domains/recipe/components/main/CocktailSearchBar.tsx
+++ b/src/domains/recipe/components/main/CocktailSearchBar.tsx
@@ -1,16 +1,16 @@
import Input from '@/shared/components/Input-box/Input';
interface Props {
- value: string;
+ keyword: string;
onChange: (v: string) => void;
}
-function CocktailSearchBar({ value, onChange }: Props) {
+function CocktailSearchBar({ keyword, onChange }: Props) {
return (
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 a63dc93f..1a298611 100644
--- a/src/domains/recipe/components/main/Cocktails.tsx
+++ b/src/domains/recipe/components/main/Cocktails.tsx
@@ -1,76 +1,52 @@
'use client';
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, 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';
+import { debounce } from '@/shared/utills/debounce';
+import { useSearchParams } from 'next/navigation';
+import { Sort } from '../../types/types';
function Cocktails() {
- const user = useAuthStore((state) => state.user);
+ const searchParams = useSearchParams();
+ const sortBy = searchParams.get('sortBy') as Sort;
+ const [keyword, setKeyword] = useState('');
+ const [input, setInput] = useState('');
- const [data, setData] = useState([]);
- const [lastId, setLastId] = useState(null);
- const [hasNextPage, setHasNextPage] = useState(true);
+ const [alcoholStrengths, setAlcoholStrengths] = useState([]);
+ const [alcoholBaseTypes, setAlcoholBaseTypes] = useState([]);
+ const [cocktailTypes, setCocktailTypes] = useState([]);
- const { inputValue, keyword, isSearching, onInputChange, noResults, setNoResults } =
- useSearchControl({ delay: 300, storageKey: 'cocktails_scoll_state' });
- const { fetchData } = RecipeFetch({ setData, lastId, setLastId, hasNextPage, setHasNextPage });
+ const { data, fetchNextPage, hasNextPage, noResults, isSearchMode } = useCocktails(
+ {
+ keyword,
+ alcoholBaseTypes,
+ alcoholStrengths,
+ cocktailTypes,
+ },
+ 20,
+ sortBy
+ );
- const {
- searchApi,
- setAlcoholBaseTypes,
- setAlcoholStrengths,
- setCocktailTypes,
- alcoholBaseTypes,
- cocktailTypes,
- alcoholStrengths,
- } = CocktailSearch({
- setData,
- setNoResults,
+ const { ref, inView } = useInView({
+ threshold: 0.1,
});
- const countLabel = isSearching
- ? hasNextPage
- ? `검색결과 현재 ${data.length}+`
- : `검색결과 총 ${data.length}`
- : hasNextPage
- ? `전체 ${data.length}+`
- : `전체 ${data.length}`;
-
- // 초기 로드 시 검색어가 있으면 검색 실행
- // useEffect(() => {
- // const readyForFirstLoad = !isSearching && hasNextPage && lastId == null && data.length === 0;
-
- // 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);
+ if (!isSearchMode && inView && hasNextPage) {
+ fetchNextPage?.();
}
- }, [keyword, isSearching, alcoholBaseTypes, alcoholStrengths, cocktailTypes]);
+ }, [inView, hasNextPage, fetchNextPage]);
- // 일반 fetch
- useEffect(() => {
- if (isSearching) return;
- fetchData();
- }, [isSearching, alcoholBaseTypes, alcoholStrengths, cocktailTypes]);
+ const debounceKeyword = useMemo(() => debounce((v: string) => setKeyword(v), 300), []);
+ const handleSearch = (v: string) => {
+ setInput(v);
+ debounceKeyword(v);
+ };
return (
@@ -80,23 +56,15 @@ function Cocktails() {
setAlcoholStrengths={setAlcoholStrengths}
setCocktailTypes={setCocktailTypes}
/>
-
+
-
(
- 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 87bd13fd..00000000
--- 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;
-}