From 1b4850b0e1fca18f57f108b7022a1d144905a517 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Sun, 5 Oct 2025 16:16:11 +0900 Subject: [PATCH 1/9] =?UTF-8?q?[feat]=EC=B9=B5=ED=85=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.ts | 7 +- src/app/community/page.tsx | 2 +- src/app/login/page.tsx | 2 +- src/app/mypage/my-active/my-comment/page.tsx | 2 +- src/app/mypage/my-active/my-like/page.tsx | 2 +- src/app/mypage/my-active/my-post/page.tsx | 2 +- src/app/mypage/my-alarm/page.tsx | 2 +- src/app/mypage/my-bar/page.tsx | 2 +- src/app/recipe/page.tsx | 15 +-- src/domains/recipe/api/CocktailSearch.tsx | 30 ++++++ src/domains/recipe/api/RecipeFetch.tsx | 49 ++++++++++ .../recipe/components/main/Accordion.tsx | 2 +- .../{ => components}/main/CocktailFilter.tsx | 4 +- .../{ => components}/main/CocktailList.tsx | 7 +- .../components/main/CocktailSearchBar.tsx | 20 ++++ .../recipe/components/main/Cocktails.tsx | 94 +++++++++++++++++++ src/domains/recipe/details/DetailItem.tsx | 2 +- src/domains/recipe/details/DetailRecipe.tsx | 2 +- .../recipe/{details => }/hook/ozToMl.ts | 0 .../recipe/{details => }/hook/useGlass.tsx | 0 src/domains/recipe/hook/useSearchControl.tsx | 67 +++++++++++++ src/domains/recipe/main/Cocktails.tsx | 89 ------------------ src/shared/components/Input-box/Input.tsx | 11 +-- 23 files changed, 288 insertions(+), 125 deletions(-) create mode 100644 src/domains/recipe/api/CocktailSearch.tsx create mode 100644 src/domains/recipe/api/RecipeFetch.tsx rename src/domains/recipe/{ => components}/main/CocktailFilter.tsx (74%) rename src/domains/recipe/{ => components}/main/CocktailList.tsx (91%) create mode 100644 src/domains/recipe/components/main/CocktailSearchBar.tsx create mode 100644 src/domains/recipe/components/main/Cocktails.tsx rename src/domains/recipe/{details => }/hook/ozToMl.ts (100%) rename src/domains/recipe/{details => }/hook/useGlass.tsx (100%) create mode 100644 src/domains/recipe/hook/useSearchControl.tsx delete mode 100644 src/domains/recipe/main/Cocktails.tsx diff --git a/next.config.ts b/next.config.ts index 5a1ade85..dca67ead 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,7 +2,12 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { images: { - domains: ['www.thecocktaildb.com'], + remotePatterns: [ + { + protocol: 'https', + hostname:'www.thecocktaildb.com' + } + ] }, env: { NPUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, diff --git a/src/app/community/page.tsx b/src/app/community/page.tsx index 86fbf54a..5a5ec3ba 100644 --- a/src/app/community/page.tsx +++ b/src/app/community/page.tsx @@ -3,7 +3,7 @@ import PageHeader from '@/domains/shared/components/page-header/PageHeader'; import { Metadata } from 'next'; export const metadata: Metadata = { - title: 'SSOUL | 커뮤니티', + title: '커뮤니티', description: '칵테일에 관한 모든 이야기', }; diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index d2548bbd..eaef7d76 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -4,7 +4,7 @@ import loginBg from '@/shared/assets/images/login_bg.webp'; import SocialLogin from '@/domains/login/main/SocialLogin'; export const metadata: Metadata = { - title: 'SSOUL | 로그인', + title: '로그인', description: '칵테일을 좋아하는 사람들을 위한 서비스 로그인 페이지', }; diff --git a/src/app/mypage/my-active/my-comment/page.tsx b/src/app/mypage/my-active/my-comment/page.tsx index c69e1e6d..538352c7 100644 --- a/src/app/mypage/my-active/my-comment/page.tsx +++ b/src/app/mypage/my-active/my-comment/page.tsx @@ -2,7 +2,7 @@ import { Metadata } from 'next'; export const metadata: Metadata = { - title: 'SSOUL | 마이페이지', + title: '마이페이지', description: 'SSOUL 서비스에서 나의 활동을 관리할 수 있는 페이지입니다', }; diff --git a/src/app/mypage/my-active/my-like/page.tsx b/src/app/mypage/my-active/my-like/page.tsx index 0a8e73e1..08fd76e4 100644 --- a/src/app/mypage/my-active/my-like/page.tsx +++ b/src/app/mypage/my-active/my-like/page.tsx @@ -2,7 +2,7 @@ import { Metadata } from 'next'; export const metadata: Metadata = { - title: 'SSOUL | 마이페이지', + title: '마이페이지', description: 'SSOUL 서비스에서 나의 활동을 관리할 수 있는 페이지입니다', }; diff --git a/src/app/mypage/my-active/my-post/page.tsx b/src/app/mypage/my-active/my-post/page.tsx index 70c47d80..ee02f84e 100644 --- a/src/app/mypage/my-active/my-post/page.tsx +++ b/src/app/mypage/my-active/my-post/page.tsx @@ -2,7 +2,7 @@ import { Metadata } from 'next'; export const metadata: Metadata = { - title: 'SSOUL | 마이페이지', + title: '마이페이지', description: 'SSOUL 서비스에서 나의 활동을 관리할 수 있는 페이지입니다', }; diff --git a/src/app/mypage/my-alarm/page.tsx b/src/app/mypage/my-alarm/page.tsx index ec9e77c1..61632edc 100644 --- a/src/app/mypage/my-alarm/page.tsx +++ b/src/app/mypage/my-alarm/page.tsx @@ -3,7 +3,7 @@ import Alarm from '@/domains/mypage/components/Alarm'; import { Metadata } from 'next'; export const metadata: Metadata = { - title: 'SSOUL | 마이페이지', + title: '마이페이지', description: 'SSOUL 서비스에서 나의 활동을 관리할 수 있는 페이지입니다', }; diff --git a/src/app/mypage/my-bar/page.tsx b/src/app/mypage/my-bar/page.tsx index 4c12cc42..f7f713c9 100644 --- a/src/app/mypage/my-bar/page.tsx +++ b/src/app/mypage/my-bar/page.tsx @@ -2,7 +2,7 @@ import CocktailCard from '@/domains/shared/components/cocktail-card/CocktailCard import { Metadata } from 'next'; export const metadata: Metadata = { - title: 'SSOUL | 마이페이지', + title: '마이페이지', description: 'SSOUL 서비스에서 나의 활동을 관리할 수 있는 페이지입니다', }; diff --git a/src/app/recipe/page.tsx b/src/app/recipe/page.tsx index 06cb569c..5ab5d9d1 100644 --- a/src/app/recipe/page.tsx +++ b/src/app/recipe/page.tsx @@ -1,14 +1,12 @@ import { Metadata } from 'next'; -import Input from '@/shared/components/Input-box/Input'; -import Accordion from '../../domains/recipe/components/main/Accordion'; import PageHeader from '@/domains/shared/components/page-header/PageHeader'; import { Suspense } from 'react'; import SkeletonRecipe from '@/domains/recipe/skeleton/SkeletonRecipe'; -import Cocktails from '@/domains/recipe/main/Cocktails'; +import Cocktails from '@/domains/recipe/components/main/Cocktails'; export const metadata: Metadata = { - title: 'SSOUL | 칵테일레시피', + title: '칵테일레시피', description: '칵테일 레시피가 궁금하신 분들을 위한 레시피 페이지', }; @@ -20,15 +18,6 @@ function Page() {
}> -
- - -
diff --git a/src/domains/recipe/api/CocktailSearch.tsx b/src/domains/recipe/api/CocktailSearch.tsx new file mode 100644 index 00000000..c6112b26 --- /dev/null +++ b/src/domains/recipe/api/CocktailSearch.tsx @@ -0,0 +1,30 @@ +import { getApi } from '@/app/api/config/appConfig'; +import { Cocktail } from '../types/types'; +import { Dispatch, SetStateAction } from 'react'; + +interface Props { + setData: Dispatch>; + setNoResults: Dispatch>; +} + +function CocktailSearch({ setData, setNoResults }: Props) { + const searchApi = async (v: string) => { + const keyword = v.trim(); + if (!keyword) { + setData([]); + return; + } + + const res = await fetch(`${getApi}/cocktails/search`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ keyword }), + }); + const json = await res.json(); + setData(json.data); + setNoResults(json.data.length === 0); + }; + + return { searchApi }; +} +export default CocktailSearch; diff --git a/src/domains/recipe/api/RecipeFetch.tsx b/src/domains/recipe/api/RecipeFetch.tsx new file mode 100644 index 00000000..8ebcbac7 --- /dev/null +++ b/src/domains/recipe/api/RecipeFetch.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { getApi } from '@/app/api/config/appConfig'; +import { Cocktail } from '../types/types'; +import { Dispatch, SetStateAction, useCallback } from 'react'; + +interface Props { + setData: React.Dispatch>; + lastId: number | null; + setLastId: Dispatch>; + hasNextPage: boolean; + setHasNextPage: Dispatch>; + SIZE?: number; +} + +export const RecipeFetch = ({ + setData, + lastId, + setLastId, + hasNextPage, + setHasNextPage, + SIZE = 20, +}: Props) => { + 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)); + } + + const res = await fetch(url.toString()); + if (!res.ok) throw new Error('레시피 데이터 요청실패'); + + const json = await res.json(); + const list: Cocktail[] = json.data ?? []; + + 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/components/main/Accordion.tsx b/src/domains/recipe/components/main/Accordion.tsx index 4364e627..18f803c8 100644 --- a/src/domains/recipe/components/main/Accordion.tsx +++ b/src/domains/recipe/components/main/Accordion.tsx @@ -5,7 +5,7 @@ import SelectBox from '@/shared/components/select-box/SelectBox'; const SELECT_OPTIONS = [ { id: 'abv', - option: ['전체', '약한 도수', '가벼운 도수', '중간 도수', '센 도수', '매우 센 도수'], + option: ['전체', '논알콜', '약한 도수', '가벼운 도수', '중간 도수', '센 도수', '매우 센 도수'], title: '도수', }, { diff --git a/src/domains/recipe/main/CocktailFilter.tsx b/src/domains/recipe/components/main/CocktailFilter.tsx similarity index 74% rename from src/domains/recipe/main/CocktailFilter.tsx rename to src/domains/recipe/components/main/CocktailFilter.tsx index 79205d68..5d2c4a0b 100644 --- a/src/domains/recipe/main/CocktailFilter.tsx +++ b/src/domains/recipe/components/main/CocktailFilter.tsx @@ -1,9 +1,9 @@ import SelectBox from '@/shared/components/select-box/SelectBox'; -function CocktailFilter({ cocktailsEA }: { cocktailsEA: number[] }) { +function CocktailFilter({ cocktailsEA }: { cocktailsEA: string }) { return (
-

{cocktailsEA[0]}개

+

{cocktailsEA}개

); diff --git a/src/domains/recipe/main/CocktailList.tsx b/src/domains/recipe/components/main/CocktailList.tsx similarity index 91% rename from src/domains/recipe/main/CocktailList.tsx rename to src/domains/recipe/components/main/CocktailList.tsx index a9b79ff3..9b921678 100644 --- a/src/domains/recipe/main/CocktailList.tsx +++ b/src/domains/recipe/components/main/CocktailList.tsx @@ -3,12 +3,12 @@ import { useRef } from 'react'; import Link from 'next/link'; import { useIntersectionObserver } from '@/domains/shared/hook/useIntersectionObserver'; -import { Cocktail } from '../types/types'; +import { Cocktail } from '../../types/types'; import CocktailCard from '@/domains/shared/components/cocktail-card/CocktailCard'; interface Props { cocktails: Cocktail[]; - RecipeFetch: (cursor?: string | undefined) => Promise; + RecipeFetch?: (cursor?: string | undefined) => Promise; hasNextPage: boolean; lastId: number | null; onItemClick: () => void; @@ -17,8 +17,9 @@ interface Props { function CocktailList({ cocktails, RecipeFetch, hasNextPage, lastId, onItemClick }: Props) { const cocktailRef = useRef(null); const onIntersect: IntersectionObserverCallback = ([entry]) => { + if (!RecipeFetch) return; if (!lastId) return; - if (entry.isIntersecting && lastId > 1) { + if (entry.isIntersecting) { RecipeFetch(); } }; diff --git a/src/domains/recipe/components/main/CocktailSearchBar.tsx b/src/domains/recipe/components/main/CocktailSearchBar.tsx new file mode 100644 index 00000000..5850054e --- /dev/null +++ b/src/domains/recipe/components/main/CocktailSearchBar.tsx @@ -0,0 +1,20 @@ +import Input from '@/shared/components/Input-box/Input'; + +interface Props { + value: string; + onChange: (v: string) => void; +} + +function CocktailSearchBar({ value, onChange }: Props) { + return ( + onChange(e.target.value)} + variant="search" + className="w-full md:max-w-80" + /> + ); +} +export default CocktailSearchBar; diff --git a/src/domains/recipe/components/main/Cocktails.tsx b/src/domains/recipe/components/main/Cocktails.tsx new file mode 100644 index 00000000..a7128d9f --- /dev/null +++ b/src/domains/recipe/components/main/Cocktails.tsx @@ -0,0 +1,94 @@ +'use client'; + +import { useEffect } from 'react'; +import CocktailFilter from './CocktailFilter'; +import CocktailList from './CocktailList'; +import { Cocktail } from '../../types/types'; +import { useMemoScroll } from '../../../shared/hook/useMemoScroll'; +import Accordion from './Accordion'; +import { RecipeFetch } from '../../api/RecipeFetch'; +import CocktailSearchBar from './CocktailSearchBar'; +import useSearchControl from '../../hook/useSearchControl'; +import CocktailSearch from '../../api/CocktailSearch'; + +function Cocktails() { + const { + data, + setData, + lastId, + setLastId, + hasNextPage, + setHasNextPage, + handleItemClick, + shouldFetch, + } = useMemoScroll({ + storageKey: 'cocktails_scroll_state', + eventName: 'resetCocktailsScroll', + }); + const { inputValue, keyword, isSearching, onInputChange, noResults, setNoResults } = + useSearchControl({ delay: 300, storageKey: 'cocktails_scoll_state' }); + const { fetchData } = RecipeFetch({ setData, lastId, setLastId, hasNextPage, setHasNextPage }); + const { searchApi } = CocktailSearch({ setData, setNoResults }); + 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]); + + // 검색어 변경 시 + useEffect(() => { + if (isSearching && keyword.trim()) { + setLastId(null); + setHasNextPage(false); + searchApi(keyword.trim()); + } else if (!isSearching) { + // 검색어를 지웠을 때만 초기화 + setData([]); + setLastId(null); + setHasNextPage(true); + } + }, [keyword, isSearching]); + + // 일반 fetch + useEffect(() => { + if (!shouldFetch) return; + if (isSearching) return; + fetchData(); + }, [shouldFetch, isSearching]); + + return ( +
+
+ + +
+ +
+ {isSearching && noResults ? ( +
검색결과가 없습니다.
+ ) : ( + + )} +
+
+ ); +} + +export default Cocktails; diff --git a/src/domains/recipe/details/DetailItem.tsx b/src/domains/recipe/details/DetailItem.tsx index 1eba2c14..88c17c5f 100644 --- a/src/domains/recipe/details/DetailItem.tsx +++ b/src/domains/recipe/details/DetailItem.tsx @@ -2,7 +2,7 @@ import Image from 'next/image'; import Label from '@/domains/shared/components/label/Label'; import AbvGraph from '@/domains/shared/components/abv-graph/AbvGraph'; import { labelTitle } from '../utills/labelTitle'; -import useGlass from './hook/useGlass'; +import useGlass from '../hook/useGlass'; interface Props { name: string; diff --git a/src/domains/recipe/details/DetailRecipe.tsx b/src/domains/recipe/details/DetailRecipe.tsx index aa4c7e54..810ce7b1 100644 --- a/src/domains/recipe/details/DetailRecipe.tsx +++ b/src/domains/recipe/details/DetailRecipe.tsx @@ -1,4 +1,4 @@ -import { ozToMl } from './hook/ozToMl'; +import { ozToMl } from '../hook/ozToMl'; type Recipe = { ingredientName: string; diff --git a/src/domains/recipe/details/hook/ozToMl.ts b/src/domains/recipe/hook/ozToMl.ts similarity index 100% rename from src/domains/recipe/details/hook/ozToMl.ts rename to src/domains/recipe/hook/ozToMl.ts diff --git a/src/domains/recipe/details/hook/useGlass.tsx b/src/domains/recipe/hook/useGlass.tsx similarity index 100% rename from src/domains/recipe/details/hook/useGlass.tsx rename to src/domains/recipe/hook/useGlass.tsx diff --git a/src/domains/recipe/hook/useSearchControl.tsx b/src/domains/recipe/hook/useSearchControl.tsx new file mode 100644 index 00000000..a00e40f1 --- /dev/null +++ b/src/domains/recipe/hook/useSearchControl.tsx @@ -0,0 +1,67 @@ +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/recipe/main/Cocktails.tsx b/src/domains/recipe/main/Cocktails.tsx deleted file mode 100644 index 697bb095..00000000 --- a/src/domains/recipe/main/Cocktails.tsx +++ /dev/null @@ -1,89 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import CocktailFilter from './CocktailFilter'; -import CocktailList from './CocktailList'; -import { getApi } from '@/app/api/config/appConfig'; -import { Cocktail } from '../types/types'; -import { useMemoScroll } from '../../shared/hook/useMemoScroll'; - -function Cocktails() { - const SIZE = 20; - const [isLoading, setIsLoading] = useState(false); - - const { - data, - setData, - lastId, - setLastId, - hasNextPage, - setHasNextPage, - handleItemClick, - shouldFetch, - } = useMemoScroll({ - storageKey: 'cocktails_scroll_state', - eventName: 'resetCocktailsScroll', - }); - - const num = data.map((a) => a.cocktailId); - - const RecipeFetch = async () => { - if (isLoading || !hasNextPage) { - return; - } - - setIsLoading(true); - - try { - const url = new URL(`${getApi}/cocktails`); - url.searchParams.set('size', String(SIZE)); - if (typeof lastId === 'number') { - url.searchParams.set('lastId', String(lastId)); - } - - const res = await fetch(url.toString()); - if (!res.ok) throw new Error('레시피 데이터 요청실패'); - - const json = await res.json(); - const list: Cocktail[] = json.data ?? []; - - 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); - } catch (err) { - console.error(err); - } finally { - setIsLoading(false); - } - }; - - // shouldFetch가 true일 때 fetch 실행 - useEffect(() => { - if (shouldFetch && data.length === 0) { - RecipeFetch(); - } - }, [shouldFetch]); - - return ( -
- -
- -
-
- ); -} - -export default Cocktails; diff --git a/src/shared/components/Input-box/Input.tsx b/src/shared/components/Input-box/Input.tsx index 6b703c75..d8bf2b44 100644 --- a/src/shared/components/Input-box/Input.tsx +++ b/src/shared/components/Input-box/Input.tsx @@ -4,21 +4,16 @@ import { cva } from 'class-variance-authority'; import { HTMLInputTypeAttribute, Ref } from 'react'; import Search from '@/shared/assets/icons/search_32.svg'; import Button from '../button/Button'; -// select나올떄 자연스러운 처리 화살표 로테이트 [x] -// 인풋 타입받을 수 있게 수정 [x] -// 인풋접근성 라벨이 중요함 라벨 을 div에 묶어서 하거나 label로 인풋감싸거나 div로 묶고 같은 선상에두게 [x] -// div안에 라벨이랑감싸기 [x] -// 텍스트 에어리어 버전도 만들기 -// 인풋 잘림 = 라인height 인풋 높이랑 맞춰두기 [x] interface Props { placeholder: string; + value?: string; type?: HTMLInputTypeAttribute; ref?: Ref; size?: 'default' | 'lg'; variant?: 'default' | 'search' | 'comment'; className?: string; - onChange?: () => void; + onChange?: (e: React.ChangeEvent) => void; id: string; } @@ -44,6 +39,7 @@ function Input({ size, variant = 'default', className, + value, id, onChange, ...rest @@ -54,6 +50,7 @@ function Input({ Date: Mon, 6 Oct 2025 00:49:51 +0900 Subject: [PATCH 2/9] =?UTF-8?q?[feat]=EC=B9=B5=ED=85=8C=EC=9D=BC=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=EB=A7=81=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/CocktailSearch.tsx | 42 +++++- .../recipe/components/details/BackBtn.tsx | 16 ++- .../recipe/components/main/Accordion.tsx | 134 +++++++++++++++++- .../recipe/components/main/Cocktails.tsx | 29 +++- .../components/select-box/SelectBox.tsx | 27 ++-- 5 files changed, 211 insertions(+), 37 deletions(-) diff --git a/src/domains/recipe/api/CocktailSearch.tsx b/src/domains/recipe/api/CocktailSearch.tsx index c6112b26..aacd03fc 100644 --- a/src/domains/recipe/api/CocktailSearch.tsx +++ b/src/domains/recipe/api/CocktailSearch.tsx @@ -1,6 +1,7 @@ +'use client'; import { getApi } from '@/app/api/config/appConfig'; import { Cocktail } from '../types/types'; -import { Dispatch, SetStateAction } from 'react'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; interface Props { setData: Dispatch>; @@ -8,23 +9,50 @@ interface Props { } function CocktailSearch({ setData, setNoResults }: Props) { - const searchApi = async (v: string) => { - const keyword = v.trim(); - if (!keyword) { + 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([]); - return; + setNoResults(false); + return null; } const res = await fetch(`${getApi}/cocktails/search`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ keyword }), + body: JSON.stringify(body), }); const json = await res.json(); + setData(json.data); setNoResults(json.data.length === 0); }; - return { searchApi }; + useEffect(() => { + searchApi(); + }, [alcoholStrengths, cocktailTypes, alcoholBaseTypes]); + + return { + searchApi, + setAlcoholBaseTypes, + setAlcoholStrengths, + setCocktailTypes, + alcoholBaseTypes, + alcoholStrengths, + cocktailTypes, + }; } export default CocktailSearch; diff --git a/src/domains/recipe/components/details/BackBtn.tsx b/src/domains/recipe/components/details/BackBtn.tsx index 1a8fa46b..ebb263f8 100644 --- a/src/domains/recipe/components/details/BackBtn.tsx +++ b/src/domains/recipe/components/details/BackBtn.tsx @@ -1,14 +1,22 @@ 'use client'; -import Back from '@/shared/assets/icons/back_36.svg'; + +import { useSearchParams } from 'next/navigation'; import Link from 'next/link'; +import Back from '@/shared/assets/icons/back_36.svg'; + +function BackButton() { + const searchParams = useSearchParams(); + const recipeUrl = `/recipe${searchParams.toString() ? `?${searchParams.toString()}` : ''}`; + + // 쿼리스트링을 유지한채 뒤로 돌아가야함 -function BackBtn() { return ( ); } -export default BackBtn; + +export default BackButton; diff --git a/src/domains/recipe/components/main/Accordion.tsx b/src/domains/recipe/components/main/Accordion.tsx index 18f803c8..bf5aacd4 100644 --- a/src/domains/recipe/components/main/Accordion.tsx +++ b/src/domains/recipe/components/main/Accordion.tsx @@ -1,36 +1,162 @@ 'use client'; import SelectBox from '@/shared/components/select-box/SelectBox'; +import { Dispatch, SetStateAction, useEffect, useMemo } from 'react'; +import { useRouter, useSearchParams, usePathname } from 'next/navigation'; + +interface Props { + setAlcoholBaseTypes: Dispatch>; + setAlcoholStrengths: Dispatch>; + setCocktailTypes: Dispatch>; +} const SELECT_OPTIONS = [ { id: 'abv', - option: ['전체', '논알콜', '약한 도수', '가벼운 도수', '중간 도수', '센 도수', '매우 센 도수'], + option: [ + '전체', + '논알콜', + '약한 도수 (1~5%)', + '가벼운 도수 (6~15%)', + '중간 도수(16~25%)', + '센 도수(26~36%)', + '매우 센 도수(36%~)', + ], + map: { + 전체: null, + 논알콜: 'NON_ALCOHOLIC', + '약한 도수 (1~5%)': 'WEAK', + '가벼운 도수 (6~15%)': 'LIGHT', + '중간 도수(16~25%)': 'MEDIUM', + '센 도수(26~36%)': 'STRONG', + '매우 센 도수(36%~)': 'VERY_STRONG', + }, title: '도수', }, { id: 'base', - option: ['전체', '위스키', '진', '럼', '보드카', '데킬라', '리큐르'], + option: ['전체', '위스키', '브랜디', '진', '럼', '보드카', '데킬라', '리큐르', '와인', '기타'], + map: { + 전체: null, + 진: 'GIN', + 럼: 'RUM', + 보드카: 'VODKA', + 위스키: 'WHISKY', + 데킬라: 'TEQUILA', + 리큐르: 'LIQUEUR', + 브랜디: 'BRANDY', + 와인: 'WINE', + 기타: 'OTHER', + }, title: '베이스', }, { id: 'glass', option: ['전체', '클래식', '롱', '슈터', '숏'], + map: { + 전체: null, + 숏: 'SHORT', + 롱: 'LONG', + 클래식: 'CLASSIC', + 슈터: 'SHOOTER', + }, title: '글라스', }, ]; -function Accordion() { +function Accordion({ setAlcoholBaseTypes, setCocktailTypes, setAlcoholStrengths }: Props) { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + // 코드를 한글 라벨로 역변환하는 함수 + const getDisplayValue = (id: string, code: string | null): string => { + if (!code) return '전체'; + + const optionGroup = SELECT_OPTIONS.find((opt) => opt.id === id); + if (!optionGroup) return '전체'; + + // map 객체에서 code와 일치하는 key(한글)를 찾기 + const entry = Object.entries(optionGroup.map).find(([_, value]) => value === code); + return entry ? entry[0] : '전체'; + }; + + // URL 파라미터에서 현재 선택된 값 가져오기 + const currentValues = useMemo(() => { + return { + abv: getDisplayValue('abv', searchParams.get('abv')), + base: getDisplayValue('base', searchParams.get('base')), + glass: getDisplayValue('glass', searchParams.get('glass')), + }; + }, [searchParams]); + + // URL에서 상태 동기화 + useEffect(() => { + const abv = searchParams.get('abv'); + const base = searchParams.get('base'); + const glass = searchParams.get('glass'); + + setAlcoholStrengths(abv ? [abv] : []); + setAlcoholBaseTypes(base ? [base] : []); + setCocktailTypes(glass ? [glass] : []); + }, [searchParams, setAlcoholStrengths, setAlcoholBaseTypes, setCocktailTypes]); + + const handleSelect = (id: string, value: string) => { + const optionGroup = SELECT_OPTIONS.find((opt) => opt.id === id); + if (!optionGroup) return; + + const code = optionGroup.map[value as keyof typeof optionGroup.map] ?? null; + const arr = code ? [code] : []; + + // 상태 즉시 업데이트 + switch (id) { + case 'abv': + setAlcoholStrengths(arr); + break; + case 'base': + setAlcoholBaseTypes(arr); + break; + case 'glass': + setCocktailTypes(arr); + break; + } + + // URL 업데이트 + const params = new URLSearchParams(searchParams.toString()); + + if (code) { + params.set(id, code); + } else { + params.delete(id); + } + + const queryString = params.toString(); + const newUrl = `${pathname}${queryString ? `?${queryString}` : ''}`; + + // shallow routing으로 URL만 변경 + window.history.pushState({ ...window.history.state, as: newUrl, url: newUrl }, '', newUrl); + }; + return (
    {SELECT_OPTIONS.map(({ id, option, title }) => { + const currentValue = currentValues[id as keyof typeof currentValues]; + return (
  • - + handleSelect(id, value)} + />
  • ); })}
); } + export default Accordion; diff --git a/src/domains/recipe/components/main/Cocktails.tsx b/src/domains/recipe/components/main/Cocktails.tsx index a7128d9f..c756d0d4 100644 --- a/src/domains/recipe/components/main/Cocktails.tsx +++ b/src/domains/recipe/components/main/Cocktails.tsx @@ -28,7 +28,19 @@ function Cocktails() { const { inputValue, keyword, isSearching, onInputChange, noResults, setNoResults } = useSearchControl({ delay: 300, storageKey: 'cocktails_scoll_state' }); const { fetchData } = RecipeFetch({ setData, lastId, setLastId, hasNextPage, setHasNextPage }); - const { searchApi } = CocktailSearch({ setData, setNoResults }); + const { + searchApi, + setAlcoholBaseTypes, + setAlcoholStrengths, + setCocktailTypes, + alcoholBaseTypes, + cocktailTypes, + alcoholStrengths, + } = CocktailSearch({ + setData, + setNoResults, + }); + const countLabel = isSearching ? hasNextPage ? `검색결과 현재 ${data.length}+` @@ -44,7 +56,7 @@ function Cocktails() { if (readyForFirstLoad) { fetchData(); } - }, [hasNextPage]); + }, [hasNextPage, lastId]); // 검색어 변경 시 useEffect(() => { @@ -58,22 +70,27 @@ function Cocktails() { setLastId(null); setHasNextPage(true); } - }, [keyword, isSearching]); + }, [keyword, isSearching, alcoholBaseTypes, alcoholStrengths, cocktailTypes]); // 일반 fetch useEffect(() => { - if (!shouldFetch) return; - if (isSearching) return; + if (!shouldFetch || isSearching) return; fetchData(); }, [shouldFetch, isSearching]); return (
- +
+ +
{isSearching && noResults ? (
검색결과가 없습니다.
diff --git a/src/shared/components/select-box/SelectBox.tsx b/src/shared/components/select-box/SelectBox.tsx index 05f8f590..7a74b3d9 100644 --- a/src/shared/components/select-box/SelectBox.tsx +++ b/src/shared/components/select-box/SelectBox.tsx @@ -1,5 +1,5 @@ 'use client'; -import { Ref, useMemo, useRef, useState } from 'react'; +import { Ref, useEffect, useMemo, useRef, useState } from 'react'; import Down from '@/shared/assets/icons/selectDown_24.svg'; import { useShallow } from 'zustand/shallow'; import { ID, useAccordionStore } from '@/domains/recipe/store/accordionStore'; @@ -11,22 +11,26 @@ interface Props { ref?: Ref; option: string[]; title: string; + value?: string; onChange?: (value: string) => void; use?: string; } -// groupKey를 Props로 내릴경우 == 아코디언 없는 경우 == select박스 -function SelectBox({ id, groupKey, ref, option, title, onChange, use }: Props) { +function SelectBox({ id, groupKey, ref, option, title, value, onChange, use }: Props) { const [isOpen, setIsOpen] = useState(false); - const [select, setSelect] = useState(''); + const [select, setSelect] = useState(value || ''); const menuRef = useRef(null); const ingroup = !!groupKey; - // groupKey가 있을경우 true - // groupkey일 경우 전달받은 ID로 식별 아닐경우 title로 식별 const keyId = useMemo(() => id ?? title, [id, title]); - // id가 없을경우 title로 키 아이디를 받음 + + // value prop이 변경되면 select state도 업데이트 + useEffect(() => { + if (value !== undefined) { + setSelect(value); + } + }, [value]); useCloseOutside({ menuRef, @@ -39,22 +43,18 @@ function SelectBox({ id, groupKey, ref, option, title, onChange, use }: Props) { const { openId, toggleGroup, closeGroup } = useAccordionStore( useShallow((s) => ({ openId: ingroup ? (s.openByGroup[groupKey] ?? null) : null, - // 그룹키가 없으면 openId == null 따라서 state로 관리됨 toggleGroup: s.toggle, closeGroup: s.closeGroup, })) ); - //groupkey가 있을 떄와 없을때로 구분해서 state혹은 store로 관리 const localOpen = ingroup ? openId === keyId : isOpen; - // 그룹일 경우 filter와 id abv | base | glass 를 const toggle = () => { if (ingroup) toggleGroup(groupKey, keyId); else setIsOpen((prev) => { const next = !prev; - console.log('TOGGLE BTN CLICK:', { prev, next }); return next; }); }; @@ -71,11 +71,6 @@ function SelectBox({ id, groupKey, ref, option, title, onChange, use }: Props) { close(); }; - useCloseOutside({ - menuRef, - onClose: close, - }); - return (
); } diff --git a/src/domains/recipe/components/main/Accordion.tsx b/src/domains/recipe/components/main/Accordion.tsx index bf5aacd4..92bea4aa 100644 --- a/src/domains/recipe/components/main/Accordion.tsx +++ b/src/domains/recipe/components/main/Accordion.tsx @@ -133,8 +133,10 @@ function Accordion({ setAlcoholBaseTypes, setCocktailTypes, setAlcoholStrengths const queryString = params.toString(); const newUrl = `${pathname}${queryString ? `?${queryString}` : ''}`; + + router.push(newUrl) // shallow routing으로 URL만 변경 - window.history.pushState({ ...window.history.state, as: newUrl, url: newUrl }, '', newUrl); + // window.history.pushState({ ...window.history.state, as: newUrl, url: newUrl }, '', newUrl); }; return ( diff --git a/src/domains/recipe/components/main/CocktailList.tsx b/src/domains/recipe/components/main/CocktailList.tsx index 9b921678..8604cd45 100644 --- a/src/domains/recipe/components/main/CocktailList.tsx +++ b/src/domains/recipe/components/main/CocktailList.tsx @@ -26,13 +26,18 @@ function CocktailList({ cocktails, RecipeFetch, hasNextPage, lastId, onItemClick useIntersectionObserver(cocktailRef, onIntersect, hasNextPage); + const handleClick = () => { + sessionStorage.setItem('listScrollY', String(window.scrollY)); + sessionStorage.setItem('saveUrl', String(location.href)) +} + return (
    {cocktails.map( @@ -40,9 +45,7 @@ function CocktailList({ cocktails, RecipeFetch, hasNextPage, lastId, onItemClick
  • { - sessionStorage.setItem('listScrollY', String(window.scrollY)); - }} + onClick={handleClick} >
    diff --git a/src/domains/shared/components/keep/Keep.tsx b/src/domains/shared/components/keep/Keep.tsx index 2a6b1c5d..1a76af45 100644 --- a/src/domains/shared/components/keep/Keep.tsx +++ b/src/domains/shared/components/keep/Keep.tsx @@ -2,8 +2,9 @@ import KeepIcon from '@/shared/assets/icons/keep_36.svg'; import KeepIconActive from '@/shared/assets/icons/keep_active_36.svg'; -import { useState } from 'react'; -import { deleteKeep, postKeep } from '../../api/keep/keep'; +import { useEffect, useState } from 'react'; +import { deleteKeep, postKeep } from '../../api/keep/keep'; +import { getApi } from '@/app/api/config/appConfig'; interface Props { className?: string; @@ -14,6 +15,18 @@ interface Props { function Keep({ className, cocktailId }: Props) { const [isClick, setIsClick] = useState(false); + + useEffect(() => { + const getKeep = async () => { + const res = await fetch(`${getApi}/me/bar`, { + credentials:'include' + }) + const json = await res.json() + json.data.items.keptAt ? setIsClick(true) : setIsClick(false) + } + getKeep() + },[]) + const handleClick = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); From 159a925844adecabd0e1d1df2d20ffc71f7daac4 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Wed, 8 Oct 2025 22:23:08 +0900 Subject: [PATCH 5/9] =?UTF-8?q?[feat]=EB=92=A4=EB=A1=9C=EA=B0=80=EA=B8=B0?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recipe/components/main/Accordion.tsx | 34 ++++++++++--------- .../recipe/components/main/CocktailList.tsx | 4 +++ src/domains/shared/hook/useMemoScroll.ts | 2 +- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/domains/recipe/components/main/Accordion.tsx b/src/domains/recipe/components/main/Accordion.tsx index 92bea4aa..f2d86b9a 100644 --- a/src/domains/recipe/components/main/Accordion.tsx +++ b/src/domains/recipe/components/main/Accordion.tsx @@ -2,7 +2,7 @@ import SelectBox from '@/shared/components/select-box/SelectBox'; import { Dispatch, SetStateAction, useEffect, useMemo } from 'react'; -import { useRouter, useSearchParams, usePathname } from 'next/navigation'; +import { useSearchParams, usePathname, useRouter } from 'next/navigation'; interface Props { setAlcoholBaseTypes: Dispatch>; @@ -65,11 +65,22 @@ const SELECT_OPTIONS = [ ]; function Accordion({ setAlcoholBaseTypes, setCocktailTypes, setAlcoholStrengths }: Props) { - const router = useRouter(); + const router = useRouter() const pathname = usePathname(); const searchParams = useSearchParams(); - // 코드를 한글 라벨로 역변환하는 함수 + // url 파라미터에서 값을 가져와 POST를 도와줌 + useEffect(() => { + const abv = searchParams.get('abv'); + const base = searchParams.get('base'); + const glass = searchParams.get('glass'); + + setAlcoholStrengths(abv ? [abv] : []); + setAlcoholBaseTypes(base ? [base] : []); + setCocktailTypes(glass ? [glass] : []); + }, [searchParams, setAlcoholStrengths, setAlcoholBaseTypes, setCocktailTypes]); + + // 파라미터 값을 한글로 역 변환해주는 함수 const getDisplayValue = (id: string, code: string | null): string => { if (!code) return '전체'; @@ -81,7 +92,8 @@ function Accordion({ setAlcoholBaseTypes, setCocktailTypes, setAlcoholStrengths return entry ? entry[0] : '전체'; }; - // URL 파라미터에서 현재 선택된 값 가져오기 + + // URL 파라미터에서 현재 선택된 값 가져오기 아코디언 UI에 적용 const currentValues = useMemo(() => { return { abv: getDisplayValue('abv', searchParams.get('abv')), @@ -90,21 +102,11 @@ function Accordion({ setAlcoholBaseTypes, setCocktailTypes, setAlcoholStrengths }; }, [searchParams]); - // URL에서 상태 동기화 - useEffect(() => { - const abv = searchParams.get('abv'); - const base = searchParams.get('base'); - const glass = searchParams.get('glass'); - - setAlcoholStrengths(abv ? [abv] : []); - setAlcoholBaseTypes(base ? [base] : []); - setCocktailTypes(glass ? [glass] : []); - }, [searchParams, setAlcoholStrengths, setAlcoholBaseTypes, setCocktailTypes]); - const handleSelect = (id: string, value: string) => { const optionGroup = SELECT_OPTIONS.find((opt) => opt.id === id); if (!optionGroup) return; + // 선택한 옵션의 밸류를 전달 const code = optionGroup.map[value as keyof typeof optionGroup.map] ?? null; const arr = code ? [code] : []; @@ -131,7 +133,7 @@ function Accordion({ setAlcoholBaseTypes, setCocktailTypes, setAlcoholStrengths } const queryString = params.toString(); - const newUrl = `${pathname}${queryString ? `?${queryString}` : ''}`; + const newUrl = `${pathname}?${queryString}`; router.push(newUrl) diff --git a/src/domains/recipe/components/main/CocktailList.tsx b/src/domains/recipe/components/main/CocktailList.tsx index 8604cd45..aed8604e 100644 --- a/src/domains/recipe/components/main/CocktailList.tsx +++ b/src/domains/recipe/components/main/CocktailList.tsx @@ -5,6 +5,7 @@ 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 { usePathname, useRouter, useSearchParams } from 'next/navigation'; interface Props { cocktails: Cocktail[]; @@ -15,6 +16,9 @@ interface Props { } function CocktailList({ cocktails, RecipeFetch, hasNextPage, lastId, onItemClick }: Props) { + const router = useRouter() + const pathname = usePathname(); + const searchParams = useSearchParams(); const cocktailRef = useRef(null); const onIntersect: IntersectionObserverCallback = ([entry]) => { if (!RecipeFetch) return; diff --git a/src/domains/shared/hook/useMemoScroll.ts b/src/domains/shared/hook/useMemoScroll.ts index b2090888..5b8871b5 100644 --- a/src/domains/shared/hook/useMemoScroll.ts +++ b/src/domains/shared/hook/useMemoScroll.ts @@ -22,7 +22,7 @@ export function useMemoScroll({ // 뒤로가기를 통해 목록 복원을 저장해주는 플래그 const NAVIGATION_FLAG_KEY = `${storageKey}_nav_flag`; - + // 실제 렌더링 되는 데이터 const [data, setData] = useState([]); const [lastId, setLastId] = useState(null); From 0ea8b3e743b391b2d32b393e02afc8a2d08f7ec2 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Wed, 8 Oct 2025 22:25:01 +0900 Subject: [PATCH 6/9] =?UTF-8?q?[chore]merge=EC=A0=84=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/recipe/components/main/CocktailList.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/domains/recipe/components/main/CocktailList.tsx b/src/domains/recipe/components/main/CocktailList.tsx index aed8604e..d7de2b40 100644 --- a/src/domains/recipe/components/main/CocktailList.tsx +++ b/src/domains/recipe/components/main/CocktailList.tsx @@ -5,7 +5,6 @@ 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 { usePathname, useRouter, useSearchParams } from 'next/navigation'; interface Props { cocktails: Cocktail[]; @@ -16,9 +15,7 @@ interface Props { } function CocktailList({ cocktails, RecipeFetch, hasNextPage, lastId, onItemClick }: Props) { - const router = useRouter() - const pathname = usePathname(); - const searchParams = useSearchParams(); + const cocktailRef = useRef(null); const onIntersect: IntersectionObserverCallback = ([entry]) => { if (!RecipeFetch) return; From e77b9949606bc40c208c65a9496518a17670cf3c Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Wed, 8 Oct 2025 22:26:51 +0900 Subject: [PATCH 7/9] =?UTF-8?q?[chore]=EB=A8=B8=EC=A7=80=20=ED=9B=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=82=AC=ED=95=AD=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/recipe/api/RecipeFetch.tsx | 11 ++----- .../recipe/components/details/BackBtn.tsx | 16 +++++----- .../recipe/components/main/Accordion.tsx | 6 ++-- .../recipe/components/main/CocktailList.tsx | 10 ++----- .../recipe/components/main/Cocktails.tsx | 1 - src/domains/recipe/hook/ozToMl.ts | 30 +++++++++---------- src/domains/shared/components/keep/Keep.tsx | 16 +++++----- src/domains/shared/hook/useMemoScroll.ts | 3 +- 8 files changed, 39 insertions(+), 54 deletions(-) diff --git a/src/domains/recipe/api/RecipeFetch.tsx b/src/domains/recipe/api/RecipeFetch.tsx index 4fdbe9a5..1fb008af 100644 --- a/src/domains/recipe/api/RecipeFetch.tsx +++ b/src/domains/recipe/api/RecipeFetch.tsx @@ -4,7 +4,6 @@ import { getApi } from '@/app/api/config/appConfig'; import { Cocktail } from '../types/types'; import { Dispatch, SetStateAction, useCallback } from 'react'; - interface Props { setData: React.Dispatch>; lastId: number | null; @@ -13,7 +12,7 @@ interface Props { setHasNextPage: Dispatch>; SIZE?: number; } -// api/cocktais fetch용 +// api/cocktais fetch용 export const RecipeFetch = ({ setData, lastId, @@ -22,10 +21,7 @@ export const RecipeFetch = ({ setHasNextPage, SIZE = 20, }: Props) => { - - const fetchData = useCallback(async () => { - // 쿼리파라미터에 값 넣기 if (!hasNextPage) return; const url = new URL(`${getApi}/cocktails`); @@ -33,8 +29,8 @@ export const RecipeFetch = ({ if (typeof lastId === 'number') { url.searchParams.set('lastId', String(lastId)); } - - const res = await fetch(url.toString(),{method:'GET'}); + + const res = await fetch(url.toString(), { method: 'GET' }); if (!res.ok) throw new Error('레시피 데이터 요청실패'); const json = await res.json(); @@ -44,7 +40,6 @@ export const RecipeFetch = ({ setData((prev) => Array.from(new Map([...prev, ...list].map((i) => [i.cocktailId, i])).values()) ); - if (list.length > 0) { setLastId(list[list.length - 1].cocktailId); diff --git a/src/domains/recipe/components/details/BackBtn.tsx b/src/domains/recipe/components/details/BackBtn.tsx index 8e37acd7..b4e2a6dc 100644 --- a/src/domains/recipe/components/details/BackBtn.tsx +++ b/src/domains/recipe/components/details/BackBtn.tsx @@ -4,19 +4,17 @@ import { useRouter } from 'next/navigation'; import Back from '@/shared/assets/icons/back_36.svg'; function BackButton() { - const router = useRouter() - + const router = useRouter(); const handleClick = () => { - const url = sessionStorage.getItem('saveUrl') - if(!url) return - router.push(url) - } - + const url = sessionStorage.getItem('saveUrl'); + if (!url) return; + router.push(url); + }; return ( - ); } diff --git a/src/domains/recipe/components/main/Accordion.tsx b/src/domains/recipe/components/main/Accordion.tsx index f2d86b9a..685053d0 100644 --- a/src/domains/recipe/components/main/Accordion.tsx +++ b/src/domains/recipe/components/main/Accordion.tsx @@ -65,7 +65,7 @@ const SELECT_OPTIONS = [ ]; function Accordion({ setAlcoholBaseTypes, setCocktailTypes, setAlcoholStrengths }: Props) { - const router = useRouter() + const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); @@ -92,7 +92,6 @@ function Accordion({ setAlcoholBaseTypes, setCocktailTypes, setAlcoholStrengths return entry ? entry[0] : '전체'; }; - // URL 파라미터에서 현재 선택된 값 가져오기 아코디언 UI에 적용 const currentValues = useMemo(() => { return { @@ -135,8 +134,7 @@ function Accordion({ setAlcoholBaseTypes, setCocktailTypes, setAlcoholStrengths const queryString = params.toString(); const newUrl = `${pathname}?${queryString}`; - - router.push(newUrl) + router.push(newUrl); // shallow routing으로 URL만 변경 // window.history.pushState({ ...window.history.state, as: newUrl, url: newUrl }, '', newUrl); }; diff --git a/src/domains/recipe/components/main/CocktailList.tsx b/src/domains/recipe/components/main/CocktailList.tsx index 1becdf87..56c0ce7b 100644 --- a/src/domains/recipe/components/main/CocktailList.tsx +++ b/src/domains/recipe/components/main/CocktailList.tsx @@ -15,7 +15,6 @@ interface Props { } function CocktailList({ cocktails, RecipeFetch, hasNextPage, lastId, onItemClick }: Props) { - const cocktailRef = useRef(null); const onIntersect: IntersectionObserverCallback = ([entry]) => { if (!RecipeFetch) return; @@ -29,8 +28,8 @@ function CocktailList({ cocktails, RecipeFetch, hasNextPage, lastId, onItemClick const handleClick = () => { sessionStorage.setItem('listScrollY', String(window.scrollY)); - sessionStorage.setItem('saveUrl', String(location.href)) -} + sessionStorage.setItem('saveUrl', String(location.href)); + }; return (
      (
    • - + = { - '¼': 8, - '½': 15, - '¾': 23, - '⅓': 10, - '⅔': 20, - '⅕': 6, - '⅖': 12, - '⅗': 18, - '⅘': 24, - '⅙': 5, - '⅚': 25, + '¼': 8, + '½': 15, + '¾': 23, + '⅓': 10, + '⅔': 20, + '⅕': 6, + '⅖': 12, + '⅗': 18, + '⅘': 24, + '⅙': 5, + '⅚': 25, '⅛': 4, - '⅜': 11, - '⅝': 19, - '⅞': 26, + '⅜': 11, + '⅝': 19, + '⅞': 26, }; // 정규식 분리 용 class diff --git a/src/domains/shared/components/keep/Keep.tsx b/src/domains/shared/components/keep/Keep.tsx index 1a76af45..7985e807 100644 --- a/src/domains/shared/components/keep/Keep.tsx +++ b/src/domains/shared/components/keep/Keep.tsx @@ -3,7 +3,7 @@ import KeepIcon from '@/shared/assets/icons/keep_36.svg'; import KeepIconActive from '@/shared/assets/icons/keep_active_36.svg'; import { useEffect, useState } from 'react'; -import { deleteKeep, postKeep } from '../../api/keep/keep'; +import { deleteKeep, postKeep } from '../../api/keep/keep'; import { getApi } from '@/app/api/config/appConfig'; interface Props { @@ -19,13 +19,13 @@ function Keep({ className, cocktailId }: Props) { useEffect(() => { const getKeep = async () => { const res = await fetch(`${getApi}/me/bar`, { - credentials:'include' - }) - const json = await res.json() - json.data.items.keptAt ? setIsClick(true) : setIsClick(false) - } - getKeep() - },[]) + credentials: 'include', + }); + const json = await res.json(); + json.data.items.keptAt ? setIsClick(true) : setIsClick(false); + }; + getKeep(); + }, []); const handleClick = async (e: React.MouseEvent) => { e.preventDefault(); diff --git a/src/domains/shared/hook/useMemoScroll.ts b/src/domains/shared/hook/useMemoScroll.ts index 5b8871b5..5be220e7 100644 --- a/src/domains/shared/hook/useMemoScroll.ts +++ b/src/domains/shared/hook/useMemoScroll.ts @@ -19,10 +19,9 @@ export function useMemoScroll({ storageKey, eventName = 'resetScroll', }: UseScrollRestorationProps) { - // 뒤로가기를 통해 목록 복원을 저장해주는 플래그 const NAVIGATION_FLAG_KEY = `${storageKey}_nav_flag`; - + // 실제 렌더링 되는 데이터 const [data, setData] = useState([]); const [lastId, setLastId] = useState(null); From 3e9613d2e77fed12319d5cdd0d4ea93ceae82aa2 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Wed, 8 Oct 2025 22:35:44 +0900 Subject: [PATCH 8/9] =?UTF-8?q?[fix]=ED=82=B5=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20401=EC=97=90=EB=9F=AC=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/shared/components/keep/Keep.tsx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/domains/shared/components/keep/Keep.tsx b/src/domains/shared/components/keep/Keep.tsx index 7985e807..2a7d68cf 100644 --- a/src/domains/shared/components/keep/Keep.tsx +++ b/src/domains/shared/components/keep/Keep.tsx @@ -2,9 +2,9 @@ import KeepIcon from '@/shared/assets/icons/keep_36.svg'; import KeepIconActive from '@/shared/assets/icons/keep_active_36.svg'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { deleteKeep, postKeep } from '../../api/keep/keep'; -import { getApi } from '@/app/api/config/appConfig'; + interface Props { className?: string; @@ -16,17 +16,6 @@ interface Props { function Keep({ className, cocktailId }: Props) { const [isClick, setIsClick] = useState(false); - useEffect(() => { - const getKeep = async () => { - const res = await fetch(`${getApi}/me/bar`, { - credentials: 'include', - }); - const json = await res.json(); - json.data.items.keptAt ? setIsClick(true) : setIsClick(false); - }; - getKeep(); - }, []); - const handleClick = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); From c01b52a4e7ef34c9070aa340d77baf689e1aee23 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Wed, 8 Oct 2025 22:36:46 +0900 Subject: [PATCH 9/9] =?UTF-8?q?[chore]=EC=BB=A4=EB=B0=8B=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EB=B2=84=EA=B7=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/shared/components/keep/Keep.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/domains/shared/components/keep/Keep.tsx b/src/domains/shared/components/keep/Keep.tsx index 2a7d68cf..5c36cb86 100644 --- a/src/domains/shared/components/keep/Keep.tsx +++ b/src/domains/shared/components/keep/Keep.tsx @@ -5,7 +5,6 @@ import KeepIconActive from '@/shared/assets/icons/keep_active_36.svg'; import { useState } from 'react'; import { deleteKeep, postKeep } from '../../api/keep/keep'; - interface Props { className?: string; cocktailId?: number;