Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 입덕 포인트 추가, 조회 구현 #311

Merged
merged 10 commits into from
Nov 23, 2023
32 changes: 32 additions & 0 deletions src/features/reviews/api/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ export interface UserEvaluation {
score: number;
}

export type AttractionType =
| "STORY"
| "CHARACTER"
| "DRAWING"
| "VOICE_ACTOR"
| "MUSIC";

export default class ReviewApi {
/** @description 리뷰 작성 요청 */
async addReview(review: AddReviewDto): Promise<void> {
Expand Down Expand Up @@ -106,4 +113,29 @@ export default class ReviewApi {
async getEvaluation(animeId: number) {
return get<UserEvaluation>(`/ratings/${animeId}`);
}

// 입덕 포인트

/** @description 입덕 포인트 남기기 */
async addAttractionPoint(
animeId: number,
attractionElements: AttractionType[],
) {
return post(`/attraction-points`, {
animeId,
attractionElements,
});
}

/** @description 입덕 포인트 존재 여부 조회 */
async getUserAttractionPointStatus(animeId: number) {
return get<{ isAttractionPoint: boolean }>(`/attraction-points/${animeId}`);
}

/** @description 리뷰 수정 시 입덕 포인트 조회 */
async getUserAttractionPoint(animeId: number, name: string) {
return get<AttractionPoint>(`/short-reviews/attraction-points`, {
params: { animeId, name },
});
}
}
30 changes: 29 additions & 1 deletion src/features/reviews/api/reviewDev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import recentReviewMock1 from "./mock/recentReview1.json";
import recentReviewMock2 from "./mock/recentReview2.json";
import recentReviewMock3 from "./mock/recentReview3.json";
import recentReviewOnlyOneMock from "./mock/recentReviewOnlyOne.json";
import { AddReviewDto, ReviewInfo, UserEvaluation } from "./review";
import {
AddReviewDto,
AttractionType,
ReviewInfo,
UserEvaluation,
} from "./review";

export default class ReviewDevApi {
/** @description 리뷰 작성 요청*/
Expand Down Expand Up @@ -71,4 +76,27 @@ export default class ReviewDevApi {
async getEvaluation(animeId: number) {
return get<UserEvaluation>(`/ratings/${animeId}`);
}

/** @description 입덕 포인트 남기기 */
async addAttractionPoint(
animeId: number,
attractionElements: AttractionType[],
) {
return post(`/attraction-points`, {
animeId,
attractionElements,
});
}

/** @description 입덕 포인트 존재 여부 조회 */
async getUserAttractionPointStatus(animeId: number) {
return get<{ isAttractionPoint: boolean }>(`/attraction-points/${animeId}`);
}

/** @description 리뷰 수정 시 입덕 포인트 조회 */
async getUserAttractionPoint(animeId: number, name: string) {
return get<AttractionPoint>(`/short-reviews/attraction-points`, {
params: { animeId, name },
});
}
}
18 changes: 5 additions & 13 deletions src/features/reviews/components/ReviewCard/ReviewMoreButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import useSnackBar from "@/components/SnackBar/useSnackBar";
import useToast from "@/components/Toast/useToast";
import DropDownModal from "@/features/users/components/DropDownModal";
import useDropDownModal from "@/features/users/components/DropDownModal/useDropDownModal";
import useDebounce from "@/hooks/useDebounce";

import useEvaluation from "../../hook/useEvaluation";
import ShortReviewModal from "../ReviewRating/ShortReviewModal";
Expand All @@ -19,15 +20,6 @@ import {
RatingContainer,
} from "./ReviewMoreButton.style";

// TODO: 서버에서 가져오기
const USER_MOCK_ATTRACTION = {
character: true,
art: true,
story: false,
voiceActing: false,
sound: true,
};

interface ReviewMoreButtonProps {
isMine: boolean;
reviewId: number;
Expand All @@ -37,6 +29,8 @@ interface ReviewMoreButtonProps {
score: number;
}

const DEBOUNCE_DELAY = 200;

export default function ReviewMoreButton({
isMine,
reviewId,
Expand All @@ -62,7 +56,7 @@ export default function ReviewMoreButton({
handleReviewModalToggle();
};

const handleRate = (value: number) => {
const handleRate = useDebounce((value: number) => {
evaluationMutation.mutate(
{ score: value },
{
Expand All @@ -71,8 +65,7 @@ export default function ReviewMoreButton({
},
},
);
console.log(value);
};
}, DEBOUNCE_DELAY);

const handleReviewDeleteClick = () => console.log("리뷰삭제");

Expand Down Expand Up @@ -150,7 +143,6 @@ export default function ReviewMoreButton({
animeId,
content,
isSpoiler,
...USER_MOCK_ATTRACTION,
}}
>
<MyRating>내 별점</MyRating>
Expand Down
30 changes: 12 additions & 18 deletions src/features/reviews/components/ReviewRating/ShortReviewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { PropsWithChildren } from "react";
import Modal from "@/components/Modal";
import Textarea from "@/components/TextArea";

import { ReviewInfo } from "../../api/review";
import useReviewForm from "../../hook/useReviewForm";
import AttractionPoint from "../AttractionPoint";

Expand All @@ -15,23 +16,16 @@ import {
} from "./ShortReviewModal.style";
import SpoilerCheckBox from "./SpoilerCheckBox";

export interface MOCK_USER_REVIEW_DATA {
reviewId: number;
animeId: number;
content: string;
isSpoiler: boolean;
character: boolean;
art: boolean;
story: boolean;
voiceActing: boolean;
sound: boolean;
}
export type UserReview = Pick<
ReviewInfo,
"reviewId" | "animeId" | "content" | "isSpoiler"
>;

interface ShortReviewModalProps {
onClose: () => void;
onReview: () => void;
showBackdrop?: boolean;
userReviewData?: MOCK_USER_REVIEW_DATA;
userReviewData?: UserReview;
}

export default function ShortReviewModal({
Expand Down Expand Up @@ -60,14 +54,14 @@ export default function ShortReviewModal({
isChecked: form.character,
},
{
name: "art",
name: "drawing",
content: (
<>
현실 찢고 들어간듯한/이쁜
<strong style={{ marginLeft: 4 }}>그림체</strong>
</>
),
isChecked: form.art,
isChecked: form.drawing,
},
{
name: "story",
Expand All @@ -80,22 +74,22 @@ export default function ShortReviewModal({
isChecked: form.story,
},
{
name: "voiceActing",
name: "voiceActor",
content: (
<>
<strong>성우</strong>들의 미친 연기력
</>
),
isChecked: form.voiceActing,
isChecked: form.voiceActor,
},
{
name: "sound",
name: "music",
content: (
<>
가슴이 옹졸해지는 <strong style={{ marginLeft: 4 }}>음악</strong>
</>
),
isChecked: form.sound,
isChecked: form.music,
},
];

Expand Down
53 changes: 53 additions & 0 deletions src/features/reviews/hook/useAttractionPoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AxiosError } from "axios";

import useAuth from "@/features/auth/hooks/useAuth";
import { useApi } from "@/hooks/useApi";
import { useCommonToastError } from "@/libs/error";

import { AttractionType } from "../api/review";

export default function useAttractionPoint(animeId: number) {
const { reviewApi } = useApi();
const { user } = useAuth();
const { toastAuthError, toastDefaultError } = useCommonToastError();
const queryClient = useQueryClient();

// 사용자가 남긴 입덕 포인트 조회
const { data: userAttraction } = useQuery({
queryKey: ["attraction", animeId, user?.name],
queryFn: () => reviewApi.getUserAttractionPoint(animeId, user?.name ?? ""),
enabled: Boolean(user?.name),
});

// 사용자의 입덕 포인트 존재 여부 조회
const { data: status } = useQuery({
queryKey: ["attraction", animeId, "status", user?.memberId],
queryFn: () => reviewApi.getUserAttractionPointStatus(animeId),
});

// 입덕 포인트 추가
const addAttraction = useMutation({
mutationFn: (attractions: AttractionType[]) =>
reviewApi.addAttractionPoint(animeId, attractions),
onSuccess: () => {
queryClient.invalidateQueries(["attraction", animeId]);
// TODO: 애니 입덕 포인트 통계 query 무효화
},
onError: (error) => {
if (error instanceof AxiosError && error.response?.status) {
const status = error.response.status;
switch (status) {
case 401:
toastAuthError();
break;
default:
toastDefaultError();
break;
}
}
},
});

return { userAttraction, status, addAttraction };
}
68 changes: 50 additions & 18 deletions src/features/reviews/hook/useReviewForm.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";

import useToast from "@/components/Toast/useToast";
import useAuth from "@/features/auth/hooks/useAuth";
import useDebounce from "@/hooks/useDebounce";

import { MOCK_USER_REVIEW_DATA } from "../components/ReviewRating/ShortReviewModal";
import { AttractionType } from "../api/review";
import { UserReview } from "../components/ReviewRating/ShortReviewModal";

import useAttractionPoint from "./useAttractionPoint";
import useReview from "./useReview";

type ReviewForm = Pick<UserReview, "content" | "isSpoiler"> & AttractionPoint;

const DEBOUNCE_DELAY = 200;

export default function useReviewForm(
onReview: () => void,
userReviewData?: MOCK_USER_REVIEW_DATA,
userReviewData?: UserReview,
) {
const { pathname } = useLocation();
const navigate = useNavigate();
Expand All @@ -19,18 +26,31 @@ export default function useReviewForm(
const { user } = useAuth();
const { addReview, updateReview } = useReview(animeId, onReview);

const { userAttraction, addAttraction, status } = useAttractionPoint(animeId);

const toast = useToast();

const [form, setForm] = useState({
const [form, setForm] = useState<ReviewForm>({
content: userReviewData?.content ?? "",
isSpoiler: userReviewData?.isSpoiler ?? false,
character: userReviewData?.character ?? false,
art: userReviewData?.art ?? false,
story: userReviewData?.story ?? false,
voiceActing: userReviewData?.voiceActing ?? false,
sound: userReviewData?.sound ?? false,
character: userAttraction?.character ?? false,
drawing: userAttraction?.drawing ?? false,
story: userAttraction?.story ?? false,
voiceActor: userAttraction?.voiceActor ?? false,
music: userAttraction?.music ?? false,
});

useEffect(() => {
setForm((prev) => ({
...prev,
character: userAttraction?.character ?? false,
drawing: userAttraction?.drawing ?? false,
story: userAttraction?.story ?? false,
voiceActor: userAttraction?.voiceActor ?? false,
music: userAttraction?.music ?? false,
}));
}, [userAttraction]);

const [error, setError] = useState(false);

const handleTextInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
Expand All @@ -50,21 +70,33 @@ export default function useReviewForm(
}));
};

const handleReviewSubmit = () => {
const handleReviewSubmit = useDebounce(() => {
// 유효성 검사
setError(false);
if (form.content.trim().length < 10) {
setError(true);
return;
}

// console.log(form);
console.log({
name: user?.name,
animeId,
isSpoiler: form.isSpoiler,
content: form.content,
});
// 체크한 입덕 포인트
const selectedAttraction = Object.keys(form)
.filter(
(key) =>
form[key as keyof ReviewForm] === true &&
!["content, isSpoiler"].includes(key),
)
.map((key) =>
key === "voiceActor" ? "VOICE_ACTOR" : key.toUpperCase(),
) as AttractionType[];

console.log("입덕 포인트: ", selectedAttraction);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

콘솔로그는 제거 부탁드립니다~

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거는 입덕 포인트 수정까지 구현한 후에 제거하려고 합니다..!


// 입덕 포인트 추가: 체크된 입덕 포인트가 있는 경우에만 요청
if (selectedAttraction.length !== 0 && !status?.isAttractionPoint) {
addAttraction.mutate(selectedAttraction);
}

// TODO: 입덕 포인트 수정

// 리뷰 수정
if (userReviewData) {
Expand Down Expand Up @@ -113,7 +145,7 @@ export default function useReviewForm(
},
);
}
};
}, DEBOUNCE_DELAY);

return {
form,
Expand Down
Loading