Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions 재순-이재현/LP project/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ProtectedLayout from './layouts/ProtectedLayout.tsx';
import GoogleLoginRedirectPage from './pages/GoogleLoginRedirectPage.tsx';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { LpDetailPage } from './pages/LpDetailPage.tsx';


//1. 홈페이지
Expand All @@ -28,6 +29,7 @@ const publicRoutes: RouteObject[] = [
{ path: "login", element: <LoginPage /> },
{ path: "signup", element: <SignupPage /> },
{ path: "v1/auth/google/callback", element: <GoogleLoginRedirectPage/>},
{ path: "lps/:lpid", element: <LpDetailPage /> },
],
},
];
Expand Down
24 changes: 23 additions & 1 deletion 재순-이재현/LP project/src/apis/lp.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PaginationDto } from "../types/common.ts";
import { ResponseLpListDto } from "../types/lp.ts";
import { ResponseLpListDto, RequestLpDto, ResponseLpDto, ResponseLikeLpDto } from "../types/lp.ts";
import { axiosInstance } from "./axios.ts";

export const getLpList = async (paginationDto: PaginationDto,
Expand All @@ -11,3 +11,25 @@ export const getLpList = async (paginationDto: PaginationDto,

return data;
};

export const getLpDetail = async (
{ lpId }: RequestLpDto
): Promise<ResponseLpDto> => {
const { data } = await axiosInstance.get(`/v1/lps/${lpId}`);
return data;
};

export const postLike = async (
{ lpId }: RequestLpDto
): Promise<ResponseLikeLpDto> => {
const { data } = await axiosInstance.post(`/v1/lps/${lpId}/likes`);
return data;
};

export const deleteLike = async (
{ lpId }: RequestLpDto
): Promise<ResponseLikeLpDto> => {
const { data } = await axiosInstance.delete(`/v1/lps/${lpId}/likes`);
return data;
};

5 changes: 4 additions & 1 deletion 재순-이재현/LP project/src/components/LpCard/LpCard.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { NavigateFunction, useNavigate } from "react-router-dom";
import { Lp } from "../../types/lp.ts";

interface LpCardProps {
lp: Lp;
}

const LpCard = ({ lp }: LpCardProps) => {
const navigate: NavigateFunction = useNavigate();
return (
<div
className="relative rounded-lg overflow-hidden shadow-lg hover:shadow-2xl transition-shadow duration-300"
onClick = {() => navigate(`/lps/${lp.id}`)}
className="relative rounded-lg overflow-hidden shadow-lg hover:shadow-2xl transition-shadow duration-300 cursor-pointer"
>
<img
src={lp.thumbnail}
Expand Down
7 changes: 5 additions & 2 deletions 재순-이재현/LP project/src/constants/key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ export const LOCAL_STORAGE_KEY: { accessToken: string; refreshToken: string } =
refreshToken: "refreshToken",
};

export const QUERY_KEY: { lps: string; lp: string; user: string } = {
export const QUERY_KEY: {
myInfo: any; lps: string; lp: string; user: string
} = {
lps: "lps",
lp: "",
user: ""
user: "",
myInfo: undefined
};
75 changes: 75 additions & 0 deletions 재순-이재현/LP project/src/hooks/mutations/useDeleteLike.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useMutation } from "@tanstack/react-query";
import { deleteLike } from "../../apis/lp.ts";
import { Likes, RequestLpDto, ResponseLikeLpDto, ResponseLpDto } from "../../types/lp.ts";
import { queryClient } from "../../App.tsx";
import { ResponseMyInfoDto } from "../../types/auth.ts";
import { QUERY_KEY } from "../../constants/key.ts";


function useDeleteLike() {
return useMutation({
mutationFn: deleteLike,

// onMutate → 요청 전에 optimistic update
onMutate: async ({ lpId }: RequestLpDto) => {
// 1. 기존 요청 중단
await queryClient.cancelQueries({ queryKey: [QUERY_KEY.lps, lpId] });

// 2. 이전 캐시 상태 저장
const previousLpPost = queryClient.getQueryData<ResponseLpDto>([QUERY_KEY.lps, lpId]);

// 3. 복사해서 newLpPost 만들기
const newLpPost: ResponseLpDto | undefined = previousLpPost
? { ...previousLpPost, status: previousLpPost.status ?? false }
: undefined;

// 4. 현재 로그인한 사용자 정보
const me = queryClient.getQueryData<ResponseMyInfoDto>([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;
71 changes: 71 additions & 0 deletions 재순-이재현/LP project/src/hooks/mutations/usePostLike.ts
Original file line number Diff line number Diff line change
@@ -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<ResponseLpDto>([QUERY_KEY.lps, lpId]);

// 3. 복사해서 newLpPost 만들기
const newLpPost: ResponseLpDto | undefined = previousLpPost
? { ...previousLpPost, status: previousLpPost.status ?? false }
: undefined;

// 4. 현재 로그인한 사용자 정보
const me = queryClient.getQueryData<ResponseMyInfoDto>([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;
13 changes: 13 additions & 0 deletions 재순-이재현/LP project/src/hooks/queries/useGetLpDetail.ts
Original file line number Diff line number Diff line change
@@ -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;
13 changes: 13 additions & 0 deletions 재순-이재현/LP project/src/hooks/queries/useGetMyInfo.ts
Original file line number Diff line number Diff line change
@@ -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;
61 changes: 61 additions & 0 deletions 재순-이재현/LP project/src/pages/LpDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="mt-12">
<h1>{lp?.data.title}</h1>
<img src={lp?.data.thumbnail} alt={lp?.data.title} />
<p>{lp?.data.content}</p>

<button onClick={() => (isLiked ? handleDislikeLp() : handleLikeLp())}>
<Heart color = {isLiked ? "red": "black"}
fill={isLiked? "red": "transparent"}/>
</button>
</div>
);
};

export default LpDetailPage;
14 changes: 12 additions & 2 deletions 재순-이재현/LP project/src/types/lp.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CursorBasedResponse } from "./common.ts";
import { CommonResponse, CursorBasedResponse } from "./common.ts";

export type Tag = {
id: number;
Expand All @@ -24,4 +24,14 @@ export type Lp = {
likes: Likes[];
}

export type ResponseLpListDto = CursorBasedResponse<Lp[]>;
export type RequestLpDto = {
lpId: number;
}

export type ResponseLpDto = CommonResponse<Lp>;
export type ResponseLpListDto = CursorBasedResponse<Lp[]>;
export type ResponseLikeLpDto = CommonResponse<{
id: number;
userId: number;
lpId: number;
}>;