Skip to content

Commit

Permalink
Merge pull request #77 from dnd-side-project/feature/76-review-like
Browse files Browse the repository at this point in the history
리뷰 좋아요 기능 추가
  • Loading branch information
froggy1014 authored Oct 17, 2024
2 parents a017a02 + 4d5524b commit bb4b103
Show file tree
Hide file tree
Showing 18 changed files with 212 additions and 73 deletions.
19 changes: 10 additions & 9 deletions src/apis/FiestaInstance.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { KyRequest, NormalizedOptions } from "ky";
import ky from "ky";
import ky, { Options } from "ky";

import { env } from "@/env";
import { getClientSideSession } from "@/lib/session";
Expand Down Expand Up @@ -35,29 +35,30 @@ const createKyInstance = () => {
methods: ["get", "post", "put", "patch", "delete"],
statusCodes: [408, 413, 429, 500, 502, 503, 504],
},
credentials: "include",
});
};

const Instance = createKyInstance();

const FiestaInstance = {
get: <T>(url: string, options?: RequestInit) =>
get: <T>(url: string, options?: Options) =>
Instance.get(url, options)
.json<FiestaResponse<T>>()
.then((res) => res.data),
post: <T>(url: string, json?: unknown, options?: RequestInit) =>
Instance.post(url, { json, ...options })
post: <T>(url: string, options?: Options) =>
Instance.post(url, options)
.json<FiestaResponse<T>>()
.then((res) => res.data),
put: <T>(url: string, json?: unknown, options?: RequestInit) =>
Instance.put(url, { json, ...options })
put: <T>(url: string, options?: Options) =>
Instance.put(url, options)
.json<FiestaResponse<T>>()
.then((res) => res.data),
patch: <T>(url: string, json?: unknown, options?: RequestInit) =>
Instance.patch(url, { json, ...options })
patch: <T>(url: string, options?: Options) =>
Instance.patch(url, options)
.json<FiestaResponse<T>>()
.then((res) => res.data),
delete: <T>(url: string, options?: RequestInit) =>
delete: <T>(url: string, options?: Options) =>
Instance.delete(url, options)
.json<FiestaResponse<T>>()
.then((res) => res.data),
Expand Down
7 changes: 3 additions & 4 deletions src/apis/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ const ENDPOINT = FIESTA_ENDPOINTS.users;

export async function postOauthLogin(body: SocialLoginRequest) {
const endpoint = ENDPOINT.login;
const response = await FiestaInstance.post<SocialLoginResponse>(
endpoint,
body,
);
const response = await FiestaInstance.post<SocialLoginResponse>(endpoint, {
json: body,
});

return response;
}
Expand Down
17 changes: 17 additions & 0 deletions src/apis/review/reviewLike/reviewLike.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import FiestaInstance from "@/apis/FiestaInstance";
import { FIESTA_ENDPOINTS } from "@/config";

import { ReviewLikeResponse } from "./reviewLikeType";

const ENDPOINT = FIESTA_ENDPOINTS.reviews;

export async function patchReviewLike({
reviewId,
}: {
reviewId: string | number;
}) {
const endpoint = ENDPOINT.like(reviewId);
const data = await FiestaInstance.patch<ReviewLikeResponse>(endpoint);

return data;
}
5 changes: 5 additions & 0 deletions src/apis/review/reviewLike/reviewLikeType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type ReviewLikeResponse = {
reviewId: number;
likeCount: number;
isLiked: boolean;
};
5 changes: 4 additions & 1 deletion src/apis/review/reviewReport/reviewReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ export async function postReviewReport(body: {
description: string;
}) {
const endpoint = ENDPOINT.reports(String(body.reviewId));

const data = await FiestaInstance.post<ReviewReportResponse>(endpoint, {
description: body.description,
json: {
description: body.description,
},
});

return data;
Expand Down
4 changes: 3 additions & 1 deletion src/apis/review/reviewUpdate/reviewUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ const ENDPOINT = FIESTA_ENDPOINTS.reviews;
export async function patchReview(payload: ReviewUpdateBody) {
const { reviewId, ...body } = payload;
const endpoint = ENDPOINT.detail(String(reviewId));
const data = await FiestaInstance.patch<ReviewUpdateResponse>(endpoint, body);
const data = await FiestaInstance.patch<ReviewUpdateResponse>(endpoint, {
json: body,
});

return data;
}
15 changes: 6 additions & 9 deletions src/apis/review/reviews/reviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export async function postReviews(payload: NewReviewSchemaType) {

const { images, ...rest } = payload;
const formData = new FormData();

formData.append(
"data",
new Blob([JSON.stringify(rest)], { type: "application/json" }),
Expand All @@ -50,10 +49,9 @@ export async function postReviews(payload: NewReviewSchemaType) {
formData.append("images", image);
});

const data = await FiestaInstance.post<PostReviewResponse>(
endpoint,
formData,
);
const data = await FiestaInstance.post<PostReviewResponse>(endpoint, {
body: formData,
});

return data;
}
Expand All @@ -72,10 +70,9 @@ export async function updateReview(payload: UpdateReviewSchemaType) {
formData.append("images", image);
});

const data = await FiestaInstance.patch<PostReviewResponse>(
endpoint,
formData,
);
const data = await FiestaInstance.patch<PostReviewResponse>(endpoint, {
body: formData,
});

return data;
}
Expand Down
2 changes: 1 addition & 1 deletion src/apis/review/reviews/reviewsType.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type FestivalReviewsParameters = {
festivalId?: number | string;
sort?: SortOption;
sort?: SortOption | string;
page?: number;
size?: number;
};
Expand Down
2 changes: 1 addition & 1 deletion src/apis/user/me/me.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const updateMe = async (body: ProfileMeUpdateSchemaType) => {
const endpoint = FIESTA_ENDPOINTS.users.me;
const data = await FiestaInstance.patch<Pick<UserMeResponse, "userId">>(
endpoint,
body,
{ json: body },
);

return data;
Expand Down
4 changes: 3 additions & 1 deletion src/apis/user/profile/patchProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { UserProfileRequest, UserProfileResponse } from "./profileType";

export const patchProfile = async (body: UserProfileRequest) => {
const endpoint = FIESTA_ENDPOINTS.users.profile;
const data = await FiestaInstance.patch<UserProfileResponse>(endpoint, body);
const data = await FiestaInstance.patch<UserProfileResponse>(endpoint, {
json: body,
});

return data;
};
4 changes: 3 additions & 1 deletion src/apis/user/profile/postProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { UserProfileRequest, UserProfileResponse } from "./profileType";

export const postProfile = async (body: UserProfileRequest) => {
const endpoint = FIESTA_ENDPOINTS.users.profile;
const data = await FiestaInstance.post<UserProfileResponse>(endpoint, body);
const data = await FiestaInstance.post<UserProfileResponse>(endpoint, {
json: body,
});

return data;
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { FC, useRef, useState } from "react";
import { parseAsInteger, useQueryState } from "nuqs";
import { FC, useMemo, useRef } from "react";

import { DetailFestivalResponse } from "@/apis/festivals/detailFestival/detailFestivalType";
import { reviewsKeys } from "@/apis/review/reviews/reviewKeys";
Expand All @@ -21,19 +22,23 @@ interface Props {
const TotalReviews: FC<Props> = ({ festivals }) => {
const totalReviewLabelRef = useRef<HTMLSpanElement>(null);

const [params, setParams] = useState<FestivalReviewsParameters>({
festivalId: festivals.festivalId,
sort: SortOption.createdAt,
page: 0,
size: 6,
const [page, setPage] = useQueryState("page", parseAsInteger.withDefault(0));
const [sort, setSort] = useQueryState("sort", {
defaultValue: SortOption.createdAt,
});

const handleSort = (sort: SortOption) => {
setParams((prev) => ({ ...prev, sort }));
};
const params: FestivalReviewsParameters = useMemo(
() => ({
festivalId: festivals.festivalId,
sort,
page,
size: 6,
}),
[page, sort, festivals],
);

const handlePage = (page: number) => {
setParams((prev) => ({ ...prev, page }));
setPage(page);

if (totalReviewLabelRef.current) {
totalReviewLabelRef.current.scrollIntoView({ behavior: "smooth" });
Expand Down Expand Up @@ -63,7 +68,7 @@ const TotalReviews: FC<Props> = ({ festivals }) => {
? "text-primary-01"
: "text-gray-scale-400 ",
)}
onClick={() => handleSort(SortOption.createdAt)}
onClick={() => setSort(SortOption.createdAt)}
>
최신순
</button>
Expand All @@ -75,7 +80,7 @@ const TotalReviews: FC<Props> = ({ festivals }) => {
? "text-primary-01"
: "text-gray-scale-400 ",
)}
onClick={() => handleSort(SortOption.likeCount)}
onClick={() => setSort(SortOption.likeCount)}
>
추천순
</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
"use client";

import { useMutation, useQueryClient } from "@tanstack/react-query";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { FC, useMemo, useState } from "react";

import { topKeywordFestivalKeys } from "@/apis/festivals/topKeywordFestival/topKeywordFestivalKeys";
import { deleteReview } from "@/apis/review/reviewDelete/reviewDelete";
import { postReviewReport } from "@/apis/review/reviewReport/reviewReport";
import { reviewsKeys } from "@/apis/review/reviews/reviewKeys";
import { Review } from "@/apis/review/reviews/reviewsType";
import type { Review } from "@/apis/review/reviews/reviewsType";
import { IconButton } from "@/components/core/Button";
import { BasicChip } from "@/components/core/Chip";
import { DropdownMenu } from "@/components/core/Dropdown";
import { ProgressCircle } from "@/components/core/Progress";
import { FestivalRequstDialog } from "@/components/Dialog";
import { HeartIcon } from "@/components/icons";
import Ratings from "@/components/rating/Ratings";
import useDeleteReview from "@/hooks/review/useDeleteReview";
import useReportReview from "@/hooks/review/useReportReview";
import useToggleReviewLike from "@/hooks/review/useToggleReviewLike";
import { formatToYYYYMMDD } from "@/lib/dayjs";
import { useUserStore } from "@/store/user";

Expand All @@ -27,30 +27,22 @@ const TotalReviewListItem: FC<Props> = ({ review }) => {
const router = useRouter();
const [isOpenReportDialog, setIsOpenReportDialog] = useState<boolean>(false);

const queryClient = useQueryClient();

const { mutate: deleteReviewMutate, isPending: isDeleting } = useMutation({
mutationFn: async (reviewId: number) => await deleteReview(reviewId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: reviewsKeys.all,
}),
queryClient.invalidateQueries({
queryKey: topKeywordFestivalKeys.list({
festivalId: review.festivalId,
}),
});
},
});
const { deleteReviewMutate, isDeleting } = useDeleteReview(review);
const { reportReview, isReporting } = useReportReview();
const { mutate: toggleReviewLike } = useToggleReviewLike(review);

const { mutate: postReviewMutate, isPending: isReporting } = useMutation({
mutationFn: async (body: { reviewId: number; description: string }) =>
await postReviewReport(body),
});
const handleReport = async (description: string) => {
reportReview({ reviewId: review.reviewId, description });
};

const handleDelete = (reviewId: number) => {
deleteReviewMutate(reviewId);
};

const handleToggle = () => {
toggleReviewLike({ reviewId: review.reviewId });
};

const myReviewOptions = [
{
label: "수정하기",
Expand Down Expand Up @@ -125,20 +117,28 @@ const TotalReviewListItem: FC<Props> = ({ review }) => {
<p className="text-body1-regular-lh-20 text-gray-scale-700">
{review.content}
</p>
<div className="flex w-full justify-between">
<div className="flex w-full items-center justify-between">
<div className="flex gap-[8px] overflow-auto scrollbar-hide">
{review.keywords.map((keyword) => (
{review.keywords.slice(0, 2).map((keyword) => (
<BasicChip key={keyword.keywordId} label={keyword.keyword} />
))}
</div>
{!!user && review.user.userId != user?.userId && (
<div className="flex w-[30px] items-center justify-between gap-1">
<IconButton active={review.isLiked} onClick={handleToggle}>
<HeartIcon width={20} height={20} className="gap-2" />
</IconButton>
<span className="text-caption2-regular text-gray-scale-600">
{review.likeCount}
</span>
</div>
)}
</div>

<FestivalRequstDialog
title="신고하기"
open={isOpenReportDialog}
onConfirm={async (description) =>
postReviewMutate({ reviewId: review.reviewId, description })
}
onConfirm={handleReport}
onOpenChange={() => setIsOpenReportDialog((prev) => !prev)}
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import AlertDialogWrapper from "../AlertDialogWrapper/AlertDialogWrapper";

interface Props extends Omit<AlertDialogProps, "onOpenChange"> {
title: string;
onConfirm: (content: string) => Promise<void>;
onConfirm: (description: string) => Promise<void>;
onOpenChange: (open: boolean) => void;
placeholder?: string;
}
Expand Down
4 changes: 2 additions & 2 deletions src/config/apiEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ const FIESTA_ENDPOINTS = {
reviews: {
topKeywords: "reviews/keywords/top",
base: "reviews",
detail: (reviewId: string) => `reviews/${reviewId}`,
like: (reviewId: string) => `reviews/${reviewId}/like`,
detail: (reviewId: string | number) => `reviews/${reviewId}`,
like: (reviewId: string | number) => `reviews/${reviewId}/like`,
mostlike: "reviews/mostlike",
keywords: "reviews/keywords",
reports: (reviewId: string) => `reviews/${reviewId}/reports`,
Expand Down
26 changes: 26 additions & 0 deletions src/hooks/review/useDeleteReview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";

import { topKeywordFestivalKeys } from "@/apis/festivals/topKeywordFestival/topKeywordFestivalKeys";
import { deleteReview } from "@/apis/review/reviewDelete/reviewDelete";
import { reviewsKeys } from "@/apis/review/reviews/reviewKeys";
import type { Review } from "@/apis/review/reviews/reviewsType";

const useDeleteReview = (review: Review) => {
const queryClient = useQueryClient();

const { mutate: deleteReviewMutate, isPending: isDeleting } = useMutation({
mutationFn: async (reviewId: number) => await deleteReview(reviewId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: reviewsKeys.all });
queryClient.invalidateQueries({
queryKey: topKeywordFestivalKeys.list({
festivalId: review.festivalId,
}),
});
},
});

return { deleteReviewMutate, isDeleting };
};

export default useDeleteReview;
Loading

0 comments on commit bb4b103

Please sign in to comment.