From 0ca6858c71e20c4cf1a20e212d5a529fb270599a Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Thu, 9 Oct 2025 16:50:36 +0900 Subject: [PATCH 01/21] =?UTF-8?q?[feat]=EC=B9=B5=ED=85=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20ui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/pages/my-active/MyLike.tsx | 13 ++++------- src/domains/recipe/api/fetchRecipeComment.ts | 20 +++++++++++++++++ .../recipe/components/details/BackBtn.tsx | 1 + .../components/details/RecipeComment.tsx | 22 ++++++++++++++++--- .../recipe/components/main/CocktailList.tsx | 2 -- .../recipe/components/main/Cocktails.tsx | 3 +++ src/domains/recipe/details/DetailMain.tsx | 3 ++- .../shared/hook/useIntersectionObserver.ts | 3 +++ src/domains/shared/hook/useMemoScroll.ts | 5 +++-- 9 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 src/domains/recipe/api/fetchRecipeComment.ts diff --git a/src/domains/mypage/components/pages/my-active/MyLike.tsx b/src/domains/mypage/components/pages/my-active/MyLike.tsx index 7785690f..333eb1fa 100644 --- a/src/domains/mypage/components/pages/my-active/MyLike.tsx +++ b/src/domains/mypage/components/pages/my-active/MyLike.tsx @@ -3,15 +3,10 @@ import { getApi } from '@/app/api/config/appConfig'; import PostCard from '@/domains/community/main/PostCard'; import { useEffect, useState } from 'react'; -interface MyLike { - postId: number; - title: string; - likedAt: Date; - posetCreatedAt: Date; -} + function MyLike() { - const [myLike, setMyLike] = useState([]); + const [myLike, setMyLike] = useState([]); const [isLoading, setIsLoading] = useState(false); const fetchLike = async () => { @@ -20,13 +15,13 @@ function MyLike() { credentials: 'include', }); const json = await res.json(); - // setMyLike(json.data.items); + setMyLike(json.data.items); }; useEffect(() => { fetchLike(); }, []); - // return ; + return ; } export default MyLike; diff --git a/src/domains/recipe/api/fetchRecipeComment.ts b/src/domains/recipe/api/fetchRecipeComment.ts new file mode 100644 index 00000000..a959662f --- /dev/null +++ b/src/domains/recipe/api/fetchRecipeComment.ts @@ -0,0 +1,20 @@ +import { getApi } from "@/app/api/config/appConfig" + +export const postRecipeComment = async (cocktailId: number, content: string) => { + try { + const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments`, { + method: 'POST', + headers:{'Content-Type':'application/json'}, + credentials: 'include', + body:JSON.stringify({content}) + }) + const text = await res.text() + if (!res.ok) throw new Error('댓글 작성 실패') + const data = JSON.parse(text) + return data + } + catch (err) { + console.error(err) + } + + } \ No newline at end of file diff --git a/src/domains/recipe/components/details/BackBtn.tsx b/src/domains/recipe/components/details/BackBtn.tsx index b4e2a6dc..069522a7 100644 --- a/src/domains/recipe/components/details/BackBtn.tsx +++ b/src/domains/recipe/components/details/BackBtn.tsx @@ -10,6 +10,7 @@ function BackButton() { const url = sessionStorage.getItem('saveUrl'); if (!url) return; router.push(url); + sessionStorage.removeItem('listScrollY'); }; return ( diff --git a/src/domains/recipe/components/details/RecipeComment.tsx b/src/domains/recipe/components/details/RecipeComment.tsx index 57397d2d..f0ab5d0d 100644 --- a/src/domains/recipe/components/details/RecipeComment.tsx +++ b/src/domains/recipe/components/details/RecipeComment.tsx @@ -1,11 +1,27 @@ import CommentHeader from '@/domains/shared/components/comment/CommentHeader'; import CommentList from '@/domains/shared/components/comment/CommentList'; +import { postRecipeComment } from '../../api/fetchRecipeComment'; +import { useComments } from '@/domains/community/hook/useComment'; +import { useAuthStore } from '@/domains/shared/store/auth'; +import { useShallow } from 'zustand/shallow'; + +interface Props { + cocktailId:number +} + +function RecipeComment({cocktailId}:Props) { + const { user, accessToken } = useAuthStore( + useShallow((state) => ({ + user: state.user, + accessToken: state.accessToken, + })) + ); + const {comments,fetchData,handleAskDeleteComment,handleUpdateComment,loadMoreComments,isEnd,isLoading} = useComments(cocktailId,user,accessToken) -function RecipeComment() { return (
- {/* */} - {/* */} + +
); } diff --git a/src/domains/recipe/components/main/CocktailList.tsx b/src/domains/recipe/components/main/CocktailList.tsx index 56c0ce7b..5ed4bcef 100644 --- a/src/domains/recipe/components/main/CocktailList.tsx +++ b/src/domains/recipe/components/main/CocktailList.tsx @@ -1,6 +1,5 @@ 'use client'; import { useRef } from 'react'; - import Link from 'next/link'; import { useIntersectionObserver } from '@/domains/shared/hook/useIntersectionObserver'; import { Cocktail } from '../../types/types'; @@ -27,7 +26,6 @@ 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)); }; diff --git a/src/domains/recipe/components/main/Cocktails.tsx b/src/domains/recipe/components/main/Cocktails.tsx index c756d0d4..30d03ad1 100644 --- a/src/domains/recipe/components/main/Cocktails.tsx +++ b/src/domains/recipe/components/main/Cocktails.tsx @@ -21,6 +21,7 @@ function Cocktails() { setHasNextPage, handleItemClick, shouldFetch, + isRestoring } = useMemoScroll({ storageKey: 'cocktails_scroll_state', eventName: 'resetCocktailsScroll', @@ -60,6 +61,7 @@ function Cocktails() { // 검색어 변경 시 useEffect(() => { + if(isRestoring) return if (isSearching && keyword.trim()) { setLastId(null); setHasNextPage(false); @@ -74,6 +76,7 @@ function Cocktails() { // 일반 fetch useEffect(() => { + if(isRestoring) return if (!shouldFetch || isSearching) return; fetchData(); }, [shouldFetch, isSearching]); diff --git a/src/domains/recipe/details/DetailMain.tsx b/src/domains/recipe/details/DetailMain.tsx index 65455b9e..22880330 100644 --- a/src/domains/recipe/details/DetailMain.tsx +++ b/src/domains/recipe/details/DetailMain.tsx @@ -42,6 +42,7 @@ function DetailMain({ id }: { id: number }) { if (!cocktail) return; const { + cocktailId, cocktailImgUrl, cocktailName, cocktailNameKo, @@ -91,7 +92,7 @@ function DetailMain({ id }: { id: number }) {
- +
diff --git a/src/domains/shared/hook/useIntersectionObserver.ts b/src/domains/shared/hook/useIntersectionObserver.ts index 6566b9f6..d93763fa 100644 --- a/src/domains/shared/hook/useIntersectionObserver.ts +++ b/src/domains/shared/hook/useIntersectionObserver.ts @@ -8,6 +8,9 @@ export const useIntersectionObserver = ( const observer = useRef(null); useEffect(() => { + + observer.current?.disconnect() + if (targetRef && targetRef.current) { observer.current = new IntersectionObserver(onIntersect, { root: null, diff --git a/src/domains/shared/hook/useMemoScroll.ts b/src/domains/shared/hook/useMemoScroll.ts index 5be220e7..eff9a3ca 100644 --- a/src/domains/shared/hook/useMemoScroll.ts +++ b/src/domains/shared/hook/useMemoScroll.ts @@ -95,7 +95,7 @@ export function useMemoScroll({ // 스크롤 복원 시도 로직 window.scrollTo({ top: scrollY, - behavior: 'instant', + behavior: 'auto', }); setTimeout(() => { @@ -107,7 +107,7 @@ export function useMemoScroll({ // 스크롤 재 조정이 필요한 경우 실행 window.scrollTo({ top: scrollY, - behavior: 'instant', + behavior: 'auto', }); } @@ -226,5 +226,6 @@ export function useMemoScroll({ handleItemClick, saveScrollState, shouldFetch, + isRestoring:isRestoringRef.current }; } From da66e023ce523fb6fe8b8c1343b0bcf68b4182ea Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Fri, 10 Oct 2025 11:43:06 +0900 Subject: [PATCH 02/21] =?UTF-8?q?[chore]=20merge=EC=A0=84=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.ts | 6 +- src/app/layout.tsx | 1 + src/domains/recipe/api/fetchRecipeComment.ts | 71 +++++++++--- src/domains/recipe/api/useRecipeComment.ts | 102 ++++++++++++++++++ .../components/details/RecipeComment.tsx | 34 +++++- .../recipe/components/main/CocktailList.tsx | 13 +-- .../recipe/components/main/Cocktails.tsx | 6 +- .../recipe/skeleton/SkeletonRecipe.tsx | 15 ++- .../shared/hook/useIntersectionObserver.ts | 4 +- src/domains/shared/hook/useMemoScroll.ts | 64 +++++------ 10 files changed, 234 insertions(+), 82 deletions(-) create mode 100644 src/domains/recipe/api/useRecipeComment.ts diff --git a/next.config.ts b/next.config.ts index dca67ead..49025491 100644 --- a/next.config.ts +++ b/next.config.ts @@ -5,9 +5,9 @@ const nextConfig: NextConfig = { remotePatterns: [ { protocol: 'https', - hostname:'www.thecocktaildb.com' - } - ] + hostname: 'www.thecocktaildb.com', + }, + ], }, env: { NPUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3d86d643..78f59c6f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -17,6 +17,7 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { + return ( diff --git a/src/domains/recipe/api/fetchRecipeComment.ts b/src/domains/recipe/api/fetchRecipeComment.ts index a959662f..51ed96e3 100644 --- a/src/domains/recipe/api/fetchRecipeComment.ts +++ b/src/domains/recipe/api/fetchRecipeComment.ts @@ -1,20 +1,61 @@ -import { getApi } from "@/app/api/config/appConfig" +import { getApi } from '@/app/api/config/appConfig'; +import { CommentType } from '@/domains/community/types/post'; export const postRecipeComment = async (cocktailId: number, content: string) => { try { - const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments`, { - method: 'POST', - headers:{'Content-Type':'application/json'}, - credentials: 'include', - body:JSON.stringify({content}) - }) - const text = await res.text() - if (!res.ok) throw new Error('댓글 작성 실패') - const data = JSON.parse(text) - return data + const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ content }), + }); + const text = await res.text(); + if (!res.ok) throw new Error('댓글 작성 실패'); + const data = JSON.parse(text); + return data; + } catch (err) { + console.error(err); } - catch (err) { - console.error(err) +}; + +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( + accessToken: string | null, + cocktailId: number, + commentId:number, + content: string +): Promise { + const response = await fetch(`${getApi}/cocktails/${cocktailId}/comments/${commentId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ content }), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('서버 응답 에러:', errorText); + throw new Error(`댓글 수정 실패: ${response.status}`); } - - } \ No newline at end of file +} \ No newline at end of file diff --git a/src/domains/recipe/api/useRecipeComment.ts b/src/domains/recipe/api/useRecipeComment.ts new file mode 100644 index 00000000..8b5129f1 --- /dev/null +++ b/src/domains/recipe/api/useRecipeComment.ts @@ -0,0 +1,102 @@ +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 { deleteComment} from '@/domains/community/api/fetchComment'; +import { getRecipeComment, updateComment } from './fetchRecipeComment'; + +export function useRecipeComments(cocktailId: number, user: User | null, accessToken: string | 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 fetchData = useCallback(async () => { + const data = await getRecipeComment(cocktailId); + if (!data) return; + setComments(data); + setIsEnd(false); + }, [cocktailId]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const handleUpdateComment = async (commentId: number,cocktailId:number,content: string) => { + + if (!user) { + alert('로그인이 필요합니다'); + return; + } + try { + await updateComment(accessToken!, cocktailId, commentId, content); + setComments((prev) => + prev + ? prev.map((comment) => + comment.commentId === commentId ? { ...comment, content } : comment + ) + : prev + ); + } catch (err) { + console.error(err); + alert('댓글 수정 중 오류가 발생했습니다.'); + } + }; + + const handleAskDeleteComment = (commentId: number, cocktailId: number) => { + setDeleteTarget({ commentId, cocktailId }); + }; + + const handleConfirmDelete = async () => { + if (!user) { + alert('로그인이 필요합니다'); + return; + } + if (!deleteTarget) return; + + try { + await deleteComment(accessToken!, deleteTarget.cocktailId, deleteTarget.commentId); + setComments((prev) => + prev ? prev.filter((c) => c.commentId !== deleteTarget.commentId) : prev + ); + } catch (err) { + console.error(err); + alert('댓글 삭제 중 오류가 발생했습니다.'); + } finally { + setDeleteTarget(null); + } + }; + + const loadMoreComments = async (lastCommentId: number) => { + if (isEnd || isLoading) return; + + setIsLoading(true); + try { + const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments?lastId=${lastCommentId}`); + const newComments = await res.json(); + + if (newComments.data.length === 0) { + setIsEnd(true); + } else { + setComments((prev) => [...(prev ?? []), ...newComments.data]); + } + } finally { + setIsLoading(false); + } + }; + + return { + comments, + isEnd, + isLoading, + deleteTarget, + setDeleteTarget, + fetchData, + handleUpdateComment, + handleAskDeleteComment, + handleConfirmDelete, + loadMoreComments, + }; +} diff --git a/src/domains/recipe/components/details/RecipeComment.tsx b/src/domains/recipe/components/details/RecipeComment.tsx index f0ab5d0d..dab7792a 100644 --- a/src/domains/recipe/components/details/RecipeComment.tsx +++ b/src/domains/recipe/components/details/RecipeComment.tsx @@ -1,9 +1,10 @@ import CommentHeader from '@/domains/shared/components/comment/CommentHeader'; import CommentList from '@/domains/shared/components/comment/CommentList'; import { postRecipeComment } from '../../api/fetchRecipeComment'; -import { useComments } from '@/domains/community/hook/useComment'; 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'; interface Props { cocktailId:number @@ -16,12 +17,37 @@ function RecipeComment({cocktailId}:Props) { accessToken: state.accessToken, })) ); - const {comments,fetchData,handleAskDeleteComment,handleUpdateComment,loadMoreComments,isEnd,isLoading} = useComments(cocktailId,user,accessToken) + const { comments, fetchData, handleAskDeleteComment, handleUpdateComment, loadMoreComments, isEnd, isLoading, deleteTarget, handleConfirmDelete, setDeleteTarget } = useRecipeComments(cocktailId, user, accessToken) + return (
- - + + + {deleteTarget && ( + setDeleteTarget(null)} + onClose={() => setDeleteTarget(null)} + title="댓글 삭제" + description="정말 이 댓글을 삭제하시겠습니까?" + /> + )}
); } diff --git a/src/domains/recipe/components/main/CocktailList.tsx b/src/domains/recipe/components/main/CocktailList.tsx index 5ed4bcef..16350365 100644 --- a/src/domains/recipe/components/main/CocktailList.tsx +++ b/src/domains/recipe/components/main/CocktailList.tsx @@ -1,5 +1,5 @@ 'use client'; -import { useRef } from 'react'; +import { useEffect, useRef } from 'react'; import Link from 'next/link'; import { useIntersectionObserver } from '@/domains/shared/hook/useIntersectionObserver'; import { Cocktail } from '../../types/types'; @@ -18,16 +18,13 @@ function CocktailList({ cocktails, RecipeFetch, hasNextPage, lastId, onItemClick const onIntersect: IntersectionObserverCallback = ([entry]) => { if (!RecipeFetch) return; if (!lastId) return; - if (entry.isIntersecting) { + if (entry.isIntersecting && lastId > 1) { RecipeFetch(); } }; useIntersectionObserver(cocktailRef, onIntersect, hasNextPage); - - const handleClick = () => { - sessionStorage.setItem('saveUrl', String(location.href)); - }; + // ⬇️ 파일 최상단 근처: 공통 디버그 토글 return (
    (
  • - + ) )} -
    +
); } diff --git a/src/domains/recipe/components/main/Cocktails.tsx b/src/domains/recipe/components/main/Cocktails.tsx index 30d03ad1..8248378e 100644 --- a/src/domains/recipe/components/main/Cocktails.tsx +++ b/src/domains/recipe/components/main/Cocktails.tsx @@ -21,7 +21,6 @@ function Cocktails() { setHasNextPage, handleItemClick, shouldFetch, - isRestoring } = useMemoScroll({ storageKey: 'cocktails_scroll_state', eventName: 'resetCocktailsScroll', @@ -61,7 +60,6 @@ function Cocktails() { // 검색어 변경 시 useEffect(() => { - if(isRestoring) return if (isSearching && keyword.trim()) { setLastId(null); setHasNextPage(false); @@ -76,10 +74,10 @@ function Cocktails() { // 일반 fetch useEffect(() => { - if(isRestoring) return if (!shouldFetch || isSearching) return; fetchData(); - }, [shouldFetch, isSearching]); + + }, [shouldFetch, isSearching,alcoholBaseTypes,alcoholStrengths,cocktailTypes ]); return (
diff --git a/src/domains/recipe/skeleton/SkeletonRecipe.tsx b/src/domains/recipe/skeleton/SkeletonRecipe.tsx index cb9fbd79..38476fd1 100644 --- a/src/domains/recipe/skeleton/SkeletonRecipe.tsx +++ b/src/domains/recipe/skeleton/SkeletonRecipe.tsx @@ -17,15 +17,12 @@ function SkeletonRecipe() { {/* 리스트 자리 */}
    {Array.from({ length: 8 }).map((_, i) => ( diff --git a/src/domains/shared/hook/useIntersectionObserver.ts b/src/domains/shared/hook/useIntersectionObserver.ts index d93763fa..540462df 100644 --- a/src/domains/shared/hook/useIntersectionObserver.ts +++ b/src/domains/shared/hook/useIntersectionObserver.ts @@ -9,8 +9,6 @@ export const useIntersectionObserver = ( useEffect(() => { - observer.current?.disconnect() - if (targetRef && targetRef.current) { observer.current = new IntersectionObserver(onIntersect, { root: null, @@ -24,5 +22,5 @@ export const useIntersectionObserver = ( observer.current.observe(targetRef.current); } return () => observer && observer.current?.disconnect(); - }, [targetRef, onIntersect]); + }, [targetRef, onIntersect,hasNextPage]); }; diff --git a/src/domains/shared/hook/useMemoScroll.ts b/src/domains/shared/hook/useMemoScroll.ts index eff9a3ca..0b9d35e7 100644 --- a/src/domains/shared/hook/useMemoScroll.ts +++ b/src/domains/shared/hook/useMemoScroll.ts @@ -10,7 +10,6 @@ interface ScrollState { data: T[]; lastId: number | null; hasNextPage: boolean; - timestamp: number; } // 뒤로가기시 스크롤위치 기억 함수 @@ -37,15 +36,14 @@ export function useMemoScroll({ // 스크롤 위치와 데이터 저장 const saveScrollState = useCallback(() => { - // 복원 중일때와 일정 스크롤 이상 안내려오면 저장 안함 - if (isRestoringRef.current || window.scrollY < 10) return; + // 복원 중일때 저장 X + if (isRestoringRef.current) return; const scrollState: ScrollState = { scrollY: window.scrollY, data: data, lastId: lastId, hasNextPage: hasNextPage, - timestamp: Date.now(), }; sessionStorage.setItem(storageKey, JSON.stringify(scrollState)); @@ -76,49 +74,46 @@ export function useMemoScroll({ data: savedData, lastId: savedLastId, hasNextPage: savedHasNextPage, - timestamp, }: ScrollState = parsed; - // 세션이 30분 지나가면 세션 삭제 - const isRecent = Date.now() - timestamp < 30 * 60 * 1000; - - if (isRecent && savedData.length > 0 && scrollY > 10) { + if (savedData.length > 0 && scrollY > 10) { // 조건 충족 시 스크롤 데이터 복원 - isRestoringRef.current = true; setData(savedData); setLastId(savedLastId); setHasNextPage(savedHasNextPage); - const restoreScroll = () => { - // 스크롤 복원 시도 로직 - window.scrollTo({ - top: scrollY, - behavior: 'auto', - }); + const restoreScroll = (targetScrollY: number, maxAttempts = 10, interval = 100) => { + let attempts = 0; + + const tryScroll = () => { + window.scrollTo({ + top: targetScrollY, + behavior: 'auto', + }); - setTimeout(() => { - // 무한 스크롤시 데이터에따라 한번에 스크롤 복원 안되는 현상 발생 => 재시도 로직 const currentScroll = window.scrollY; - const diff = Math.abs(currentScroll - scrollY); - - if (diff > 5) { - // 스크롤 재 조정이 필요한 경우 실행 - window.scrollTo({ - top: scrollY, - behavior: 'auto', - }); + const threshold = 50; + console.log( + `Attempt ${attempts + 1}: target=${targetScrollY}, current=${currentScroll}` + ); + + if (Math.abs(currentScroll - targetScrollY) < threshold || attempts >= maxAttempts) { + console.log('Scroll restoration completed'); + return; } - setTimeout(() => { - // 복원 완료 ref초기화 - isRestoringRef.current = false; - scrollRestoredRef.current = true; - }, 300); - }, 100); + attempts++; + setTimeout(tryScroll, interval); + }; + + tryScroll(); }; + // 사용 + restoreScroll(scrollY); + // 재 조정시 애니메이션 매끄럽게 처리 setTimeout(restoreScroll, 0); requestAnimationFrame(() => { @@ -145,8 +140,7 @@ export function useMemoScroll({ const handleItemClick = useCallback(() => { if (!isRestoringRef.current && window.scrollY > 10) { saveScrollState(); - - // 뒤로가기임을 표시하는 플래그 설정 + sessionStorage.setItem('saveUrl', String(location.href)); sessionStorage.setItem(NAVIGATION_FLAG_KEY, 'back'); } }, [saveScrollState, NAVIGATION_FLAG_KEY]); @@ -155,7 +149,6 @@ export function useMemoScroll({ useEffect(() => { if (hasMountedRef.current) return; hasMountedRef.current = true; - const restored = restoreScrollState(); if (!restored) { @@ -226,6 +219,5 @@ export function useMemoScroll({ handleItemClick, saveScrollState, shouldFetch, - isRestoring:isRestoringRef.current }; } From 44b38a4bf7120f623257d162cdc3b3fea1d27cea Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Fri, 10 Oct 2025 12:07:28 +0900 Subject: [PATCH 03/21] =?UTF-8?q?[chore]=20=ED=99=95=EC=9D=B8=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/recipe/api/fetchRecipeComment.ts | 25 +++++++++++++++++--- src/domains/recipe/api/useRecipeComment.ts | 23 +++++++++--------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/domains/recipe/api/fetchRecipeComment.ts b/src/domains/recipe/api/fetchRecipeComment.ts index 51ed96e3..90d1f412 100644 --- a/src/domains/recipe/api/fetchRecipeComment.ts +++ b/src/domains/recipe/api/fetchRecipeComment.ts @@ -40,11 +40,11 @@ export const getRecipeComment = async (cocktailId: number): Promise { - const response = await fetch(`${getApi}/cocktails/${cocktailId}/comments/${commentId}`, { + const response = await fetch(`${getApi}/cocktails/${postId}/comments/${commentId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', @@ -58,4 +58,23 @@ export async function updateComment( console.error('서버 응답 에러:', errorText); throw new Error(`댓글 수정 실패: ${response.status}`); } -} \ No newline at end of file +} + +export async function deleteRecipeComment( + accessToken: string | null, + cocktailId: number, + commentId: number +): Promise { + const response = await fetch(`${getApi}/cocktails/${cocktailId}/comments/${commentId}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + 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 8b5129f1..437fd150 100644 --- a/src/domains/recipe/api/useRecipeComment.ts +++ b/src/domains/recipe/api/useRecipeComment.ts @@ -2,36 +2,35 @@ 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 { deleteComment} from '@/domains/community/api/fetchComment'; -import { getRecipeComment, updateComment } from './fetchRecipeComment'; +import { deleteRecipeComment, getRecipeComment, updateComment } from './fetchRecipeComment'; -export function useRecipeComments(cocktailId: number, user: User | null, accessToken: string | null) { +export function useRecipeComments(postId: number, user: User | null, accessToken: string | 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>( + const [deleteTarget, setDeleteTarget] = useState<{ commentId: number; postId: number } | null>( null ); const fetchData = useCallback(async () => { - const data = await getRecipeComment(cocktailId); + const data = await getRecipeComment(postId); if (!data) return; setComments(data); setIsEnd(false); - }, [cocktailId]); + }, [postId]); useEffect(() => { fetchData(); }, [fetchData]); - const handleUpdateComment = async (commentId: number,cocktailId:number,content: string) => { + const handleUpdateComment = async (postId:number,commentId: number,content: string) => { if (!user) { alert('로그인이 필요합니다'); return; } try { - await updateComment(accessToken!, cocktailId, commentId, content); + await updateComment(accessToken!, postId, commentId, content); setComments((prev) => prev ? prev.map((comment) => @@ -45,8 +44,8 @@ export function useRecipeComments(cocktailId: number, user: User | null, accessT } }; - const handleAskDeleteComment = (commentId: number, cocktailId: number) => { - setDeleteTarget({ commentId, cocktailId }); + const handleAskDeleteComment = (commentId: number, postId: number) => { + setDeleteTarget({ commentId, postId }); }; const handleConfirmDelete = async () => { @@ -57,7 +56,7 @@ export function useRecipeComments(cocktailId: number, user: User | null, accessT if (!deleteTarget) return; try { - await deleteComment(accessToken!, deleteTarget.cocktailId, deleteTarget.commentId); + await deleteRecipeComment(accessToken!, deleteTarget.postId, deleteTarget.commentId); setComments((prev) => prev ? prev.filter((c) => c.commentId !== deleteTarget.commentId) : prev ); @@ -74,7 +73,7 @@ export function useRecipeComments(cocktailId: number, user: User | null, accessT setIsLoading(true); try { - const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments?lastId=${lastCommentId}`); + const res = await fetch(`${getApi}/cocktails/${postId}/comments?lastId=${lastCommentId}`); const newComments = await res.json(); if (newComments.data.length === 0) { From 14f2ea884d23c11d9c1ea7078a20592e7274d8ed Mon Sep 17 00:00:00 2001 From: EunbinJung Date: Fri, 10 Oct 2025 12:49:18 +0900 Subject: [PATCH 04/21] =?UTF-8?q?=EB=8C=93=EA=B8=80=EC=97=90=EB=9F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/recipe/api/fetchRecipeComment.ts | 3 ++- src/domains/recipe/api/useRecipeComment.ts | 5 ++--- .../components/details/RecipeComment.tsx | 20 ++++++++++++++----- .../shared/components/comment/CommentList.tsx | 10 +++++----- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/domains/recipe/api/fetchRecipeComment.ts b/src/domains/recipe/api/fetchRecipeComment.ts index 90d1f412..c0bc5cef 100644 --- a/src/domains/recipe/api/fetchRecipeComment.ts +++ b/src/domains/recipe/api/fetchRecipeComment.ts @@ -41,9 +41,10 @@ export const getRecipeComment = async (cocktailId: number): Promise { + console.log(postId, typeof postId); const response = await fetch(`${getApi}/cocktails/${postId}/comments/${commentId}`, { method: 'PATCH', headers: { diff --git a/src/domains/recipe/api/useRecipeComment.ts b/src/domains/recipe/api/useRecipeComment.ts index 437fd150..14a59706 100644 --- a/src/domains/recipe/api/useRecipeComment.ts +++ b/src/domains/recipe/api/useRecipeComment.ts @@ -23,8 +23,7 @@ export function useRecipeComments(postId: number, user: User | null, accessToken fetchData(); }, [fetchData]); - const handleUpdateComment = async (postId:number,commentId: number,content: string) => { - + const handleUpdateComment = async (commentId: number, content: string) => { if (!user) { alert('로그인이 필요합니다'); return; @@ -44,7 +43,7 @@ export function useRecipeComments(postId: number, user: User | null, accessToken } }; - const handleAskDeleteComment = (commentId: number, postId: number) => { + const handleAskDeleteComment = (commentId: number) => { setDeleteTarget({ commentId, postId }); }; diff --git a/src/domains/recipe/components/details/RecipeComment.tsx b/src/domains/recipe/components/details/RecipeComment.tsx index dab7792a..d6d50146 100644 --- a/src/domains/recipe/components/details/RecipeComment.tsx +++ b/src/domains/recipe/components/details/RecipeComment.tsx @@ -6,19 +6,29 @@ import { useShallow } from 'zustand/shallow'; import ConfirmModal from '@/shared/components/modal-pop/ConfirmModal'; import { useRecipeComments } from '../../api/useRecipeComment'; -interface Props { - cocktailId:number +interface Props { + cocktailId: number; } -function RecipeComment({cocktailId}:Props) { +function RecipeComment({ cocktailId }: Props) { const { user, accessToken } = useAuthStore( useShallow((state) => ({ user: state.user, accessToken: state.accessToken, })) ); - const { comments, fetchData, handleAskDeleteComment, handleUpdateComment, loadMoreComments, isEnd, isLoading, deleteTarget, handleConfirmDelete, setDeleteTarget } = useRecipeComments(cocktailId, user, accessToken) - + const { + comments, + fetchData, + handleAskDeleteComment, + handleUpdateComment, + loadMoreComments, + isEnd, + isLoading, + deleteTarget, + handleConfirmDelete, + setDeleteTarget, + } = useRecipeComments(cocktailId, user, accessToken); return (
    diff --git a/src/domains/shared/components/comment/CommentList.tsx b/src/domains/shared/components/comment/CommentList.tsx index 09604016..f21bc736 100644 --- a/src/domains/shared/components/comment/CommentList.tsx +++ b/src/domains/shared/components/comment/CommentList.tsx @@ -10,8 +10,8 @@ import { usePrevious } from 'react-use'; type Props = { comments: CommentType[] | null; currentUserNickname?: string; - onUpdateComment: (commentId: number, postId: number, content: string) => Promise; - onDeleteComment: (commentId: number, postId: number) => void; + onUpdateComment: (commentId: number, content: string) => Promise; + onDeleteComment: (commentId: number) => void; onLoadMore?: (lastCommentId: number) => void; // ← 무한스크롤 콜백 isEnd?: boolean; isLoading: boolean; @@ -70,7 +70,7 @@ function CommentList({ >
      {rowVirtualizer.getVirtualItems().map(({ index, key, start }) => { - const { commentId, content, userNickName, createdAt, postId } = comments[index]; + const { commentId, content, userNickName, createdAt } = comments[index]; const isEditing = editCommentId === commentId; const isMyComment = comments && currentUserNickname === userNickName; @@ -113,7 +113,7 @@ function CommentList({ onSubmitEdit={() => { const updatedContent = editedContentMap[commentId]; if (!updatedContent) return; - onUpdateComment(commentId, postId, updatedContent).then(() => { + onUpdateComment(commentId, updatedContent).then(() => { setEditCommentId(null); setEditedContentMap((prev) => { const next = { ...prev }; @@ -122,7 +122,7 @@ function CommentList({ }); }); }} - onDelete={() => onDeleteComment(commentId, postId)} + onDelete={() => onDeleteComment(commentId)} onEdit={() => { setEditCommentId(commentId); setEditedContentMap((prev) => ({ From 749c0f77ab1ac5b751a5d01ff0f15825c43d5941 Mon Sep 17 00:00:00 2001 From: EunbinJung Date: Fri, 10 Oct 2025 12:50:14 +0900 Subject: [PATCH 05/21] =?UTF-8?q?=ED=8F=AC=EB=A7=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/layout.tsx | 1 - src/domains/mypage/components/pages/my-active/MyLike.tsx | 2 -- src/domains/recipe/components/main/Cocktails.tsx | 3 +-- src/domains/shared/hook/useIntersectionObserver.ts | 3 +-- 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 78f59c6f..3d86d643 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -17,7 +17,6 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - return ( diff --git a/src/domains/mypage/components/pages/my-active/MyLike.tsx b/src/domains/mypage/components/pages/my-active/MyLike.tsx index 333eb1fa..598575ac 100644 --- a/src/domains/mypage/components/pages/my-active/MyLike.tsx +++ b/src/domains/mypage/components/pages/my-active/MyLike.tsx @@ -3,8 +3,6 @@ import { getApi } from '@/app/api/config/appConfig'; import PostCard from '@/domains/community/main/PostCard'; import { useEffect, useState } from 'react'; - - function MyLike() { const [myLike, setMyLike] = useState([]); const [isLoading, setIsLoading] = useState(false); diff --git a/src/domains/recipe/components/main/Cocktails.tsx b/src/domains/recipe/components/main/Cocktails.tsx index 8248378e..2d912e11 100644 --- a/src/domains/recipe/components/main/Cocktails.tsx +++ b/src/domains/recipe/components/main/Cocktails.tsx @@ -76,8 +76,7 @@ function Cocktails() { useEffect(() => { if (!shouldFetch || isSearching) return; fetchData(); - - }, [shouldFetch, isSearching,alcoholBaseTypes,alcoholStrengths,cocktailTypes ]); + }, [shouldFetch, isSearching, alcoholBaseTypes, alcoholStrengths, cocktailTypes]); return (
      diff --git a/src/domains/shared/hook/useIntersectionObserver.ts b/src/domains/shared/hook/useIntersectionObserver.ts index 540462df..31c56008 100644 --- a/src/domains/shared/hook/useIntersectionObserver.ts +++ b/src/domains/shared/hook/useIntersectionObserver.ts @@ -8,7 +8,6 @@ export const useIntersectionObserver = ( const observer = useRef(null); useEffect(() => { - if (targetRef && targetRef.current) { observer.current = new IntersectionObserver(onIntersect, { root: null, @@ -22,5 +21,5 @@ export const useIntersectionObserver = ( observer.current.observe(targetRef.current); } return () => observer && observer.current?.disconnect(); - }, [targetRef, onIntersect,hasNextPage]); + }, [targetRef, onIntersect, hasNextPage]); }; From b987ff2f07b2f9443ff495bd3b177cf8edf7dc63 Mon Sep 17 00:00:00 2001 From: EunbinJung Date: Fri, 10 Oct 2025 12:56:19 +0900 Subject: [PATCH 06/21] =?UTF-8?q?=EC=BB=A4=EB=AE=A4=EB=8B=88=ED=8B=B0=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/community/hook/useComment.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domains/community/hook/useComment.ts b/src/domains/community/hook/useComment.ts index 6903a277..15b8a63c 100644 --- a/src/domains/community/hook/useComment.ts +++ b/src/domains/community/hook/useComment.ts @@ -23,7 +23,7 @@ export function useComments(postId: number, user: User | null, accessToken: stri fetchData(); }, [fetchData]); - const handleUpdateComment = async (commentId: number, postId: number, content: string) => { + const handleUpdateComment = async (commentId: number, content: string) => { if (!user) { alert('로그인이 필요합니다'); return; @@ -43,7 +43,7 @@ export function useComments(postId: number, user: User | null, accessToken: stri } }; - const handleAskDeleteComment = (commentId: number, postId: number) => { + const handleAskDeleteComment = (commentId: number) => { setDeleteTarget({ commentId, postId }); }; From e81d1099f50535f0c5d8b7f301f2c0b6b9fbe61b Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Fri, 10 Oct 2025 13:26:14 +0900 Subject: [PATCH 07/21] =?UTF-8?q?[chore]=ED=92=80=20=EC=A0=84=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/app/layout.tsx | 1 - .../community/hook/useCommentAnimation.ts | 2 +- .../components/pages/my-active/MyLike.tsx | 2 -- src/domains/recipe/api/fetchRecipeComment.ts | 7 +++--- src/domains/recipe/api/useRecipeComment.ts | 25 +++++++++++-------- .../recipe/components/main/Cocktails.tsx | 3 +-- .../shared/components/comment/CommentList.tsx | 8 +++--- .../shared/hook/useIntersectionObserver.ts | 3 +-- 8 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 78f59c6f..3d86d643 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -17,7 +17,6 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - return ( diff --git a/src/domains/community/hook/useCommentAnimation.ts b/src/domains/community/hook/useCommentAnimation.ts index aff9ac99..d75fb8f1 100644 --- a/src/domains/community/hook/useCommentAnimation.ts +++ b/src/domains/community/hook/useCommentAnimation.ts @@ -3,7 +3,7 @@ import { CommentType } from '../types/post'; import gsap from 'gsap'; export function useCommentEnterAnimation( - comments: CommentType[] | null, + comments: CommentType[] | null, parentRef: React.RefObject ) { const [prevFirstCommentId, setPrevFirstCommentId] = useState(null); diff --git a/src/domains/mypage/components/pages/my-active/MyLike.tsx b/src/domains/mypage/components/pages/my-active/MyLike.tsx index 333eb1fa..598575ac 100644 --- a/src/domains/mypage/components/pages/my-active/MyLike.tsx +++ b/src/domains/mypage/components/pages/my-active/MyLike.tsx @@ -3,8 +3,6 @@ import { getApi } from '@/app/api/config/appConfig'; import PostCard from '@/domains/community/main/PostCard'; import { useEffect, useState } from 'react'; - - function MyLike() { const [myLike, setMyLike] = useState([]); const [isLoading, setIsLoading] = useState(false); diff --git a/src/domains/recipe/api/fetchRecipeComment.ts b/src/domains/recipe/api/fetchRecipeComment.ts index 90d1f412..176fd74e 100644 --- a/src/domains/recipe/api/fetchRecipeComment.ts +++ b/src/domains/recipe/api/fetchRecipeComment.ts @@ -40,11 +40,12 @@ export const getRecipeComment = async (cocktailId: number): Promise { - const response = await fetch(`${getApi}/cocktails/${postId}/comments/${commentId}`, { + + const response = await fetch(`${getApi}/cocktails/${cocktailId}/comments/${commentId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', diff --git a/src/domains/recipe/api/useRecipeComment.ts b/src/domains/recipe/api/useRecipeComment.ts index 437fd150..59e8bab2 100644 --- a/src/domains/recipe/api/useRecipeComment.ts +++ b/src/domains/recipe/api/useRecipeComment.ts @@ -4,33 +4,36 @@ import { User } from '@/domains/shared/store/auth'; import { CommentType } from '@/domains/community/types/post'; import { deleteRecipeComment, getRecipeComment, updateComment } from './fetchRecipeComment'; -export function useRecipeComments(postId: number, user: User | null, accessToken: string | null) { +export function useRecipeComments(cocktailId: number, user: User | null, accessToken: string | null) { const [comments, setComments] = useState(null); const [isEnd, setIsEnd] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [deleteTarget, setDeleteTarget] = useState<{ commentId: number; postId: number } | null>( + const [deleteTarget, setDeleteTarget] = useState<{ commentId: number; cocktailId: number } | null>( null ); const fetchData = useCallback(async () => { - const data = await getRecipeComment(postId); + const data = await getRecipeComment(cocktailId); if (!data) return; setComments(data); setIsEnd(false); - }, [postId]); + }, [cocktailId]); useEffect(() => { fetchData(); }, [fetchData]); - const handleUpdateComment = async (postId:number,commentId: number,content: string) => { - + const handleUpdateComment = async ( + commentId: number, + content: string + ) => { + if (!user) { alert('로그인이 필요합니다'); return; } try { - await updateComment(accessToken!, postId, commentId, content); + await updateComment(accessToken!, cocktailId, commentId, content); setComments((prev) => prev ? prev.map((comment) => @@ -44,8 +47,8 @@ export function useRecipeComments(postId: number, user: User | null, accessToken } }; - const handleAskDeleteComment = (commentId: number, postId: number) => { - setDeleteTarget({ commentId, postId }); + const handleAskDeleteComment = (commentId: number, cocktailId: number) => { + setDeleteTarget({ commentId, cocktailId }); }; const handleConfirmDelete = async () => { @@ -56,7 +59,7 @@ export function useRecipeComments(postId: number, user: User | null, accessToken if (!deleteTarget) return; try { - await deleteRecipeComment(accessToken!, deleteTarget.postId, deleteTarget.commentId); + await deleteRecipeComment(accessToken!, deleteTarget.cocktailId, deleteTarget.commentId); setComments((prev) => prev ? prev.filter((c) => c.commentId !== deleteTarget.commentId) : prev ); @@ -73,7 +76,7 @@ export function useRecipeComments(postId: number, user: User | null, accessToken setIsLoading(true); try { - const res = await fetch(`${getApi}/cocktails/${postId}/comments?lastId=${lastCommentId}`); + const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments?lastId=${lastCommentId}`); const newComments = await res.json(); if (newComments.data.length === 0) { diff --git a/src/domains/recipe/components/main/Cocktails.tsx b/src/domains/recipe/components/main/Cocktails.tsx index 8248378e..2d912e11 100644 --- a/src/domains/recipe/components/main/Cocktails.tsx +++ b/src/domains/recipe/components/main/Cocktails.tsx @@ -76,8 +76,7 @@ function Cocktails() { useEffect(() => { if (!shouldFetch || isSearching) return; fetchData(); - - }, [shouldFetch, isSearching,alcoholBaseTypes,alcoholStrengths,cocktailTypes ]); + }, [shouldFetch, isSearching, alcoholBaseTypes, alcoholStrengths, cocktailTypes]); return (
      diff --git a/src/domains/shared/components/comment/CommentList.tsx b/src/domains/shared/components/comment/CommentList.tsx index 09604016..515135ab 100644 --- a/src/domains/shared/components/comment/CommentList.tsx +++ b/src/domains/shared/components/comment/CommentList.tsx @@ -10,8 +10,8 @@ import { usePrevious } from 'react-use'; type Props = { comments: CommentType[] | null; currentUserNickname?: string; - onUpdateComment: (commentId: number, postId: number, content: string) => Promise; - onDeleteComment: (commentId: number, postId: number) => void; + onUpdateComment: (commentId: number, content: string) => Promise; + onDeleteComment: (commentId: number) => void; onLoadMore?: (lastCommentId: number) => void; // ← 무한스크롤 콜백 isEnd?: boolean; isLoading: boolean; @@ -113,7 +113,7 @@ function CommentList({ onSubmitEdit={() => { const updatedContent = editedContentMap[commentId]; if (!updatedContent) return; - onUpdateComment(commentId, postId, updatedContent).then(() => { + onUpdateComment(commentId,updatedContent,).then(() => { setEditCommentId(null); setEditedContentMap((prev) => { const next = { ...prev }; @@ -122,7 +122,7 @@ function CommentList({ }); }); }} - onDelete={() => onDeleteComment(commentId, postId)} + onDelete={() => onDeleteComment(commentId)} onEdit={() => { setEditCommentId(commentId); setEditedContentMap((prev) => ({ diff --git a/src/domains/shared/hook/useIntersectionObserver.ts b/src/domains/shared/hook/useIntersectionObserver.ts index 540462df..31c56008 100644 --- a/src/domains/shared/hook/useIntersectionObserver.ts +++ b/src/domains/shared/hook/useIntersectionObserver.ts @@ -8,7 +8,6 @@ export const useIntersectionObserver = ( const observer = useRef(null); useEffect(() => { - if (targetRef && targetRef.current) { observer.current = new IntersectionObserver(onIntersect, { root: null, @@ -22,5 +21,5 @@ export const useIntersectionObserver = ( observer.current.observe(targetRef.current); } return () => observer && observer.current?.disconnect(); - }, [targetRef, onIntersect,hasNextPage]); + }, [targetRef, onIntersect, hasNextPage]); }; From 7ac9c4d9ec3dc033060ec87bdf0e6d87c0e4f119 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Fri, 10 Oct 2025 13:51:53 +0900 Subject: [PATCH 08/21] =?UTF-8?q?[feat]=20=EC=B9=B5=ED=85=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/recipe/api/useRecipeComment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domains/recipe/api/useRecipeComment.ts b/src/domains/recipe/api/useRecipeComment.ts index f59e8aea..19f91a54 100644 --- a/src/domains/recipe/api/useRecipeComment.ts +++ b/src/domains/recipe/api/useRecipeComment.ts @@ -44,7 +44,7 @@ export function useRecipeComments(cocktailId: number, user: User | null, accessT }; const handleAskDeleteComment = (commentId: number) => { - setDeleteTarget({ commentId, postId }); + setDeleteTarget({ commentId, cocktailId }); }; const handleConfirmDelete = async () => { From 943550ca1951f94c1dcfdaa0d7fca75317855f56 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Fri, 10 Oct 2025 15:18:23 +0900 Subject: [PATCH 09/21] =?UTF-8?q?[feat]=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=ED=82=B5=20=EB=B2=84=ED=8A=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/recipe/api/RecipeFetch.tsx | 29 ++++++--- .../recipe/components/main/CocktailList.tsx | 6 +- src/domains/recipe/types/types.ts | 1 + .../components/cocktail-card/CocktailCard.tsx | 4 +- src/domains/shared/components/keep/Keep.tsx | 5 +- src/domains/shared/hook/useMemoScroll.ts | 63 ++++++++++--------- 6 files changed, 66 insertions(+), 42 deletions(-) diff --git a/src/domains/recipe/api/RecipeFetch.tsx b/src/domains/recipe/api/RecipeFetch.tsx index 1fb008af..7241a77d 100644 --- a/src/domains/recipe/api/RecipeFetch.tsx +++ b/src/domains/recipe/api/RecipeFetch.tsx @@ -2,7 +2,7 @@ import { getApi } from '@/app/api/config/appConfig'; import { Cocktail } from '../types/types'; -import { Dispatch, SetStateAction, useCallback } from 'react'; +import { Dispatch, SetStateAction, useCallback, useState } from 'react'; interface Props { setData: React.Dispatch>; @@ -21,6 +21,7 @@ export const RecipeFetch = ({ setHasNextPage, SIZE = 20, }: Props) => { + const fetchData = useCallback(async () => { // 쿼리파라미터에 값 넣기 if (!hasNextPage) return; @@ -30,16 +31,28 @@ export const RecipeFetch = ({ url.searchParams.set('lastId', String(lastId)); } - const res = await fetch(url.toString(), { method: 'GET' }); - if (!res.ok) throw new Error('레시피 데이터 요청실패'); + const [recipeRes,keepRes] = await Promise.all([ + fetch(url.toString(), { method: 'GET' }), + fetch(`${getApi}/me/bar`, { + method: 'GET', + credentials:'include' + }) + ]) + if (!recipeRes.ok || !keepRes.ok) throw new Error('레시피 데이터 요청실패'); + + const [recipeJson, barJson] = await Promise.all([recipeRes.json(), keepRes.json()]); + const favoriteIds = new Set(barJson.data.items.map((m:{cocktailId:number}) => m.cocktailId)) - const json = await res.json(); - const list: Cocktail[] = json.data ?? []; + const merged = recipeJson.data.map((cocktail:Cocktail) => ({ + ...cocktail, + isFavorited:favoriteIds.has(cocktail.cocktailId) + })) + setData(merged) + + const list: Cocktail[] = recipeJson.data ?? []; // 중복 아이디 에러있어서 Map으로 Merge - 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/main/CocktailList.tsx b/src/domains/recipe/components/main/CocktailList.tsx index 16350365..97a74363 100644 --- a/src/domains/recipe/components/main/CocktailList.tsx +++ b/src/domains/recipe/components/main/CocktailList.tsx @@ -1,5 +1,5 @@ 'use client'; -import { useEffect, useRef } from 'react'; +import {useRef } from 'react'; import Link from 'next/link'; import { useIntersectionObserver } from '@/domains/shared/hook/useIntersectionObserver'; import { Cocktail } from '../../types/types'; @@ -24,7 +24,6 @@ function CocktailList({ cocktails, RecipeFetch, hasNextPage, lastId, onItemClick }; useIntersectionObserver(cocktailRef, onIntersect, hasNextPage); - // ⬇️ 파일 최상단 근처: 공통 디버그 토글 return (
        {cocktails.map( - ({ cocktailImgUrl, cocktailId, cocktailName, cocktailNameKo, alcoholStrength }) => ( + ({ cocktailImgUrl, cocktailId, cocktailName, cocktailNameKo, alcoholStrength,isFavorited }) => (
      • {alcoholTitle &&
        - {id && } + {id && }
    )} diff --git a/src/domains/shared/components/keep/Keep.tsx b/src/domains/shared/components/keep/Keep.tsx index bc306688..ed71a73a 100644 --- a/src/domains/shared/components/keep/Keep.tsx +++ b/src/domains/shared/components/keep/Keep.tsx @@ -9,13 +9,14 @@ import { useToast } from '@/shared/hook/useToast'; interface Props { className?: string; cocktailId?: number; + favor?:boolean } // ID는 커뮤니티 공유할때 id 타입보고 옵셔널 체크 풀어주세요! // 만약 타입 안맞는다면 그냥 두셔도 됩니다. -function Keep({ className, cocktailId }: Props) { +function Keep({ className, cocktailId,favor}: Props) { const { toastSuccess } = useToast(); - const [isClick, setIsClick] = useState(false); + const [isClick, setIsClick] = useState(favor); 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 0b9d35e7..5be220e7 100644 --- a/src/domains/shared/hook/useMemoScroll.ts +++ b/src/domains/shared/hook/useMemoScroll.ts @@ -10,6 +10,7 @@ interface ScrollState { data: T[]; lastId: number | null; hasNextPage: boolean; + timestamp: number; } // 뒤로가기시 스크롤위치 기억 함수 @@ -36,14 +37,15 @@ export function useMemoScroll({ // 스크롤 위치와 데이터 저장 const saveScrollState = useCallback(() => { - // 복원 중일때 저장 X - if (isRestoringRef.current) return; + // 복원 중일때와 일정 스크롤 이상 안내려오면 저장 안함 + if (isRestoringRef.current || window.scrollY < 10) return; const scrollState: ScrollState = { scrollY: window.scrollY, data: data, lastId: lastId, hasNextPage: hasNextPage, + timestamp: Date.now(), }; sessionStorage.setItem(storageKey, JSON.stringify(scrollState)); @@ -74,46 +76,49 @@ export function useMemoScroll({ data: savedData, lastId: savedLastId, hasNextPage: savedHasNextPage, + timestamp, }: ScrollState = parsed; - if (savedData.length > 0 && scrollY > 10) { + // 세션이 30분 지나가면 세션 삭제 + const isRecent = Date.now() - timestamp < 30 * 60 * 1000; + + if (isRecent && savedData.length > 0 && scrollY > 10) { // 조건 충족 시 스크롤 데이터 복원 + isRestoringRef.current = true; setData(savedData); setLastId(savedLastId); setHasNextPage(savedHasNextPage); - const restoreScroll = (targetScrollY: number, maxAttempts = 10, interval = 100) => { - let attempts = 0; - - const tryScroll = () => { - window.scrollTo({ - top: targetScrollY, - behavior: 'auto', - }); + const restoreScroll = () => { + // 스크롤 복원 시도 로직 + window.scrollTo({ + top: scrollY, + behavior: 'instant', + }); + setTimeout(() => { + // 무한 스크롤시 데이터에따라 한번에 스크롤 복원 안되는 현상 발생 => 재시도 로직 const currentScroll = window.scrollY; - const threshold = 50; - console.log( - `Attempt ${attempts + 1}: target=${targetScrollY}, current=${currentScroll}` - ); - - if (Math.abs(currentScroll - targetScrollY) < threshold || attempts >= maxAttempts) { - console.log('Scroll restoration completed'); - return; + const diff = Math.abs(currentScroll - scrollY); + + if (diff > 5) { + // 스크롤 재 조정이 필요한 경우 실행 + window.scrollTo({ + top: scrollY, + behavior: 'instant', + }); } - attempts++; - setTimeout(tryScroll, interval); - }; - - tryScroll(); + setTimeout(() => { + // 복원 완료 ref초기화 + isRestoringRef.current = false; + scrollRestoredRef.current = true; + }, 300); + }, 100); }; - // 사용 - restoreScroll(scrollY); - // 재 조정시 애니메이션 매끄럽게 처리 setTimeout(restoreScroll, 0); requestAnimationFrame(() => { @@ -140,7 +145,8 @@ export function useMemoScroll({ const handleItemClick = useCallback(() => { if (!isRestoringRef.current && window.scrollY > 10) { saveScrollState(); - sessionStorage.setItem('saveUrl', String(location.href)); + + // 뒤로가기임을 표시하는 플래그 설정 sessionStorage.setItem(NAVIGATION_FLAG_KEY, 'back'); } }, [saveScrollState, NAVIGATION_FLAG_KEY]); @@ -149,6 +155,7 @@ export function useMemoScroll({ useEffect(() => { if (hasMountedRef.current) return; hasMountedRef.current = true; + const restored = restoreScrollState(); if (!restored) { From 116298ddaf3f0b7048391db03d90d987b7a664a7 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Fri, 10 Oct 2025 15:53:40 +0900 Subject: [PATCH 10/21] =?UTF-8?q?[feat]=20=EB=82=98=EB=A7=8C=EC=9D=98=20?= =?UTF-8?q?=EB=B0=94=20=EB=9D=BC=EB=B2=A8=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/pages/my-active/MyComment.tsx | 5 +++-- .../mypage/components/pages/my-bar/MyBar.tsx | 20 ++++++++++++------- src/domains/mypage/utills/abvMap.ts | 11 ++++++++++ src/domains/recipe/api/RecipeFetch.tsx | 10 ++++------ 4 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 src/domains/mypage/utills/abvMap.ts diff --git a/src/domains/mypage/components/pages/my-active/MyComment.tsx b/src/domains/mypage/components/pages/my-active/MyComment.tsx index c52be8b8..0eb74b12 100644 --- a/src/domains/mypage/components/pages/my-active/MyComment.tsx +++ b/src/domains/mypage/components/pages/my-active/MyComment.tsx @@ -16,6 +16,7 @@ function MyComment() { console.log(json); setMyComment(json.data.items); }; + console.log(myComment) useEffect(() => { fetchComment(); @@ -23,13 +24,13 @@ function MyComment() { return (
    - {/* {CommentList.length !== 0 ? ( + {CommentList.length !== 0 ? ( ) : (

    작성한 댓글이 없습니다.

    - )} */} + )}
    ); } diff --git a/src/domains/mypage/components/pages/my-bar/MyBar.tsx b/src/domains/mypage/components/pages/my-bar/MyBar.tsx index bb96e8b2..43691ba5 100644 --- a/src/domains/mypage/components/pages/my-bar/MyBar.tsx +++ b/src/domains/mypage/components/pages/my-bar/MyBar.tsx @@ -1,5 +1,6 @@ 'use client'; import { getApi } from '@/app/api/config/appConfig'; +import { abvMap } from '@/domains/mypage/utills/abvMap'; import CocktailCard from '@/domains/shared/components/cocktail-card/CocktailCard'; import Link from 'next/link'; import { useEffect, useState } from 'react'; @@ -7,14 +8,16 @@ import { useEffect, useState } from 'react'; interface MyCocktail { cocktailId: number; cocktailName: string; + cocktailNameKo: string; id: number; imageUrl: string; + alcoholStrength: string; } function MyBar() { const [myCocktail, setMyCocktail] = useState([]); const fetchData = async () => { - const res = await fetch(`${getApi}/me/bar`, { + const res = await fetch(`${getApi}/me/bar/detail`, { method: 'GET', credentials: 'include', }); @@ -37,17 +40,20 @@ function MyBar() { md:[grid-template-columns:repeat(3,minmax(0,250px))] " > - {myCocktail.map(({ cocktailId, cocktailName, imageUrl }) => ( - + {myCocktail.map(({ cocktailId, cocktailName, imageUrl, cocktailNameKo, alcoholStrength }) => { + const alcohol = abvMap(alcoholStrength) + return ( + - - ))} + ) + })} ) : (
    diff --git a/src/domains/mypage/utills/abvMap.ts b/src/domains/mypage/utills/abvMap.ts new file mode 100644 index 00000000..0e51bbfa --- /dev/null +++ b/src/domains/mypage/utills/abvMap.ts @@ -0,0 +1,11 @@ +export function abvMap(input:string){ + if(!input) return '' + switch (input) { + case 'NON_ALCOHOLIC': return '논 알콜' + case 'WEAK': return '약한 도수' + case 'LIGHT': return '가벼운 도수' + case 'MEDIUM': return '중간 도수' + case 'STRONG': return '센 도수' + case 'VERY_STRONG' : return '매우 센 도수' + } +} \ No newline at end of file diff --git a/src/domains/recipe/api/RecipeFetch.tsx b/src/domains/recipe/api/RecipeFetch.tsx index 7241a77d..bdfb207f 100644 --- a/src/domains/recipe/api/RecipeFetch.tsx +++ b/src/domains/recipe/api/RecipeFetch.tsx @@ -41,19 +41,17 @@ export const RecipeFetch = ({ if (!recipeRes.ok || !keepRes.ok) throw new Error('레시피 데이터 요청실패'); const [recipeJson, barJson] = await Promise.all([recipeRes.json(), keepRes.json()]); - const favoriteIds = new Set(barJson.data.items.map((m:{cocktailId:number}) => m.cocktailId)) + const bars = Array.isArray(barJson?.data) ? barJson.data : []; + const favoriteIds = new Set(bars.map((m:{cocktailId:number}) => m.cocktailId)) + const list: Cocktail[] = recipeJson.data ?? []; + const merged = recipeJson.data.map((cocktail:Cocktail) => ({ ...cocktail, isFavorited:favoriteIds.has(cocktail.cocktailId) })) setData(merged) - const list: Cocktail[] = recipeJson.data ?? []; - - // 중복 아이디 에러있어서 Map으로 Merge - - if (list.length > 0) { setLastId(list[list.length - 1].cocktailId); } From cf9b1817cd234d9e69c8b474e579c549802819c3 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Sat, 11 Oct 2025 14:15:00 +0900 Subject: [PATCH 11/21] =?UTF-8?q?[feat]=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=95=9C=20=EC=9C=A0=EC=A0=80=20=ED=82=B5=EB=B2=84=ED=8A=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.ts | 1 + package-lock.json | 16 + package.json | 3 +- .../community/hook/useCommentAnimation.ts | 2 +- src/domains/mypage/components/ToggleBtn.tsx | 1 - .../components/pages/my-active/MyComment.tsx | 5 +- .../components/pages/my-alarm/MyAlarm.tsx | 4 + .../mypage/components/pages/my-bar/MyBar.tsx | 45 ++- src/domains/mypage/main/MyNav.tsx | 14 +- src/domains/mypage/utills/abvMap.ts | 24 +- src/domains/recipe/api/RecipeFetch.tsx | 41 ++- src/domains/recipe/api/useRecipeComment.ts | 13 +- .../recipe/components/main/Accordion.tsx | 2 - .../recipe/components/main/CocktailList.tsx | 24 +- .../recipe/components/main/Cocktails.tsx | 26 +- .../recipe/skeleton/SkeletonRecipe.tsx | 2 +- .../components/cocktail-card/CocktailCard.tsx | 4 +- src/domains/shared/components/keep/Keep.tsx | 13 +- .../shared/hook/useIntersectionObserver.ts | 4 +- src/domains/shared/hook/useMemoScroll.ts | 330 +++++++----------- src/shared/styles/global.css | 4 + 21 files changed, 273 insertions(+), 305 deletions(-) diff --git a/next.config.ts b/next.config.ts index 49025491..121b43bc 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,6 +1,7 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { + images: { remotePatterns: [ { diff --git a/package-lock.json b/package-lock.json index bcea2d9b..497c4796 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-hot-toast": "^2.6.0", + "react-intersection-observer": "^9.16.0", "react-use": "^17.6.0" }, "devDependencies": { @@ -8243,6 +8244,21 @@ "react-dom": ">=16" } }, + "node_modules/react-intersection-observer": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", + "integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==", + "license": "MIT", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 7654fec6..30cba0cb 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-hot-toast": "^2.6.0", + "react-intersection-observer": "^9.16.0", "react-use": "^17.6.0" }, "devDependencies": { @@ -61,4 +62,4 @@ }, "homepage": "https://github.com/prgrms-web-devcourse-final-project/WEB5_6_HaeDokCoding_FE#readme", "description": "" -} \ No newline at end of file +} diff --git a/src/domains/community/hook/useCommentAnimation.ts b/src/domains/community/hook/useCommentAnimation.ts index d75fb8f1..aff9ac99 100644 --- a/src/domains/community/hook/useCommentAnimation.ts +++ b/src/domains/community/hook/useCommentAnimation.ts @@ -3,7 +3,7 @@ import { CommentType } from '../types/post'; import gsap from 'gsap'; export function useCommentEnterAnimation( - comments: CommentType[] | null, + comments: CommentType[] | null, parentRef: React.RefObject ) { const [prevFirstCommentId, setPrevFirstCommentId] = useState(null); diff --git a/src/domains/mypage/components/ToggleBtn.tsx b/src/domains/mypage/components/ToggleBtn.tsx index 4443525c..d066618e 100644 --- a/src/domains/mypage/components/ToggleBtn.tsx +++ b/src/domains/mypage/components/ToggleBtn.tsx @@ -16,7 +16,6 @@ function ToggleBtn() { credentials: 'include', }); const json = await res.json(); - console.log(json); setIsAlarm(json.data.enabled); } catch { console.error(); diff --git a/src/domains/mypage/components/pages/my-active/MyComment.tsx b/src/domains/mypage/components/pages/my-active/MyComment.tsx index 0eb74b12..c52be8b8 100644 --- a/src/domains/mypage/components/pages/my-active/MyComment.tsx +++ b/src/domains/mypage/components/pages/my-active/MyComment.tsx @@ -16,7 +16,6 @@ function MyComment() { console.log(json); setMyComment(json.data.items); }; - console.log(myComment) useEffect(() => { fetchComment(); @@ -24,13 +23,13 @@ function MyComment() { return (
    - {CommentList.length !== 0 ? ( + {/* {CommentList.length !== 0 ? ( ) : (

    작성한 댓글이 없습니다.

    - )} + )} */}
    ); } diff --git a/src/domains/mypage/components/pages/my-alarm/MyAlarm.tsx b/src/domains/mypage/components/pages/my-alarm/MyAlarm.tsx index d0b2431f..512248bd 100644 --- a/src/domains/mypage/components/pages/my-alarm/MyAlarm.tsx +++ b/src/domains/mypage/components/pages/my-alarm/MyAlarm.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import Alarm from '../../Alarm'; import { getApi } from '@/app/api/config/appConfig'; +import TextButton from '@/shared/components/button/TextButton'; interface MyAlarm { notificationId: number; @@ -29,6 +30,9 @@ function MyAlarm() { return (
    +
    + 전체삭제 +
    {myAlarm.length !== 0 ? ( myAlarm.map(({ notificationId, title, content }) => ( diff --git a/src/domains/mypage/components/pages/my-bar/MyBar.tsx b/src/domains/mypage/components/pages/my-bar/MyBar.tsx index 43691ba5..3bceb240 100644 --- a/src/domains/mypage/components/pages/my-bar/MyBar.tsx +++ b/src/domains/mypage/components/pages/my-bar/MyBar.tsx @@ -2,6 +2,7 @@ import { getApi } from '@/app/api/config/appConfig'; import { abvMap } from '@/domains/mypage/utills/abvMap'; import CocktailCard from '@/domains/shared/components/cocktail-card/CocktailCard'; +import TextButton from '@/shared/components/button/TextButton'; import Link from 'next/link'; import { useEffect, useState } from 'react'; @@ -31,29 +32,35 @@ function MyBar() { return (
    +
    + 전체삭제 +
    {myCocktail.length !== 0 ? (
    - {myCocktail.map(({ cocktailId, cocktailName, imageUrl, cocktailNameKo, alcoholStrength }) => { - const alcohol = abvMap(alcoholStrength) - return ( - - - ) - })} + {myCocktail.map( + ({ cocktailId, cocktailName, imageUrl, cocktailNameKo, alcoholStrength }) => { + const alcohol = abvMap(alcoholStrength); + return ( + + + + ); + } + )}
    ) : (
    diff --git a/src/domains/mypage/main/MyNav.tsx b/src/domains/mypage/main/MyNav.tsx index fac49fd5..c0579e76 100644 --- a/src/domains/mypage/main/MyNav.tsx +++ b/src/domains/mypage/main/MyNav.tsx @@ -92,17 +92,9 @@ function MyNav() { role="tabpanel" aria-labelledby={`main-tab-${i}`} hidden={isActive !== i} - > - {/* 필요하면 여기 메인 탭별 콘텐츠 렌더 */} -
    + >
    ))} - {(isActive == 0 || isActive == 2) && ( - setIsDeleteAll(!isDeleteAll)}> - 전체삭제 - - )} - {isActive == 1 && (
    + > ))}
); diff --git a/src/domains/mypage/utills/abvMap.ts b/src/domains/mypage/utills/abvMap.ts index 0e51bbfa..975ab58d 100644 --- a/src/domains/mypage/utills/abvMap.ts +++ b/src/domains/mypage/utills/abvMap.ts @@ -1,11 +1,17 @@ -export function abvMap(input:string){ - if(!input) return '' +export function abvMap(input: string) { + if (!input) return ''; switch (input) { - case 'NON_ALCOHOLIC': return '논 알콜' - case 'WEAK': return '약한 도수' - case 'LIGHT': return '가벼운 도수' - case 'MEDIUM': return '중간 도수' - case 'STRONG': return '센 도수' - case 'VERY_STRONG' : return '매우 센 도수' + case 'NON_ALCOHOLIC': + return '논 알콜'; + case 'WEAK': + return '약한 도수'; + case 'LIGHT': + return '가벼운 도수'; + case 'MEDIUM': + return '중간 도수'; + case 'STRONG': + return '센 도수'; + case 'VERY_STRONG': + return '매우 센 도수'; } -} \ No newline at end of file +} diff --git a/src/domains/recipe/api/RecipeFetch.tsx b/src/domains/recipe/api/RecipeFetch.tsx index bdfb207f..ac4a34b7 100644 --- a/src/domains/recipe/api/RecipeFetch.tsx +++ b/src/domains/recipe/api/RecipeFetch.tsx @@ -3,6 +3,7 @@ import { getApi } from '@/app/api/config/appConfig'; import { Cocktail } from '../types/types'; import { Dispatch, SetStateAction, useCallback, useState } from 'react'; +import { useAuthStore } from '@/domains/shared/store/auth'; interface Props { setData: React.Dispatch>; @@ -12,6 +13,7 @@ interface Props { setHasNextPage: Dispatch>; SIZE?: number; } + // api/cocktais fetch용 export const RecipeFetch = ({ setData, @@ -22,6 +24,7 @@ export const RecipeFetch = ({ SIZE = 20, }: Props) => { + const user = useAuthStore() const fetchData = useCallback(async () => { // 쿼리파라미터에 값 넣기 if (!hasNextPage) return; @@ -30,32 +33,34 @@ export const RecipeFetch = ({ 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 ?? [] - const [recipeRes,keepRes] = await Promise.all([ - fetch(url.toString(), { method: 'GET' }), - fetch(`${getApi}/me/bar`, { + if (user) { + const keepRes = await fetch(`${getApi}/me/bar`, { method: 'GET', credentials:'include' }) - ]) - if (!recipeRes.ok || !keepRes.ok) throw new Error('레시피 데이터 요청실패'); - - const [recipeJson, barJson] = await Promise.all([recipeRes.json(), keepRes.json()]); - const bars = Array.isArray(barJson?.data) ? barJson.data : []; - - const favoriteIds = new Set(bars.map((m:{cocktailId:number}) => m.cocktailId)) - const list: Cocktail[] = recipeJson.data ?? []; - - const merged = recipeJson.data.map((cocktail:Cocktail) => ({ - ...cocktail, - isFavorited:favoriteIds.has(cocktail.cocktailId) - })) - setData(merged) - + 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/useRecipeComment.ts b/src/domains/recipe/api/useRecipeComment.ts index 19f91a54..9fdd8e88 100644 --- a/src/domains/recipe/api/useRecipeComment.ts +++ b/src/domains/recipe/api/useRecipeComment.ts @@ -4,13 +4,18 @@ import { User } from '@/domains/shared/store/auth'; import { CommentType } from '@/domains/community/types/post'; import { deleteRecipeComment, getRecipeComment, updateComment } from './fetchRecipeComment'; -export function useRecipeComments(cocktailId: number, user: User | null, accessToken: string | null) { +export function useRecipeComments( + cocktailId: number, + user: User | null, + accessToken: string | 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 [deleteTarget, setDeleteTarget] = useState<{ + commentId: number; + cocktailId: number; + } | null>(null); const fetchData = useCallback(async () => { const data = await getRecipeComment(cocktailId); diff --git a/src/domains/recipe/components/main/Accordion.tsx b/src/domains/recipe/components/main/Accordion.tsx index 685053d0..771c5a1b 100644 --- a/src/domains/recipe/components/main/Accordion.tsx +++ b/src/domains/recipe/components/main/Accordion.tsx @@ -135,8 +135,6 @@ function Accordion({ setAlcoholBaseTypes, setCocktailTypes, setAlcoholStrengths const newUrl = `${pathname}?${queryString}`; router.push(newUrl); - // shallow routing으로 URL만 변경 - // 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 97a74363..0b2f7e63 100644 --- a/src/domains/recipe/components/main/CocktailList.tsx +++ b/src/domains/recipe/components/main/CocktailList.tsx @@ -1,19 +1,19 @@ 'use client'; -import {useRef } from 'react'; +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; - onItemClick: () => void; } -function CocktailList({ cocktails, RecipeFetch, hasNextPage, lastId, onItemClick }: Props) { +function CocktailList({ cocktails, RecipeFetch, hasNextPage, lastId }: Props) { const cocktailRef = useRef(null); const onIntersect: IntersectionObserverCallback = ([entry]) => { if (!RecipeFetch) return; @@ -24,7 +24,12 @@ function CocktailList({ cocktails, RecipeFetch, hasNextPage, lastId, onItemClick }; 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, + isFavorited, + }) => ( +
  • ({ - storageKey: 'cocktails_scroll_state', - eventName: 'resetCocktailsScroll', - }); + 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 }); + const { searchApi, setAlcoholBaseTypes, @@ -74,9 +65,9 @@ function Cocktails() { // 일반 fetch useEffect(() => { - if (!shouldFetch || isSearching) return; + if (isSearching) return; fetchData(); - }, [shouldFetch, isSearching, alcoholBaseTypes, alcoholStrengths, cocktailTypes]); + }, [isSearching, alcoholBaseTypes, alcoholStrengths, cocktailTypes]); return (
    @@ -100,7 +91,6 @@ function Cocktails() { RecipeFetch={isSearching ? undefined : fetchData} hasNextPage={isSearching ? false : hasNextPage} lastId={lastId} - onItemClick={handleItemClick} /> )}
    diff --git a/src/domains/recipe/skeleton/SkeletonRecipe.tsx b/src/domains/recipe/skeleton/SkeletonRecipe.tsx index 38476fd1..a2fa7716 100644 --- a/src/domains/recipe/skeleton/SkeletonRecipe.tsx +++ b/src/domains/recipe/skeleton/SkeletonRecipe.tsx @@ -25,7 +25,7 @@ function SkeletonRecipe() { lg:[grid-template-columns:repeat(4,minmax(0,250px))] " > - {Array.from({ length: 8 }).map((_, i) => ( + {Array.from({ length: 20 }).map((_, i) => (
  • diff --git a/src/domains/shared/components/cocktail-card/CocktailCard.tsx b/src/domains/shared/components/cocktail-card/CocktailCard.tsx index a9fb4768..b113c4fa 100644 --- a/src/domains/shared/components/cocktail-card/CocktailCard.tsx +++ b/src/domains/shared/components/cocktail-card/CocktailCard.tsx @@ -17,7 +17,7 @@ interface Props { className?: string; textSize1?: string; textSize2?: string; - favor?:boolean + favor?: boolean; } function CocktailCard({ @@ -30,7 +30,7 @@ function CocktailCard({ textSize2, alcohol, id, - favor + favor, }: Props) { const alcoholTitle = labelTitle(alcohol); diff --git a/src/domains/shared/components/keep/Keep.tsx b/src/domains/shared/components/keep/Keep.tsx index ed71a73a..16020939 100644 --- a/src/domains/shared/components/keep/Keep.tsx +++ b/src/domains/shared/components/keep/Keep.tsx @@ -5,25 +5,30 @@ import KeepIconActive from '@/shared/assets/icons/keep_active_36.svg'; import { useState } from 'react'; import { deleteKeep, postKeep } from '../../api/keep/keep'; import { useToast } from '@/shared/hook/useToast'; +import { useAuthStore } from '../../store/auth'; interface Props { className?: string; cocktailId?: number; - favor?:boolean + favor?: boolean; } // ID는 커뮤니티 공유할때 id 타입보고 옵셔널 체크 풀어주세요! // 만약 타입 안맞는다면 그냥 두셔도 됩니다. -function Keep({ className, cocktailId,favor}: Props) { - const { toastSuccess } = useToast(); +function Keep({ className, cocktailId, favor }: Props) { + const user = useAuthStore() + const {toastInfo, toastSuccess } = useToast(); const [isClick, setIsClick] = useState(favor); const handleClick = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); + if (!user) { + toastInfo('로그인 후 이용 가능합니다.') + return + } setIsClick(!isClick); - try { if (!cocktailId) return; if (!isClick) { diff --git a/src/domains/shared/hook/useIntersectionObserver.ts b/src/domains/shared/hook/useIntersectionObserver.ts index 31c56008..85ddc67b 100644 --- a/src/domains/shared/hook/useIntersectionObserver.ts +++ b/src/domains/shared/hook/useIntersectionObserver.ts @@ -11,7 +11,7 @@ export const useIntersectionObserver = ( if (targetRef && targetRef.current) { observer.current = new IntersectionObserver(onIntersect, { root: null, - rootMargin: '0px', + rootMargin: '200px', threshold: 1.0, }); if (!hasNextPage) { @@ -21,5 +21,5 @@ export const useIntersectionObserver = ( observer.current.observe(targetRef.current); } return () => observer && observer.current?.disconnect(); - }, [targetRef, onIntersect, hasNextPage]); + }, [targetRef, onIntersect]); }; diff --git a/src/domains/shared/hook/useMemoScroll.ts b/src/domains/shared/hook/useMemoScroll.ts index 5be220e7..c519acfd 100644 --- a/src/domains/shared/hook/useMemoScroll.ts +++ b/src/domains/shared/hook/useMemoScroll.ts @@ -1,230 +1,156 @@ -import { useEffect, useRef, useState, useCallback } from 'react'; - -interface UseScrollRestorationProps { - storageKey: string; - eventName?: string; -} - -interface ScrollState { - scrollY: number; - data: T[]; - lastId: number | null; - hasNextPage: boolean; - timestamp: number; +// 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; // 선택: 있으면 조기 종료에 사용 } -// 뒤로가기시 스크롤위치 기억 함수 - -export function useMemoScroll({ - storageKey, - eventName = 'resetScroll', -}: UseScrollRestorationProps) { - // 뒤로가기를 통해 목록 복원을 저장해주는 플래그 - const NAVIGATION_FLAG_KEY = `${storageKey}_nav_flag`; +type SavedShape = { targetId: number | null; scrollY: number }; - // 실제 렌더링 되는 데이터 - const [data, setData] = useState([]); - const [lastId, setLastId] = useState(null); - const [hasNextPage, setHasNextPage] = useState(true); - const [shouldFetch, setShouldFetch] = useState(false); +export function useScrollRestore({ + lastId, + fetchData, + currentDataLength, + hasNextPage, +}: UseScrollRestoreProps) { + const pathname = usePathname(); + const KEY = `scroll-${pathname}`; - // 스크롤 복원중일 때 값이 바뀜 const isRestoringRef = useRef(false); - // 스크롤 복원 후 값 바뀜 - const scrollRestoredRef = useRef(false); - // 컴포넌트 마운트시 값 바뀜 - const hasMountedRef = useRef(false); - - // 스크롤 위치와 데이터 저장 - const saveScrollState = useCallback(() => { - // 복원 중일때와 일정 스크롤 이상 안내려오면 저장 안함 - if (isRestoringRef.current || window.scrollY < 10) return; - - const scrollState: ScrollState = { - scrollY: window.scrollY, - data: data, - lastId: lastId, - hasNextPage: hasNextPage, - timestamp: Date.now(), - }; + const hasRestoredRef = useRef(false); + const lastIdRef = useRef(lastId); + const lenRef = useRef(currentDataLength); - sessionStorage.setItem(storageKey, JSON.stringify(scrollState)); - }, [data, lastId, hasNextPage, storageKey]); - - // 저장된 상태 복원 - const restoreScrollState = useCallback(() => { - const saved = sessionStorage.getItem(storageKey); - const navFlag = sessionStorage.getItem(NAVIGATION_FLAG_KEY); + 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; + } - if (!saved) return false; + 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; - // 네비게이션 플래그가 없으면 복원하지 않음 (새로 진입한 경우) - if (navFlag !== 'back') { - sessionStorage.removeItem(storageKey); - return false; + const raw = sessionStorage.getItem(KEY); + if (!raw) { + hasRestoredRef.current = true; + return; } - // 플래그 사용 후 제거 - sessionStorage.removeItem(NAVIGATION_FLAG_KEY); - + let saved: SavedShape | null = null; try { - // 데이터 복원 - const parsed = JSON.parse(saved); - - const { - scrollY, - data: savedData, - lastId: savedLastId, - hasNextPage: savedHasNextPage, - timestamp, - }: ScrollState = parsed; - - // 세션이 30분 지나가면 세션 삭제 - const isRecent = Date.now() - timestamp < 30 * 60 * 1000; - - if (isRecent && savedData.length > 0 && scrollY > 10) { - // 조건 충족 시 스크롤 데이터 복원 - - isRestoringRef.current = true; - - setData(savedData); - setLastId(savedLastId); - setHasNextPage(savedHasNextPage); - - const restoreScroll = () => { - // 스크롤 복원 시도 로직 - window.scrollTo({ - top: scrollY, - behavior: 'instant', - }); - - setTimeout(() => { - // 무한 스크롤시 데이터에따라 한번에 스크롤 복원 안되는 현상 발생 => 재시도 로직 - const currentScroll = window.scrollY; - const diff = Math.abs(currentScroll - scrollY); - - if (diff > 5) { - // 스크롤 재 조정이 필요한 경우 실행 - window.scrollTo({ - top: scrollY, - behavior: 'instant', - }); - } - - setTimeout(() => { - // 복원 완료 ref초기화 - isRestoringRef.current = false; - scrollRestoredRef.current = true; - }, 300); - }, 100); - }; - - // 재 조정시 애니메이션 매끄럽게 처리 - setTimeout(restoreScroll, 0); - requestAnimationFrame(() => { - setTimeout(restoreScroll, 50); - }); - - return true; - } - } catch (err) { - console.error(err); - sessionStorage.removeItem(storageKey); - return false; + saved = JSON.parse(raw) as SavedShape; + } catch { + sessionStorage.removeItem(KEY); + return; } - }, [storageKey, NAVIGATION_FLAG_KEY]); - - // 스크롤 리셋 - const resetScroll = useCallback(() => { - window.scrollTo({ top: 0, behavior: 'smooth' }); - sessionStorage.removeItem(storageKey); - sessionStorage.removeItem(NAVIGATION_FLAG_KEY); - }, [storageKey, NAVIGATION_FLAG_KEY]); - - // 아이템 클릭 시 호출할 함수 (네비게이션 플래그 설정) - const handleItemClick = useCallback(() => { - if (!isRestoringRef.current && window.scrollY > 10) { - saveScrollState(); - - // 뒤로가기임을 표시하는 플래그 설정 - sessionStorage.setItem(NAVIGATION_FLAG_KEY, 'back'); + if (!saved) { + sessionStorage.removeItem(KEY); + return; } - }, [saveScrollState, NAVIGATION_FLAG_KEY]); - // 컴포넌트 마운트 시 복원 시도 - useEffect(() => { - if (hasMountedRef.current) return; - hasMountedRef.current = true; + const { targetId, scrollY } = saved; + isRestoringRef.current = true; - const restored = restoreScrollState(); + const MAX_FETCH = 50; - if (!restored) { - setShouldFetch(true); - } - }, [restoreScrollState]); + const restore = async () => { + let tries = 0; + let lastProgressLen = lenRef.current; + let lastProgressId = lastIdRef.current; - // 헤더에서 같은 페이지 클릭 시 이벤트 리스너 - useEffect(() => { - const handleResetScroll = () => { - resetScroll(); - }; + // 내림차순 전제: + // 더 불러올수록 현재 최소 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; // 안전망 - window.addEventListener(eventName, handleResetScroll); + await fetchData(); - return () => { - window.removeEventListener(eventName, handleResetScroll); - }; - }, [eventName, resetScroll]); + // 진행 없음(길이와 lastId 모두 동일) → 중단 + const noLenChange = lenRef.current === lastProgressLen; + const noIdChange = lastIdRef.current === lastProgressId; + if (noLenChange && noIdChange) break; - // 언마운트 시 정리 (복원되지 않은 데이터 정리) - useEffect(() => { - return () => { - // 네비게이션 플래그가 없으면 데이터도 삭제 - const navFlag = sessionStorage.getItem(NAVIGATION_FLAG_KEY); - if (!navFlag) { - sessionStorage.removeItem(storageKey); - } - }; - }, [storageKey, NAVIGATION_FLAG_KEY]); + lastProgressLen = lenRef.current; + lastProgressId = lastIdRef.current; - // 스크롤 이벤트 리스너 - useEffect(() => { - if (data.length === 0) return; - - const handleScroll = () => { - if (isRestoringRef.current) return; - - if (scrollRestoredRef.current) { - scrollRestoredRef.current = false; + // 다음 렌더로 넘겨 레이아웃 안정화 + await new Promise((r) => setTimeout(r, 0)); } - saveScrollState(); + requestAnimationFrame(() => jumpOnce(scrollY)); }; - // 디바운스 유틸을 이벤트마다 새로 생성시 timer초기화 => 로컬timeout사용 - let timeoutId: NodeJS.Timeout; - const debouncedHandleScroll = () => { - clearTimeout(timeoutId); - timeoutId = setTimeout(handleScroll, 100); - }; + restore(); + }, [KEY, fetchData, hasNextPage, jumpOnce]); - window.addEventListener('scroll', debouncedHandleScroll, { passive: true }); - - return () => { - clearTimeout(timeoutId); - window.removeEventListener('scroll', debouncedHandleScroll); + // 저장 + const saveScroll = useCallback(() => { + const payload: SavedShape = { + targetId: lastIdRef.current, + scrollY: window.scrollY, }; - }, [data, saveScrollState]); - - return { - data, - setData, - lastId, - setLastId, - hasNextPage, - setHasNextPage, - handleItemClick, - saveScrollState, - shouldFetch, - }; + sessionStorage.setItem(KEY, JSON.stringify(payload)); + sessionStorage.setItem('saveUrl', location.href); + }, [KEY]); + + return saveScroll; } diff --git a/src/shared/styles/global.css b/src/shared/styles/global.css index 5bc37f34..936c9ee2 100644 --- a/src/shared/styles/global.css +++ b/src/shared/styles/global.css @@ -14,3 +14,7 @@ color: #fff; background-color: var(--color-primary); } + +html { + scroll-behavior: auto; /* smooth가 아닌 auto로 */ +} \ No newline at end of file From 7e3f63861495705f229c04198a926ec8ecdd8071 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Sat, 11 Oct 2025 16:24:03 +0900 Subject: [PATCH 12/21] =?UTF-8?q?[feaet]=20=EC=83=81=EC=84=B8=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=82=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/pages/my-active/MyComment.tsx | 18 +++++++---- .../recipe/components/main/CocktailFilter.tsx | 2 +- src/domains/recipe/details/DetailMain.tsx | 30 +++++++++++++++++-- src/domains/recipe/details/DetailsHeader.tsx | 8 +++-- .../shared/components/comment/CommentList.tsx | 15 ++++++---- src/domains/shared/components/keep/Keep.tsx | 8 +++-- 6 files changed, 63 insertions(+), 18 deletions(-) diff --git a/src/domains/mypage/components/pages/my-active/MyComment.tsx b/src/domains/mypage/components/pages/my-active/MyComment.tsx index c52be8b8..82e07ce3 100644 --- a/src/domains/mypage/components/pages/my-active/MyComment.tsx +++ b/src/domains/mypage/components/pages/my-active/MyComment.tsx @@ -2,18 +2,26 @@ import { getApi } from '@/app/api/config/appConfig'; import { CommentType } from '@/domains/community/types/post'; import CommentList from '@/domains/shared/components/comment/CommentList'; +import { useAuthStore } from '@/domains/shared/store/auth'; import { useEffect, useState } from 'react'; +import { useShallow } from 'zustand/shallow'; function MyComment() { +const { user } = useAuthStore( + useShallow((state) => ({ + user: state.user, + })) +); + const [myComment, setMyComment] = useState([]); - const [isLoading, setIsLoading] = useState(false); + const [isLoading] = useState(false); + const fetchComment = async () => { const res = await fetch(`${getApi}/me/comments`, { method: 'GET', credentials: 'include', }); const json = await res.json(); - console.log(json); setMyComment(json.data.items); }; @@ -23,13 +31,13 @@ function MyComment() { return (
    - {/* {CommentList.length !== 0 ? ( - + {CommentList.length !== 0 ? ( + ) : (

    작성한 댓글이 없습니다.

    - )} */} + )}
    ); } diff --git a/src/domains/recipe/components/main/CocktailFilter.tsx b/src/domains/recipe/components/main/CocktailFilter.tsx index 5d2c4a0b..1186c535 100644 --- a/src/domains/recipe/components/main/CocktailFilter.tsx +++ b/src/domains/recipe/components/main/CocktailFilter.tsx @@ -4,7 +4,7 @@ function CocktailFilter({ cocktailsEA }: { cocktailsEA: string }) { return (

    {cocktailsEA}개

    - +
    ); } diff --git a/src/domains/recipe/details/DetailMain.tsx b/src/domains/recipe/details/DetailMain.tsx index 22880330..30a6fc53 100644 --- a/src/domains/recipe/details/DetailMain.tsx +++ b/src/domains/recipe/details/DetailMain.tsx @@ -11,23 +11,47 @@ import { Suspense, useEffect, useState } from 'react'; import SkeletonDetail from '../skeleton/SkeletonDetail'; import RecipeComment from '../components/details/RecipeComment'; import { getApi } from '@/app/api/config/appConfig'; +import { useAuthStore } from '@/domains/shared/store/auth'; + +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) { + const keepRes = await fetch(`${getApi}/me/bar`, { + method: 'GET', + credentials:'include' + }) + const keepjson = await keepRes.json() + if (!keepRes.ok) throw new Error('킵 한 아이템 호출 에러') + const keepIds = keepjson.data.map((a: Kept) => String(a.cocktailId)); + setIsKept(keepIds.includes(String(id))); + } else { + setIsKept(false) + } }; + + useEffect(() => { fetchData(); - window.scrollTo(0, 0); }, []); useEffect(() => { + window.scrollTo(0, 0); return () => { // 레시피 페이지로 돌아가지 않는 경우 (헤더 탭 클릭 등) // 네비게이션 플래그를 제거하여 스크롤 복원 방지 @@ -57,7 +81,7 @@ function DetailMain({ id }: { id: number }) { }>

    ${cocktailNameKo} 상세정보

    - +
    diff --git a/src/domains/recipe/details/DetailsHeader.tsx b/src/domains/recipe/details/DetailsHeader.tsx index 9cc2c372..e461a12a 100644 --- a/src/domains/recipe/details/DetailsHeader.tsx +++ b/src/domains/recipe/details/DetailsHeader.tsx @@ -12,18 +12,22 @@ interface Meta { url: string; } -function DetailsHeader({ id }: { id: number }) { +function DetailsHeader({ id, favor }: { id: number, favor: boolean | null }) { + const [isShare, setIsShare] = useState(false); const [meta, setMeta] = useState(null); + const url = async () => { const res = await fetch(`${getApi}/cocktails/${id}/share`); const json = await res.json(); setMeta(json.data); }; + useEffect(() => { url(); }, []); + return (
    {isShare && meta && ( @@ -38,7 +42,7 @@ function DetailsHeader({ id }: { id: number }) {
    setIsShare(true)} /> - +
    ); diff --git a/src/domains/shared/components/comment/CommentList.tsx b/src/domains/shared/components/comment/CommentList.tsx index f21bc736..9fc8c330 100644 --- a/src/domains/shared/components/comment/CommentList.tsx +++ b/src/domains/shared/components/comment/CommentList.tsx @@ -10,8 +10,8 @@ import { usePrevious } from 'react-use'; type Props = { comments: CommentType[] | null; currentUserNickname?: string; - onUpdateComment: (commentId: number, content: string) => Promise; - onDeleteComment: (commentId: number) => void; + onUpdateComment?: (commentId: number, content: string) => Promise; + onDeleteComment?: (commentId: number) => void; onLoadMore?: (lastCommentId: number) => void; // ← 무한스크롤 콜백 isEnd?: boolean; isLoading: boolean; @@ -27,6 +27,7 @@ function CommentList({ isEnd, myPage = false, }: Props) { + const parentRef = useRef(null); const [editCommentId, setEditCommentId] = useState(null); const [editedContentMap, setEditedContentMap] = useState>({}); @@ -73,7 +74,6 @@ function CommentList({ const { commentId, content, userNickName, createdAt } = comments[index]; const isEditing = editCommentId === commentId; const isMyComment = comments && currentUserNickname === userNickName; - const isLast = index === comments.length - 1; return ( @@ -105,7 +105,7 @@ function CommentList({ >
    { const updatedContent = editedContentMap[commentId]; if (!updatedContent) return; + if (!onUpdateComment) return onUpdateComment(commentId, updatedContent).then(() => { setEditCommentId(null); setEditedContentMap((prev) => { @@ -122,7 +123,11 @@ function CommentList({ }); }); }} - onDelete={() => onDeleteComment(commentId)} + + onDelete={() => { + if (!onDeleteComment) return + onDeleteComment(commentId) + }} onEdit={() => { setEditCommentId(commentId); setEditedContentMap((prev) => ({ diff --git a/src/domains/shared/components/keep/Keep.tsx b/src/domains/shared/components/keep/Keep.tsx index 16020939..63d88a49 100644 --- a/src/domains/shared/components/keep/Keep.tsx +++ b/src/domains/shared/components/keep/Keep.tsx @@ -2,7 +2,7 @@ import KeepIcon from '@/shared/assets/icons/keep_36.svg'; import KeepIconActive from '@/shared/assets/icons/keep_active_36.svg'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { deleteKeep, postKeep } from '../../api/keep/keep'; import { useToast } from '@/shared/hook/useToast'; import { useAuthStore } from '../../store/auth'; @@ -10,7 +10,7 @@ import { useAuthStore } from '../../store/auth'; interface Props { className?: string; cocktailId?: number; - favor?: boolean; + favor?: boolean | null } // ID는 커뮤니티 공유할때 id 타입보고 옵셔널 체크 풀어주세요! // 만약 타입 안맞는다면 그냥 두셔도 됩니다. @@ -20,6 +20,10 @@ function Keep({ className, cocktailId, favor }: Props) { const {toastInfo, toastSuccess } = useToast(); const [isClick, setIsClick] = useState(favor); + useEffect(() => { + setIsClick(favor) + },[favor]) + const handleClick = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); From 0a4742ecfbe6391fb6d19a0b2d4d45ca479b869a Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Sat, 11 Oct 2025 16:43:12 +0900 Subject: [PATCH 13/21] =?UTF-8?q?[feat]=EB=A0=88=EC=8B=9C=ED=94=BC?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recipe/components/main/CocktailFilter.tsx | 42 ++++++++++++++++++- .../recipe/components/main/Cocktails.tsx | 2 +- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/domains/recipe/components/main/CocktailFilter.tsx b/src/domains/recipe/components/main/CocktailFilter.tsx index 1186c535..aa86d61b 100644 --- a/src/domains/recipe/components/main/CocktailFilter.tsx +++ b/src/domains/recipe/components/main/CocktailFilter.tsx @@ -1,10 +1,48 @@ +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'; -function CocktailFilter({ cocktailsEA }: { cocktailsEA: string }) { +interface Props { + cocktailsEA: string; + setData: Dispatch>; +} + +function CocktailFilter({ cocktailsEA,setData }:Props) { + + const sortMap = { + 최신순: 'recent', + 인기순: 'popular', + 댓글순: 'comments', + } + const searchParams = useSearchParams(); + const query = searchParams.get('sortBy'); + 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) + } + }; + return (

    {cocktailsEA}개

    - + { + const sortValue = sortMap[value as keyof typeof sortMap]; + handleChange(value); + router.push(`?sortBy=${sortValue}`); + }} + />
    ); } diff --git a/src/domains/recipe/components/main/Cocktails.tsx b/src/domains/recipe/components/main/Cocktails.tsx index 111433ab..4d075d04 100644 --- a/src/domains/recipe/components/main/Cocktails.tsx +++ b/src/domains/recipe/components/main/Cocktails.tsx @@ -80,7 +80,7 @@ function Cocktails() {
    - +
    {isSearching && noResults ? ( From 0572d208f3cc35ed548b5c6acc0960c4a02620e1 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Sat, 11 Oct 2025 17:51:12 +0900 Subject: [PATCH 14/21] =?UTF-8?q?[feat]=20=EB=8C=93=EA=B8=80=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=EC=8B=9C=20=ED=95=B4=EB=8B=B9=20=EA=B8=80=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=B4=EB=82=B4=EC=A3=BC=EB=8A=94=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/mypage/my-active/my-like/page.tsx | 2 +- .../components/pages/my-active/MyComment.tsx | 17 ++- .../components/pages/my-active/MyLike.tsx | 10 +- src/domains/recipe/api/RecipeFetch.tsx | 38 ++++--- .../recipe/components/main/CocktailFilter.tsx | 41 ++++--- src/domains/recipe/details/DetailMain.tsx | 26 ++--- src/domains/recipe/details/DetailsHeader.tsx | 8 +- .../shared/components/comment/CommentList.tsx | 104 ++++++++++++++++-- src/domains/shared/components/keep/Keep.tsx | 16 +-- src/domains/shared/hook/useMemoScroll.ts | 2 +- src/shared/components/tool-tip/ToolTip.tsx | 2 +- 11 files changed, 185 insertions(+), 81 deletions(-) diff --git a/src/app/mypage/my-active/my-like/page.tsx b/src/app/mypage/my-active/my-like/page.tsx index 2544af73..8c3d624e 100644 --- a/src/app/mypage/my-active/my-like/page.tsx +++ b/src/app/mypage/my-active/my-like/page.tsx @@ -8,6 +8,6 @@ export const metadata: Metadata = { }; function Page() { - // return ; + return ; } export default Page; diff --git a/src/domains/mypage/components/pages/my-active/MyComment.tsx b/src/domains/mypage/components/pages/my-active/MyComment.tsx index 82e07ce3..daf1e549 100644 --- a/src/domains/mypage/components/pages/my-active/MyComment.tsx +++ b/src/domains/mypage/components/pages/my-active/MyComment.tsx @@ -7,11 +7,11 @@ import { useEffect, useState } from 'react'; import { useShallow } from 'zustand/shallow'; function MyComment() { -const { user } = useAuthStore( - useShallow((state) => ({ - user: state.user, - })) -); + const { user } = useAuthStore( + useShallow((state) => ({ + user: state.user, + })) + ); const [myComment, setMyComment] = useState([]); const [isLoading] = useState(false); @@ -32,7 +32,12 @@ const { user } = useAuthStore( return (
    {CommentList.length !== 0 ? ( - + ) : (

    작성한 댓글이 없습니다.

    diff --git a/src/domains/mypage/components/pages/my-active/MyLike.tsx b/src/domains/mypage/components/pages/my-active/MyLike.tsx index 598575ac..c8a3529e 100644 --- a/src/domains/mypage/components/pages/my-active/MyLike.tsx +++ b/src/domains/mypage/components/pages/my-active/MyLike.tsx @@ -20,6 +20,14 @@ function MyLike() { fetchLike(); }, []); - return ; + return ( +
    + {myLike.length > 0 ? ( + + ) : ( +
    아직 좋아요를 누른 글이 없습니다
    + )} +
    + ); } export default MyLike; diff --git a/src/domains/recipe/api/RecipeFetch.tsx b/src/domains/recipe/api/RecipeFetch.tsx index ac4a34b7..a86fecb5 100644 --- a/src/domains/recipe/api/RecipeFetch.tsx +++ b/src/domains/recipe/api/RecipeFetch.tsx @@ -23,8 +23,7 @@ export const RecipeFetch = ({ setHasNextPage, SIZE = 20, }: Props) => { - - const user = useAuthStore() + const user = useAuthStore(); const fetchData = useCallback(async () => { // 쿼리파라미터에 값 넣기 if (!hasNextPage) return; @@ -35,32 +34,41 @@ export const RecipeFetch = ({ } 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 ?? [] + 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 ?? [] : [] + 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())) + 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())) + 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/CocktailFilter.tsx b/src/domains/recipe/components/main/CocktailFilter.tsx index aa86d61b..1941e240 100644 --- a/src/domains/recipe/components/main/CocktailFilter.tsx +++ b/src/domains/recipe/components/main/CocktailFilter.tsx @@ -9,28 +9,27 @@ interface Props { setData: Dispatch>; } -function CocktailFilter({ cocktailsEA,setData }:Props) { +function CocktailFilter({ cocktailsEA, setData }: Props) { + const sortMap = { + 최신순: 'recent', + 인기순: 'popular', + 댓글순: 'comments', + }; + const searchParams = useSearchParams(); + const query = searchParams.get('sortBy'); + 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 sortMap = { - 최신순: 'recent', - 인기순: 'popular', - 댓글순: 'comments', - } - const searchParams = useSearchParams(); - const query = searchParams.get('sortBy'); - 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) - } - }; - return (

    {cocktailsEA}개

    diff --git a/src/domains/recipe/details/DetailMain.tsx b/src/domains/recipe/details/DetailMain.tsx index 30a6fc53..7f4fc4c8 100644 --- a/src/domains/recipe/details/DetailMain.tsx +++ b/src/domains/recipe/details/DetailMain.tsx @@ -13,17 +13,17 @@ import RecipeComment from '../components/details/RecipeComment'; import { getApi } from '@/app/api/config/appConfig'; import { useAuthStore } from '@/domains/shared/store/auth'; -interface Kept{ - cocktailId: number, - id: number, - keptAt:Date +interface Kept { + cocktailId: number; + id: number; + keptAt: Date; } function DetailMain({ id }: { id: number }) { - const user = useAuthStore() + const user = useAuthStore(); const [cocktail, setCocktail] = useState(); - const [isKept, setIsKept] = useState(null) - + const [isKept, setIsKept] = useState(null); + const fetchData = async () => { const res = await fetch(`${getApi}/cocktails/${id}`); const json = await res.json(); @@ -33,19 +33,17 @@ function DetailMain({ id }: { id: number }) { if (user) { const keepRes = await fetch(`${getApi}/me/bar`, { method: 'GET', - credentials:'include' - }) - const keepjson = await keepRes.json() - if (!keepRes.ok) throw new Error('킵 한 아이템 호출 에러') + credentials: 'include', + }); + const keepjson = await keepRes.json(); + if (!keepRes.ok) throw new Error('킵 한 아이템 호출 에러'); const keepIds = keepjson.data.map((a: Kept) => String(a.cocktailId)); setIsKept(keepIds.includes(String(id))); } else { - setIsKept(false) + setIsKept(false); } }; - - useEffect(() => { fetchData(); }, []); diff --git a/src/domains/recipe/details/DetailsHeader.tsx b/src/domains/recipe/details/DetailsHeader.tsx index e461a12a..1e11bd91 100644 --- a/src/domains/recipe/details/DetailsHeader.tsx +++ b/src/domains/recipe/details/DetailsHeader.tsx @@ -12,22 +12,20 @@ interface Meta { url: string; } -function DetailsHeader({ id, favor }: { id: number, favor: boolean | null }) { - +function DetailsHeader({ id, favor }: { id: number; favor: boolean | null }) { const [isShare, setIsShare] = useState(false); const [meta, setMeta] = useState(null); - + const url = async () => { const res = await fetch(`${getApi}/cocktails/${id}/share`); const json = await res.json(); setMeta(json.data); }; - + useEffect(() => { url(); }, []); - return (
    {isShare && meta && ( diff --git a/src/domains/shared/components/comment/CommentList.tsx b/src/domains/shared/components/comment/CommentList.tsx index 9fc8c330..2a371f36 100644 --- a/src/domains/shared/components/comment/CommentList.tsx +++ b/src/domains/shared/components/comment/CommentList.tsx @@ -6,6 +6,8 @@ import { useInfiniteScrollObserver } from '@/shared/hook/useInfiniteScrollObserv import { useItemVirtualizer } from '@/domains/community/hook/useItemVirtualizer'; import { useCommentEnterAnimation } from '@/domains/community/hook/useCommentAnimation'; import { usePrevious } from 'react-use'; +import Link from 'next/link'; +import { getApi } from '@/app/api/config/appConfig'; type Props = { comments: CommentType[] | null; @@ -27,7 +29,6 @@ function CommentList({ isEnd, myPage = false, }: Props) { - const parentRef = useRef(null); const [editCommentId, setEditCommentId] = useState(null); const [editedContentMap, setEditedContentMap] = useState>({}); @@ -71,12 +72,100 @@ function CommentList({ >
      {rowVirtualizer.getVirtualItems().map(({ index, key, start }) => { - const { commentId, content, userNickName, createdAt } = comments[index]; + const { commentId, content, userNickName, createdAt, postId } = comments[index]; const isEditing = editCommentId === commentId; const isMyComment = comments && currentUserNickname === userNickName; const isLast = index === comments.length - 1; - return ( + return myPage ? ( + +
    • { + if (el) { + requestAnimationFrame(() => { + try { + rowVirtualizer.measureElement(el); + } catch (e) { + console.error('measureElement failed', e); + } + }); + if (index === 0) firstItemRef.current = el; + if (isLast) observeLastItem(el); + } + }} + style={{ + position: 'absolute', + top: 0, + left: 0, + width: '100%', + transform: `translateY(${start}px)`, + minHeight: '60px', // ← 최소 보장 + }} + > +
      + { + const updatedContent = editedContentMap[commentId]; + if (!updatedContent) return; + if (!onUpdateComment) return; + onUpdateComment(commentId, updatedContent).then(() => { + setEditCommentId(null); + setEditedContentMap((prev) => { + const next = { ...prev }; + delete next[commentId]; + return next; + }); + }); + }} + onDelete={() => { + if (!onDeleteComment) return; + onDeleteComment(commentId); + }} + onEdit={() => { + setEditCommentId(commentId); + setEditedContentMap((prev) => ({ + ...prev, + [commentId]: content, // 기존 내용 세팅 + })); + }} + onCancelEdit={() => { + setEditCommentId(null); + setEditedContentMap((prev) => { + const next = { ...prev }; + delete next[commentId]; + return next; + }); + }} + /> +
      + {isEditing ? ( + + setEditedContentMap((prev) => ({ + ...prev, + [commentId]: e.target.value, + })) + } + /> + ) : ( +
      +

      {content}

      +
      + )} +
      +
      +
    • + + ) : (
    • { const updatedContent = editedContentMap[commentId]; if (!updatedContent) return; - if (!onUpdateComment) return + if (!onUpdateComment) return; onUpdateComment(commentId, updatedContent).then(() => { setEditCommentId(null); setEditedContentMap((prev) => { @@ -123,10 +212,9 @@ function CommentList({ }); }); }} - onDelete={() => { - if (!onDeleteComment) return - onDeleteComment(commentId) + if (!onDeleteComment) return; + onDeleteComment(commentId); }} onEdit={() => { setEditCommentId(commentId); diff --git a/src/domains/shared/components/keep/Keep.tsx b/src/domains/shared/components/keep/Keep.tsx index 63d88a49..b308e75d 100644 --- a/src/domains/shared/components/keep/Keep.tsx +++ b/src/domains/shared/components/keep/Keep.tsx @@ -10,26 +10,26 @@ import { useAuthStore } from '../../store/auth'; interface Props { className?: string; cocktailId?: number; - favor?: boolean | null + favor?: boolean | null; } // ID는 커뮤니티 공유할때 id 타입보고 옵셔널 체크 풀어주세요! // 만약 타입 안맞는다면 그냥 두셔도 됩니다. function Keep({ className, cocktailId, favor }: Props) { - const user = useAuthStore() - const {toastInfo, toastSuccess } = useToast(); + const user = useAuthStore(); + const { toastInfo, toastSuccess } = useToast(); const [isClick, setIsClick] = useState(favor); useEffect(() => { - setIsClick(favor) - },[favor]) - + setIsClick(favor); + }, [favor]); + const handleClick = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!user) { - toastInfo('로그인 후 이용 가능합니다.') - return + toastInfo('로그인 후 이용 가능합니다.'); + return; } setIsClick(!isClick); diff --git a/src/domains/shared/hook/useMemoScroll.ts b/src/domains/shared/hook/useMemoScroll.ts index c519acfd..87bd13fd 100644 --- a/src/domains/shared/hook/useMemoScroll.ts +++ b/src/domains/shared/hook/useMemoScroll.ts @@ -75,7 +75,7 @@ export function useScrollRestore({ }, { once: true } ); - setTimeout(() => finish(), 1000); + setTimeout(() => finish(), 1000); }, [KEY] ); diff --git a/src/shared/components/tool-tip/ToolTip.tsx b/src/shared/components/tool-tip/ToolTip.tsx index 12e885c9..9a703a20 100644 --- a/src/shared/components/tool-tip/ToolTip.tsx +++ b/src/shared/components/tool-tip/ToolTip.tsx @@ -89,7 +89,7 @@ function ToolTip({ position, message, viewPoint = 'web', children, ref }: Props) aria-expanded={isVisible} ref={ref} onMouseLeave={() => setIsVisible(false)} - onClick={handleClick} + onMouseEnter={handleClick} > {children} {isVisible && ( From fd29597dced59c92809d3176d1da01d4f1c41f5a Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Sat, 11 Oct 2025 18:02:44 +0900 Subject: [PATCH 15/21] =?UTF-8?q?[feat]=EB=8C=93=EA=B8=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=ED=86=A0=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/recipe/api/fetchRecipeComment.ts | 19 +------------- src/domains/recipe/api/useRecipeComment.ts | 9 ++++--- .../components/details/RecipeComment.tsx | 25 ++++++++++++++++++- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/domains/recipe/api/fetchRecipeComment.ts b/src/domains/recipe/api/fetchRecipeComment.ts index c0bc5cef..627001ee 100644 --- a/src/domains/recipe/api/fetchRecipeComment.ts +++ b/src/domains/recipe/api/fetchRecipeComment.ts @@ -1,23 +1,6 @@ import { getApi } from '@/app/api/config/appConfig'; import { CommentType } from '@/domains/community/types/post'; -export const postRecipeComment = async (cocktailId: number, content: string) => { - try { - const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', - body: JSON.stringify({ content }), - }); - const text = await res.text(); - if (!res.ok) throw new Error('댓글 작성 실패'); - const data = JSON.parse(text); - return data; - } catch (err) { - console.error(err); - } -}; - export const getRecipeComment = async (cocktailId: number): Promise => { try { const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments`, { @@ -74,7 +57,7 @@ export async function deleteRecipeComment( }); if (!response.ok) { - const errorText = await response.text(); // 👈 응답 본문을 텍스트로 읽기 + 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 9fdd8e88..d5aa6f6d 100644 --- a/src/domains/recipe/api/useRecipeComment.ts +++ b/src/domains/recipe/api/useRecipeComment.ts @@ -3,6 +3,7 @@ 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 { useToast } from '@/shared/hook/useToast'; export function useRecipeComments( cocktailId: number, @@ -16,6 +17,7 @@ export function useRecipeComments( commentId: number; cocktailId: number; } | null>(null); + const { toastError } = useToast(); const fetchData = useCallback(async () => { const data = await getRecipeComment(cocktailId); @@ -30,7 +32,7 @@ export function useRecipeComments( const handleUpdateComment = async (commentId: number, content: string) => { if (!user) { - alert('로그인이 필요합니다'); + toastError('로그인이 필요합니다'); return; } try { @@ -44,7 +46,7 @@ export function useRecipeComments( ); } catch (err) { console.error(err); - alert('댓글 수정 중 오류가 발생했습니다.'); + toastError('댓글 수정 중 오류가 발생했습니다.'); } }; @@ -54,7 +56,7 @@ export function useRecipeComments( const handleConfirmDelete = async () => { if (!user) { - alert('로그인이 필요합니다'); + toastError('로그인이 필요합니다'); return; } if (!deleteTarget) return; @@ -66,7 +68,6 @@ export function useRecipeComments( ); } catch (err) { console.error(err); - alert('댓글 삭제 중 오류가 발생했습니다.'); } finally { setDeleteTarget(null); } diff --git a/src/domains/recipe/components/details/RecipeComment.tsx b/src/domains/recipe/components/details/RecipeComment.tsx index d6d50146..a90db218 100644 --- a/src/domains/recipe/components/details/RecipeComment.tsx +++ b/src/domains/recipe/components/details/RecipeComment.tsx @@ -1,10 +1,11 @@ import CommentHeader from '@/domains/shared/components/comment/CommentHeader'; import CommentList from '@/domains/shared/components/comment/CommentList'; -import { postRecipeComment } from '../../api/fetchRecipeComment'; 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'; interface Props { cocktailId: number; @@ -17,6 +18,28 @@ function RecipeComment({ cocktailId }: Props) { accessToken: state.accessToken, })) ); + const { toastInfo } = useToast(); + + const postRecipeComment = async (cocktailId: number, content: string) => { + try { + const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ content }), + }); + const text = await res.text(); + if (!res.ok) { + toastInfo('댓글은 한 개만 작성가능합니다'); + return; + } + const data = JSON.parse(text); + return data; + } catch (err) { + console.error(err); + } + }; + const { comments, fetchData, From 8b0a72edfa5deb17963a4fb53bd7b5d9eaee4487 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Sat, 11 Oct 2025 18:26:44 +0900 Subject: [PATCH 16/21] =?UTF-8?q?[feat]=EB=8C=93=EA=B8=80=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/shared/components/comment/CommentHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domains/shared/components/comment/CommentHeader.tsx b/src/domains/shared/components/comment/CommentHeader.tsx index 08158041..cef9da6d 100644 --- a/src/domains/shared/components/comment/CommentHeader.tsx +++ b/src/domains/shared/components/comment/CommentHeader.tsx @@ -60,7 +60,7 @@ function CommentHeader({ intervalCall1000(async () => { const success = await createComment(newComment); if (!success) { - console.error('엔터키로 댓글 작성 실패'); + console.log('칵테일 페이지에서 댓글은 한개만 입력 가능합니다') } }); } From 9acd060c2aa32cc9657855c99c61bd42b8942849 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Sat, 11 Oct 2025 18:27:00 +0900 Subject: [PATCH 17/21] =?UTF-8?q?[feat]=EB=8C=93=EA=B8=80=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/shared/components/comment/CommentHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domains/shared/components/comment/CommentHeader.tsx b/src/domains/shared/components/comment/CommentHeader.tsx index cef9da6d..fc00ada0 100644 --- a/src/domains/shared/components/comment/CommentHeader.tsx +++ b/src/domains/shared/components/comment/CommentHeader.tsx @@ -60,7 +60,7 @@ function CommentHeader({ intervalCall1000(async () => { const success = await createComment(newComment); if (!success) { - console.log('칵테일 페이지에서 댓글은 한개만 입력 가능합니다') + console.log('칵테일 페이지에서 댓글은 한개만 입력 가능합니다'); } }); } From 733ce4f32776991e631e4f03b02f62fa72af4111 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Sat, 11 Oct 2025 20:43:58 +0900 Subject: [PATCH 18/21] =?UTF-8?q?[fix]=20=EB=8C=93=EA=B8=80,=20=ED=82=B5?= =?UTF-8?q?=20=EC=95=84=EC=9D=B4=ED=85=9C=20=EB=B9=84=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B2=BD=EA=B3=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recipe/components/details/RecipeComment.tsx | 13 +++++++++++-- src/domains/recipe/details/DetailMain.tsx | 8 ++++---- src/domains/shared/components/keep/Keep.tsx | 3 ++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/domains/recipe/components/details/RecipeComment.tsx b/src/domains/recipe/components/details/RecipeComment.tsx index a90db218..0b4ce84e 100644 --- a/src/domains/recipe/components/details/RecipeComment.tsx +++ b/src/domains/recipe/components/details/RecipeComment.tsx @@ -18,9 +18,16 @@ function RecipeComment({ cocktailId }: Props) { accessToken: state.accessToken, })) ); + const { toastInfo } = useToast(); const postRecipeComment = async (cocktailId: number, content: string) => { + + if (!user?.id) { + toastInfo('로그인 후 이용 가능합니다'); + return + } + try { const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments`, { method: 'POST', @@ -29,10 +36,12 @@ function RecipeComment({ cocktailId }: Props) { body: JSON.stringify({ content }), }); const text = await res.text(); - if (!res.ok) { + + if (!res.ok) { toastInfo('댓글은 한 개만 작성가능합니다'); - return; + return } + const data = JSON.parse(text); return data; } catch (err) { diff --git a/src/domains/recipe/details/DetailMain.tsx b/src/domains/recipe/details/DetailMain.tsx index 7f4fc4c8..468be48d 100644 --- a/src/domains/recipe/details/DetailMain.tsx +++ b/src/domains/recipe/details/DetailMain.tsx @@ -30,17 +30,17 @@ function DetailMain({ id }: { id: number }) { if (!res.ok) throw new Error('데이터 요청 실패'); setCocktail(json.data); - if (user) { + if (!user) { + setIsKept(false); + return + } else { const keepRes = await fetch(`${getApi}/me/bar`, { method: 'GET', credentials: 'include', }); const keepjson = await keepRes.json(); - if (!keepRes.ok) throw new Error('킵 한 아이템 호출 에러'); const keepIds = keepjson.data.map((a: Kept) => String(a.cocktailId)); setIsKept(keepIds.includes(String(id))); - } else { - setIsKept(false); } }; diff --git a/src/domains/shared/components/keep/Keep.tsx b/src/domains/shared/components/keep/Keep.tsx index b308e75d..aa3b8811 100644 --- a/src/domains/shared/components/keep/Keep.tsx +++ b/src/domains/shared/components/keep/Keep.tsx @@ -16,7 +16,7 @@ interface Props { // 만약 타입 안맞는다면 그냥 두셔도 됩니다. function Keep({ className, cocktailId, favor }: Props) { - const user = useAuthStore(); + const { user } = useAuthStore(); const { toastInfo, toastSuccess } = useToast(); const [isClick, setIsClick] = useState(favor); @@ -27,6 +27,7 @@ function Keep({ className, cocktailId, favor }: Props) { const handleClick = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); + if (!user) { toastInfo('로그인 후 이용 가능합니다.'); return; From 3ae278d12fab9ea2439d485db8c9e01b05f45902 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Sat, 11 Oct 2025 20:46:05 +0900 Subject: [PATCH 19/21] =?UTF-8?q?[fix]=20mypage=20401=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mypage/components/pages/my-active/MyLike.tsx | 2 +- .../mypage/components/pages/my-active/MyPost.tsx | 1 + .../recipe/components/details/RecipeComment.tsx | 13 ++++++------- src/domains/recipe/details/DetailMain.tsx | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/domains/mypage/components/pages/my-active/MyLike.tsx b/src/domains/mypage/components/pages/my-active/MyLike.tsx index c8a3529e..5f283250 100644 --- a/src/domains/mypage/components/pages/my-active/MyLike.tsx +++ b/src/domains/mypage/components/pages/my-active/MyLike.tsx @@ -8,7 +8,7 @@ function MyLike() { const [isLoading, setIsLoading] = useState(false); const fetchLike = async () => { - const res = await fetch(`${getApi}/me/likes/posts`, { + const res = await fetch(`${getApi}/me/likes`, { method: 'GET', credentials: 'include', }); diff --git a/src/domains/mypage/components/pages/my-active/MyPost.tsx b/src/domains/mypage/components/pages/my-active/MyPost.tsx index 24515b32..c17c6dc8 100644 --- a/src/domains/mypage/components/pages/my-active/MyPost.tsx +++ b/src/domains/mypage/components/pages/my-active/MyPost.tsx @@ -12,6 +12,7 @@ function MyPost() { credentials: 'include', }); const json = await res.json(); + console.log(json); setMyPost(json.data.items); }; diff --git a/src/domains/recipe/components/details/RecipeComment.tsx b/src/domains/recipe/components/details/RecipeComment.tsx index 0b4ce84e..2678fcd1 100644 --- a/src/domains/recipe/components/details/RecipeComment.tsx +++ b/src/domains/recipe/components/details/RecipeComment.tsx @@ -22,12 +22,11 @@ function RecipeComment({ cocktailId }: Props) { const { toastInfo } = useToast(); const postRecipeComment = async (cocktailId: number, content: string) => { - if (!user?.id) { - toastInfo('로그인 후 이용 가능합니다'); - return + toastInfo('로그인 후 이용 가능합니다'); + return; } - + try { const res = await fetch(`${getApi}/cocktails/${cocktailId}/comments`, { method: 'POST', @@ -36,10 +35,10 @@ function RecipeComment({ cocktailId }: Props) { body: JSON.stringify({ content }), }); const text = await res.text(); - - if (!res.ok) { + + if (!res.ok) { toastInfo('댓글은 한 개만 작성가능합니다'); - return + return; } const data = JSON.parse(text); diff --git a/src/domains/recipe/details/DetailMain.tsx b/src/domains/recipe/details/DetailMain.tsx index 468be48d..de057e02 100644 --- a/src/domains/recipe/details/DetailMain.tsx +++ b/src/domains/recipe/details/DetailMain.tsx @@ -32,7 +32,7 @@ function DetailMain({ id }: { id: number }) { if (!user) { setIsKept(false); - return + return; } else { const keepRes = await fetch(`${getApi}/me/bar`, { method: 'GET', From d025835411a0a39faa44d6db66f8c515723098a3 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Sat, 11 Oct 2025 22:12:51 +0900 Subject: [PATCH 20/21] =?UTF-8?q?[feat]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=8F=99=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 27 ++++++++++ package.json | 1 + src/app/layout.tsx | 36 +++++++------ src/app/mypage/layout.tsx | 15 +++--- src/app/mypage/my-setting/page.tsx | 2 +- src/domains/mypage/api/fetchProfile.ts | 51 +++++++++++-------- .../mypage/components/EditNickName.tsx | 32 +++++------- src/domains/mypage/main/MyProfile.tsx | 14 +++-- src/domains/mypage/main/MySetting.tsx | 5 +- src/domains/mypage/types/type.d.ts | 12 +++++ src/shared/api/Provider.tsx | 13 +++++ 11 files changed, 136 insertions(+), 72 deletions(-) create mode 100644 src/domains/mypage/types/type.d.ts create mode 100644 src/shared/api/Provider.tsx diff --git a/package-lock.json b/package-lock.json index 497c4796..0826c9fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "license": "ISC", "dependencies": { + "@tanstack/react-query": "^5.90.2", "@tanstack/react-virtual": "^3.13.12", "class-variance-authority": "^0.7.1", "gsap": "^3.13.0", @@ -3299,6 +3300,32 @@ "tailwindcss": "4.1.13" } }, + "node_modules/@tanstack/query-core": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz", + "integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.2.tgz", + "integrity": "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@tanstack/react-virtual": { "version": "3.13.12", "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", diff --git a/package.json b/package.json index 30cba0cb..1ce98419 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ ] }, "dependencies": { + "@tanstack/react-query": "^5.90.2", "@tanstack/react-virtual": "^3.13.12", "class-variance-authority": "^0.7.1", "gsap": "^3.13.0", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c5f134dd..a109e01d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,6 +6,8 @@ import FooterWrapper from '@/shared/components/footer/FooterWrapper'; import ScrollTopBtnWrapper from '@/shared/components/scroll-top/ScrollTopBtnWrapper'; import KaKaoScript from './api/kakao/KaKaoScript'; import IdleHandler from '@/domains/login/components/IdleHandler'; +import Provider from '@/shared/api/Provider'; + export const metadata: Metadata = { title: { default: 'SSOUL', template: 'SSOUL | %s' }, @@ -21,24 +23,26 @@ export default function RootLayout({ return ( -
      - -
      {children}
      - + +
      + +
      {children}
      + - - + + - + + diff --git a/src/app/mypage/layout.tsx b/src/app/mypage/layout.tsx index 92109875..fedb3d7a 100644 --- a/src/app/mypage/layout.tsx +++ b/src/app/mypage/layout.tsx @@ -5,13 +5,14 @@ import { Suspense } from 'react'; function Layout({ children }: { children: React.ReactNode }) { return ( - }> -
      - - -
      {children}
      -
      -
      + + }> +
      + + +
      {children}
      +
      +
      ); } export default Layout; diff --git a/src/app/mypage/my-setting/page.tsx b/src/app/mypage/my-setting/page.tsx index b7e3f792..2cb147bd 100644 --- a/src/app/mypage/my-setting/page.tsx +++ b/src/app/mypage/my-setting/page.tsx @@ -2,7 +2,7 @@ import MySetting from '@/domains/mypage/main/MySetting'; import { Metadata } from 'next'; export const metadata: Metadata = { - title: 'SSOUL | 마이페이지', + title: '마이페이지', description: 'SSOUL 서비스에서 나의 활동을 관리할 수 있는 페이지입니다', }; diff --git a/src/domains/mypage/api/fetchProfile.ts b/src/domains/mypage/api/fetchProfile.ts index 6dbc620f..ff230c7a 100644 --- a/src/domains/mypage/api/fetchProfile.ts +++ b/src/domains/mypage/api/fetchProfile.ts @@ -1,22 +1,11 @@ 'use client'; import { getApi } from '@/app/api/config/appConfig'; -import { useEffect, useState } from 'react'; - -interface Profile { - abvDegree: number; - abvLabel: string; - abvLevel: number; - email?: string; - id: number; - myCommentCount: number; - myKeepCount: number; - myLikedPostCount: number; - myPostCount: number; - nickname: string; -} +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { Profile } from '../types/type'; function useFetchProfile() { - const [profile, setProfile] = useState(null); + + const queryClient = useQueryClient() const fetchProfile = async () => { const res = await fetch(`${getApi}/me/profile`, { @@ -24,13 +13,35 @@ function useFetchProfile() { credentials: 'include', }); const json = await res.json(); - setProfile(json.data); + + return json.data }; - useEffect(() => { - fetchProfile(); - }, []); + const patchNickName = useMutation({ + mutationFn: async (nickname: string) => { + const res = await fetch(`${getApi}/me/profile`, { + method: 'PATCH', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({nickname}) + }) + if (!res.ok) throw new Error('닉네임 수정 실패') + const json = await res.json() + return json.data + }, + + onMutate: async (nickname) => { + // 같은 키로 요청중인 fetch 중단 + await queryClient.cancelQueries({ queryKey: ['myProfile'] }) + // 캐시에 저장된 데이터를 즉시 가져오는 역할 실패시 prev로 롤백 + const prev = queryClient.getQueryData(['myProfile']) + // 캐시 내용을 수정 + queryClient.setQueryData(['myProfile'], (old:Profile) => ({ ...old, nickname })) + return {prev} + } + }) + const profile = useQuery({queryKey:['myProfile'], queryFn:fetchProfile}) - return { profile, fetchProfile }; +return{fetchProfile , profile, patchNickName} } export default useFetchProfile; diff --git a/src/domains/mypage/components/EditNickName.tsx b/src/domains/mypage/components/EditNickName.tsx index 05a7c419..4a2a989e 100644 --- a/src/domains/mypage/components/EditNickName.tsx +++ b/src/domains/mypage/components/EditNickName.tsx @@ -5,6 +5,7 @@ import Input from '@/shared/components/Input-box/Input'; import ModalLayout from '@/shared/components/modal-pop/ModalLayout'; import { useToast } from '@/shared/hook/useToast'; import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import useFetchProfile from '../api/fetchProfile'; interface Props { open: boolean; @@ -27,33 +28,26 @@ function EditNickName({ }: Props) { const [defaultNickname, setDefaultNickname] = useState(nickname); const { toastSuccess, toastError } = useToast(); - + const { patchNickName } = useFetchProfile() + useEffect(() => { setEditNickName(nickname); setDefaultNickname(nickname); }, [nickname, setEditNickName]); const handlesave = async () => { - if (editNickName.length <= 1 || editNickName.length >= 8) { - toastError('닉네임은 2글자 이상 입력해야합니다'); - return; - } + if (editNickName.length <= 1 || editNickName.length >= 8) { + toastError('닉네임은 2글자 이상 8글자 이내로 입력해야합니다'); + return; + } - await setNickName(editNickName); + await setNickName(editNickName); + // CRUD중 CUD를 관리하는 메서드 + await patchNickName.mutateAsync(editNickName) - await fetch(`${getApi}/me/profile`, { - method: 'PATCH', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - nickname: editNickName, - }), - }); - await setIsOpen(false); - toastSuccess('닉네임이 저장되었습니다.'); - }; + await setIsOpen(false); + toastSuccess('닉네임이 저장되었습니다.'); + } const handleChange = (e: React.ChangeEvent) => { setEditNickName(e.target.value); diff --git a/src/domains/mypage/main/MyProfile.tsx b/src/domains/mypage/main/MyProfile.tsx index b3206ecb..d891353b 100644 --- a/src/domains/mypage/main/MyProfile.tsx +++ b/src/domains/mypage/main/MyProfile.tsx @@ -4,15 +4,13 @@ import AbvGraph from '@/domains/shared/components/abv-graph/AbvGraph'; import MyAbv from './MyAbv'; import SsuryImage from './SsuryImage'; import useFetchProfile from '../api/fetchProfile'; -import { useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; function MyProfile() { - const { profile, fetchProfile } = useFetchProfile(); - useEffect(() => { - fetchProfile(); - }, [profile?.nickname]); + const { fetchProfile } = useFetchProfile(); + const {data} = useQuery({ queryKey: ['myProfile'], queryFn: fetchProfile }); - if (!profile) return; + if (!data) return; const { nickname, abvLevel, @@ -21,12 +19,12 @@ function MyProfile() { myCommentCount, myKeepCount, abvDegree, - } = profile; + } = data return (
      - {profile && ( + {data && ( <>
      diff --git a/src/domains/mypage/main/MySetting.tsx b/src/domains/mypage/main/MySetting.tsx index f5d01c93..e724fa4b 100644 --- a/src/domains/mypage/main/MySetting.tsx +++ b/src/domains/mypage/main/MySetting.tsx @@ -5,9 +5,12 @@ import WithdrawModal from '@/domains/mypage/components/WithdrawModal'; import TextButton from '@/shared/components/button/TextButton'; import { useEffect, useState } from 'react'; import useFetchProfile from '../api/fetchProfile'; +import { useQuery } from '@tanstack/react-query'; + function MySetting() { - const { profile } = useFetchProfile(); + const { fetchProfile } = useFetchProfile(); + const { data:profile } = useQuery({ queryKey: ['myProfile'], queryFn: fetchProfile }); const [isOpen, setIsOpen] = useState(false); const [isQuit, setIsQuit] = useState(false); const [nickname, setNickName] = useState(profile?.nickname); diff --git a/src/domains/mypage/types/type.d.ts b/src/domains/mypage/types/type.d.ts new file mode 100644 index 00000000..41ccee95 --- /dev/null +++ b/src/domains/mypage/types/type.d.ts @@ -0,0 +1,12 @@ +export interface Profile { + abvDegree: number; + abvLabel: string; + abvLevel: number; + email: string | null; + id: number; + myCommentCount: number + myKeepCount: number + myLikedPostCount: number; + myPostCount: number; + nickname: string; +} \ No newline at end of file diff --git a/src/shared/api/Provider.tsx b/src/shared/api/Provider.tsx new file mode 100644 index 00000000..69325022 --- /dev/null +++ b/src/shared/api/Provider.tsx @@ -0,0 +1,13 @@ + +'use client'; +import { useState } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +export default function Providers({ children }: { children: React.ReactNode }) { + const [client] = useState(() => new QueryClient()); + return ( + + {children} + + ); +} From a3ef49b36e66ad856c02313ec684ba2c524b62c1 Mon Sep 17 00:00:00 2001 From: mtm1018 Date: Sun, 12 Oct 2025 01:34:40 +0900 Subject: [PATCH 21/21] =?UTF-8?q?[chore]=ED=8F=AC=EB=A7=A4=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/layout.tsx | 1 - src/app/mypage/layout.tsx | 15 ++++----- src/domains/mypage/api/fetchProfile.ts | 33 +++++++++---------- .../mypage/components/EditNickName.tsx | 24 +++++++------- src/domains/mypage/main/MyProfile.tsx | 4 +-- src/domains/mypage/main/MySetting.tsx | 3 +- src/domains/mypage/types/type.d.ts | 6 ++-- src/shared/api/Provider.tsx | 7 +--- 8 files changed, 42 insertions(+), 51 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a109e01d..cdf6dfee 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,7 +8,6 @@ import KaKaoScript from './api/kakao/KaKaoScript'; import IdleHandler from '@/domains/login/components/IdleHandler'; import Provider from '@/shared/api/Provider'; - export const metadata: Metadata = { title: { default: 'SSOUL', template: 'SSOUL | %s' }, metadataBase: new URL('http://www.ssoul.life'), diff --git a/src/app/mypage/layout.tsx b/src/app/mypage/layout.tsx index fedb3d7a..92109875 100644 --- a/src/app/mypage/layout.tsx +++ b/src/app/mypage/layout.tsx @@ -5,14 +5,13 @@ import { Suspense } from 'react'; function Layout({ children }: { children: React.ReactNode }) { return ( - - }> -
      - - -
      {children}
      -
      -
      + }> +
      + + +
      {children}
      +
      +
      ); } export default Layout; diff --git a/src/domains/mypage/api/fetchProfile.ts b/src/domains/mypage/api/fetchProfile.ts index ff230c7a..b8ffd4c5 100644 --- a/src/domains/mypage/api/fetchProfile.ts +++ b/src/domains/mypage/api/fetchProfile.ts @@ -4,8 +4,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Profile } from '../types/type'; function useFetchProfile() { - - const queryClient = useQueryClient() + const queryClient = useQueryClient(); const fetchProfile = async () => { const res = await fetch(`${getApi}/me/profile`, { @@ -13,8 +12,8 @@ function useFetchProfile() { credentials: 'include', }); const json = await res.json(); - - return json.data + + return json.data; }; const patchNickName = useMutation({ @@ -23,25 +22,25 @@ function useFetchProfile() { method: 'PATCH', credentials: 'include', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({nickname}) - }) - if (!res.ok) throw new Error('닉네임 수정 실패') - const json = await res.json() - return json.data + body: JSON.stringify({ nickname }), + }); + if (!res.ok) throw new Error('닉네임 수정 실패'); + const json = await res.json(); + return json.data; }, onMutate: async (nickname) => { // 같은 키로 요청중인 fetch 중단 - await queryClient.cancelQueries({ queryKey: ['myProfile'] }) + await queryClient.cancelQueries({ queryKey: ['myProfile'] }); // 캐시에 저장된 데이터를 즉시 가져오는 역할 실패시 prev로 롤백 - const prev = queryClient.getQueryData(['myProfile']) + const prev = queryClient.getQueryData(['myProfile']); // 캐시 내용을 수정 - queryClient.setQueryData(['myProfile'], (old:Profile) => ({ ...old, nickname })) - return {prev} - } - }) - const profile = useQuery({queryKey:['myProfile'], queryFn:fetchProfile}) + queryClient.setQueryData(['myProfile'], (old: Profile) => ({ ...old, nickname })); + return { prev }; + }, + }); + const profile = useQuery({ queryKey: ['myProfile'], queryFn: fetchProfile }); -return{fetchProfile , profile, patchNickName} + return { fetchProfile, profile, patchNickName }; } export default useFetchProfile; diff --git a/src/domains/mypage/components/EditNickName.tsx b/src/domains/mypage/components/EditNickName.tsx index 4a2a989e..dcee62c8 100644 --- a/src/domains/mypage/components/EditNickName.tsx +++ b/src/domains/mypage/components/EditNickName.tsx @@ -28,26 +28,26 @@ function EditNickName({ }: Props) { const [defaultNickname, setDefaultNickname] = useState(nickname); const { toastSuccess, toastError } = useToast(); - const { patchNickName } = useFetchProfile() - + const { patchNickName } = useFetchProfile(); + useEffect(() => { setEditNickName(nickname); setDefaultNickname(nickname); }, [nickname, setEditNickName]); const handlesave = async () => { - if (editNickName.length <= 1 || editNickName.length >= 8) { - toastError('닉네임은 2글자 이상 8글자 이내로 입력해야합니다'); - return; - } + if (editNickName.length <= 1 || editNickName.length >= 8) { + toastError('닉네임은 2글자 이상 8글자 이내로 입력해야합니다'); + return; + } - await setNickName(editNickName); - // CRUD중 CUD를 관리하는 메서드 - await patchNickName.mutateAsync(editNickName) + await setNickName(editNickName); + // CRUD중 CUD를 관리하는 메서드 + await patchNickName.mutateAsync(editNickName); - await setIsOpen(false); - toastSuccess('닉네임이 저장되었습니다.'); - } + await setIsOpen(false); + toastSuccess('닉네임이 저장되었습니다.'); + }; const handleChange = (e: React.ChangeEvent) => { setEditNickName(e.target.value); diff --git a/src/domains/mypage/main/MyProfile.tsx b/src/domains/mypage/main/MyProfile.tsx index d891353b..6f3b3de3 100644 --- a/src/domains/mypage/main/MyProfile.tsx +++ b/src/domains/mypage/main/MyProfile.tsx @@ -8,7 +8,7 @@ import { useQuery } from '@tanstack/react-query'; function MyProfile() { const { fetchProfile } = useFetchProfile(); - const {data} = useQuery({ queryKey: ['myProfile'], queryFn: fetchProfile }); + const { data } = useQuery({ queryKey: ['myProfile'], queryFn: fetchProfile }); if (!data) return; const { @@ -19,7 +19,7 @@ function MyProfile() { myCommentCount, myKeepCount, abvDegree, - } = data + } = data; return (
      diff --git a/src/domains/mypage/main/MySetting.tsx b/src/domains/mypage/main/MySetting.tsx index e724fa4b..716f1ecf 100644 --- a/src/domains/mypage/main/MySetting.tsx +++ b/src/domains/mypage/main/MySetting.tsx @@ -7,10 +7,9 @@ import { useEffect, useState } from 'react'; import useFetchProfile from '../api/fetchProfile'; import { useQuery } from '@tanstack/react-query'; - function MySetting() { const { fetchProfile } = useFetchProfile(); - const { data:profile } = useQuery({ queryKey: ['myProfile'], queryFn: fetchProfile }); + const { data: profile } = useQuery({ queryKey: ['myProfile'], queryFn: fetchProfile }); const [isOpen, setIsOpen] = useState(false); const [isQuit, setIsQuit] = useState(false); const [nickname, setNickName] = useState(profile?.nickname); diff --git a/src/domains/mypage/types/type.d.ts b/src/domains/mypage/types/type.d.ts index 41ccee95..58ab2699 100644 --- a/src/domains/mypage/types/type.d.ts +++ b/src/domains/mypage/types/type.d.ts @@ -4,9 +4,9 @@ export interface Profile { abvLevel: number; email: string | null; id: number; - myCommentCount: number - myKeepCount: number + myCommentCount: number; + myKeepCount: number; myLikedPostCount: number; myPostCount: number; nickname: string; -} \ No newline at end of file +} diff --git a/src/shared/api/Provider.tsx b/src/shared/api/Provider.tsx index 69325022..dafdf239 100644 --- a/src/shared/api/Provider.tsx +++ b/src/shared/api/Provider.tsx @@ -1,13 +1,8 @@ - 'use client'; import { useState } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; export default function Providers({ children }: { children: React.ReactNode }) { const [client] = useState(() => new QueryClient()); - return ( - - {children} - - ); + return {children}; }