navigate(`/lps/${lp.id}`)}
+ className="relative rounded-lg overflow-hidden shadow-lg hover:shadow-2xl transition-shadow duration-300 cursor-pointer"
>

{
+ // 1. 기존 요청 중단
+ await queryClient.cancelQueries({ queryKey: [QUERY_KEY.lps, lpId] });
+
+ // 2. 이전 캐시 상태 저장
+ const previousLpPost = queryClient.getQueryData
([QUERY_KEY.lps, lpId]);
+
+ // 3. 복사해서 newLpPost 만들기
+ const newLpPost: ResponseLpDto | undefined = previousLpPost
+ ? { ...previousLpPost, status: previousLpPost.status ?? false }
+ : undefined;
+
+ // 4. 현재 로그인한 사용자 정보
+ const me = queryClient.getQueryData([QUERY_KEY.myInfo]);
+
+ const userId: number = Number((me as ResponseMyInfoDto)?.data.id);
+
+ // 5. 좋아요 배열에서 해당 userId의 위치 찾기
+ const likedIndex: number =
+ previousLpPost?.data.likes.findIndex((like: Likes) => like.userId === userId) ?? -1;
+
+ // 6. 있으면 삭제, 없으면 추가 (토글 로직)
+ if (likedIndex >= 0) {
+ previousLpPost?.data.likes.splice(likedIndex, 1);
+ } else {
+ const newLike: Likes = { userId, lpId: lpId } as Likes;
+ previousLpPost?.data.likes.push(newLike);
+ }
+// 업데이트된 게시글 데이터를 캐시에 저장
+queryClient.setQueryData([QUERY_KEY.lps, lpId], newLpPost);
+
+return { previousLpPost, newLpPost };
+},
+
+onError: (
+ error: Error,
+ newLp: RequestLpDto,
+ context: { previousLpPost: ResponseLpDto | undefined; newLpPost: ResponseLpDto | undefined } | undefined
+) => {
+ console.log(error, newLp);
+ if (context?.previousLpPost) {
+ queryClient.setQueryData(
+ [QUERY_KEY.lps, newLp.lpId],
+ context.previousLpPost
+ );
+ }
+},
+
+onSettled: async (
+ data: ResponseLikeLpDto | undefined,
+ error: Error | null,
+ variables: RequestLpDto,
+ context: { previousLpPost: ResponseLpDto | undefined; newLpPost: ResponseLpDto | undefined } | undefined
+) => await queryClient.invalidateQueries({
+ queryKey: [QUERY_KEY.lps, variables.lpId],
+})
+ });
+}
+
+
+
+export default useDeleteLike;
\ No newline at end of file
diff --git "a/\354\236\254\354\210\234-\354\235\264\354\236\254\355\230\204/LP project/src/hooks/mutations/usePostLike.ts" "b/\354\236\254\354\210\234-\354\235\264\354\236\254\355\230\204/LP project/src/hooks/mutations/usePostLike.ts"
new file mode 100644
index 0000000..0fdfd71
--- /dev/null
+++ "b/\354\236\254\354\210\234-\354\235\264\354\236\254\355\230\204/LP project/src/hooks/mutations/usePostLike.ts"
@@ -0,0 +1,71 @@
+import { useMutation } from "@tanstack/react-query";
+import { postLike } from "../../apis/lp";
+import { Likes, RequestLpDto, ResponseLikeLpDto, ResponseLpDto } from "../../types/lp";
+import { queryClient } from "../../App";
+import { QUERY_KEY } from "../../constants/key";
+import { ResponseMyInfoDto } from "../../types/auth";
+
+function usePostLike() {
+ return useMutation({
+ mutationFn: postLike,
+ // onMutate → 요청 전에 optimistic update
+ onMutate: async ({ lpId }: RequestLpDto) => {
+ // 1. 기존 요청 중단
+ await queryClient.cancelQueries({ queryKey: [QUERY_KEY.lps, lpId] });
+
+ // 2. 이전 캐시 상태 저장
+ const previousLpPost = queryClient.getQueryData([QUERY_KEY.lps, lpId]);
+
+ // 3. 복사해서 newLpPost 만들기
+ const newLpPost: ResponseLpDto | undefined = previousLpPost
+ ? { ...previousLpPost, status: previousLpPost.status ?? false }
+ : undefined;
+
+ // 4. 현재 로그인한 사용자 정보
+ const me = queryClient.getQueryData([QUERY_KEY.myInfo]);
+
+ const userId: number = Number((me as ResponseMyInfoDto)?.data.id);
+
+ // 5. 좋아요 배열에서 해당 userId의 위치 찾기
+ const likedIndex: number =
+ previousLpPost?.data.likes.findIndex((like: Likes) => like.userId === userId) ?? -1;
+
+ // 6. 있으면 삭제, 없으면 추가 (토글 로직)
+ if (likedIndex >= 0) {
+ previousLpPost?.data.likes.splice(likedIndex, 1);
+ } else {
+ const newLike: Likes = { userId, lpId: lpId } as Likes;
+ previousLpPost?.data.likes.push(newLike);
+ }
+ // 업데이트된 게시글 데이터를 캐시에 저장
+ queryClient.setQueryData([QUERY_KEY.lps, lpId], newLpPost);
+
+ return { previousLpPost, newLpPost };
+ },
+
+ onError: (
+ error: Error,
+ newLp: RequestLpDto,
+ context: { previousLpPost: ResponseLpDto | undefined; newLpPost: ResponseLpDto | undefined } | undefined
+ ) => {
+ console.log(error, newLp);
+ if (context?.previousLpPost) {
+ queryClient.setQueryData(
+ [QUERY_KEY.lps, newLp.lpId],
+ context.previousLpPost
+ );
+ }
+ },
+
+ onSettled: async (
+ data: ResponseLikeLpDto | undefined,
+ error: Error | null,
+ variables: RequestLpDto,
+ context: { previousLpPost: ResponseLpDto | undefined; newLpPost: ResponseLpDto | undefined } | undefined
+ ) => await queryClient.invalidateQueries({
+ queryKey: [QUERY_KEY.lps, variables.lpId],
+ })
+});
+}
+
+export default usePostLike;
diff --git "a/\354\236\254\354\210\234-\354\235\264\354\236\254\355\230\204/LP project/src/hooks/queries/useGetLpDetail.ts" "b/\354\236\254\354\210\234-\354\235\264\354\236\254\355\230\204/LP project/src/hooks/queries/useGetLpDetail.ts"
new file mode 100644
index 0000000..61ddf25
--- /dev/null
+++ "b/\354\236\254\354\210\234-\354\235\264\354\236\254\355\230\204/LP project/src/hooks/queries/useGetLpDetail.ts"
@@ -0,0 +1,13 @@
+import { useQuery } from "@tanstack/react-query";
+import { QUERY_KEY } from "../../constants/key.ts";
+import { RequestLpDto } from "../../types/lp.ts";
+import { getLpDetail } from "../../apis/lp.ts";
+
+function useGetLpDetail({ lpId }: RequestLpDto) {
+ return useQuery({
+ queryKey: [QUERY_KEY.lps, lpId],
+ queryFn: () => getLpDetail({ lpId }),
+ });
+}
+
+export default useGetLpDetail;
diff --git "a/\354\236\254\354\210\234-\354\235\264\354\236\254\355\230\204/LP project/src/hooks/queries/useGetMyInfo.ts" "b/\354\236\254\354\210\234-\354\235\264\354\236\254\355\230\204/LP project/src/hooks/queries/useGetMyInfo.ts"
new file mode 100644
index 0000000..a330669
--- /dev/null
+++ "b/\354\236\254\354\210\234-\354\235\264\354\236\254\355\230\204/LP project/src/hooks/queries/useGetMyInfo.ts"
@@ -0,0 +1,13 @@
+import { useQuery } from "@tanstack/react-query";
+import { QUERY_KEY } from "../../constants/key.ts";
+import { getMyInfo } from "../../apis/auth.ts";
+
+function useGetMyInfo(accessToken: string|null) {
+ return useQuery({
+ queryKey: [QUERY_KEY.myInfo],
+ queryFn: getMyInfo,
+ enabled: !!accessToken,
+ });
+}
+
+export default useGetMyInfo;
diff --git "a/\354\236\254\354\210\234-\354\235\264\354\236\254\355\230\204/LP project/src/pages/LpDetailPage.tsx" "b/\354\236\254\354\210\234-\354\235\264\354\236\254\355\230\204/LP project/src/pages/LpDetailPage.tsx"
new file mode 100644
index 0000000..ea6e324
--- /dev/null
+++ "b/\354\236\254\354\210\234-\354\235\264\354\236\254\355\230\204/LP project/src/pages/LpDetailPage.tsx"
@@ -0,0 +1,61 @@
+import { useParams } from "react-router-dom";
+import useGetLpDetail from "../hooks/queries/useGetLpDetail.ts";
+import { Likes, ResponseLpDto } from "../types/lp.ts";
+import {Heart} from "lucide-react"
+import { useAuth } from "../context/AuthContext.tsx";
+import usePostLike from "../hooks/mutations/usePostLike.ts";
+import useDeleteLike from "../hooks/mutations/useDeleteLike.ts";
+import useGetMyInfo from "../hooks/queries/useGetMyInfo.ts";
+
+export const LpDetailPage = () => {
+ const lpId = useParams().lpId;
+ const {accessToken} = useAuth();
+
+ const {
+ data: lp,
+ isPending,
+ isError,
+ }: {
+ data: ResponseLpDto | undefined;
+ isPending: boolean;
+ isError: boolean;
+ } = useGetLpDetail({ lpId: Number(lpId) });
+
+ const { data: me } = useGetMyInfo(accessToken);
+
+const { mutate: likeMutate } = usePostLike();
+const { mutate: disLikeMutate } = useDeleteLike();
+
+const isLiked: boolean | undefined = lp?.data.likes.some(
+ (like: Likes) => like.userId === me?.data.id
+ );
+
+
+ const handleLikeLp = async() => {
+ likeMutate({lpId: Number(lpId)})
+ }
+
+const handleDislikeLp = async() => {
+ disLikeMutate({lpId: Number(lpId)});
+};
+
+
+ if (isPending && isError) {
+ return <>>;
+ }
+
+ return (
+
+
{lp?.data.title}
+

+
{lp?.data.content}
+
+
+
+ );
+};
+
+export default LpDetailPage;
diff --git "a/\354\236\254\354\210\234-\354\235\264\354\236\254\355\230\204/LP project/src/types/lp.ts" "b/\354\236\254\354\210\234-\354\235\264\354\236\254\355\230\204/LP project/src/types/lp.ts"
index bea438a..2659126 100644
--- "a/\354\236\254\354\210\234-\354\235\264\354\236\254\355\230\204/LP project/src/types/lp.ts"
+++ "b/\354\236\254\354\210\234-\354\235\264\354\236\254\355\230\204/LP project/src/types/lp.ts"
@@ -1,4 +1,4 @@
-import { CursorBasedResponse } from "./common.ts";
+import { CommonResponse, CursorBasedResponse } from "./common.ts";
export type Tag = {
id: number;
@@ -24,4 +24,14 @@ export type Lp = {
likes: Likes[];
}
-export type ResponseLpListDto = CursorBasedResponse;
\ No newline at end of file
+export type RequestLpDto = {
+ lpId: number;
+}
+
+export type ResponseLpDto = CommonResponse;
+export type ResponseLpListDto = CursorBasedResponse;
+export type ResponseLikeLpDto = CommonResponse<{
+ id: number;
+ userId: number;
+ lpId: number;
+}>;
\ No newline at end of file