@@ -75,7 +84,17 @@ function VoteDescription({
onClick={() => onClickVote("B")}
percent={percentageB > 0 ? percentageB : 0}
>
-
{titleB}
{percentageB}%
@@ -93,7 +112,9 @@ function VoteDescription({
);
}
-const Container = styled.div``;
+const Container = styled.div`
+ overflow: hidden;
+`;
const ImageWrapper = styled.div`
position: relative;
@@ -126,13 +147,15 @@ const FlexRow = styled.div`
const variantStyles = {
active: css`
transition: all 0.3s ease-in-out;
- width: 100%;
+ width: 90%;
font-size: 16px;
font-weight: 700;
padding: 0 1px;
+ pointer-events: none;
`,
inactive: css`
- width: 0%;
+ width: 10%;
+ pointer-events: none;
`,
};
@@ -147,6 +170,7 @@ const LeftVote = styled.div<{ selected: ActiveType; percent: number }>`
aspect-ratio: 1;
max-height: 300px;
display: flex;
+ transition: all 0.3s ease-in-out;
justify-content: center;
.overlay {
position: absolute;
@@ -164,6 +188,7 @@ const LeftVote = styled.div<{ selected: ActiveType; percent: number }>`
background: rgba(250, 94, 45, 0.7);
border-radius: 10px;
border: 2px solid #ff4a16;
+
${({ selected, percent }) =>
selected === "active" &&
css`
@@ -172,6 +197,9 @@ const LeftVote = styled.div<{ selected: ActiveType; percent: number }>`
`};
}
${({ selected }) => typeGuardVariantStyle(selected)}
+ &:hover {
+ width: 90%;
+ }
`;
const RightVote = styled(LeftVote)`
diff --git a/apps/jurumarble/src/app/vote/[id]/hooks/useCommentFilter.ts b/apps/jurumarble/src/app/vote/[id]/hooks/useCommentFilter.ts
deleted file mode 100644
index 150f6e69..00000000
--- a/apps/jurumarble/src/app/vote/[id]/hooks/useCommentFilter.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { useState } from "react";
-
-export interface CommentFilter {
- age: number | null;
- mbti: string | null;
- gender: string | null;
- sortBy: string | null;
-}
-
-export default function useCommentFilter() {
- const [commentFilter, setCommentFilter] = useState
({
- age: null,
- mbti: null,
- gender: null,
- sortBy: "ByTime",
- });
-
- const onChangeCommentFilter = (sort: string) => {
- setCommentFilter((prev) => ({
- ...prev,
- sortBy: sort,
- }));
- };
-
- return { commentFilter, onChangeCommentFilter };
-}
diff --git a/apps/jurumarble/src/app/vote/[id]/page.tsx b/apps/jurumarble/src/app/vote/[id]/page.tsx
index 4d50b601..a92b4f0c 100644
--- a/apps/jurumarble/src/app/vote/[id]/page.tsx
+++ b/apps/jurumarble/src/app/vote/[id]/page.tsx
@@ -9,19 +9,38 @@ import VoteDescription from "./components/VoteDescription";
import { useState } from "react";
import ChipContainer from "./components/ChipContainer";
import CommentContainer from "./components/CommentContainer";
-import { useSearchParams } from "next/navigation";
+import { useParams, useSearchParams } from "next/navigation";
import { useToggle } from "@monorepo/hooks";
import SearchRestaurantModal from "./components/SearchRestaurantModal";
+import usePostBookmarkService from "../post/services/useBookmarkService";
+import useVoteLoadService from "./services/useVoteLoadService";
+import useExecuteVoteService from "./services/useExecuteVoteService";
function Detail() {
- const searchParams = useSearchParams();
- const postId = searchParams.get("id");
+ const params = useParams();
+
+ const postId = params.id;
const [selected, setSelected] = useState<"A" | "B" | null>(null);
+
+ const [isSearchRestaurantModal, onToggleSearchRestaurantModal] = useToggle(true);
+
+ const { data, isError, isLoading } = useVoteLoadService(Number(postId));
+
+ const { mutateBookMark, bookMarkCheckQuery } = usePostBookmarkService(Number(postId));
+
+ const { mutate, select } = useExecuteVoteService(Number(postId));
const onMutateVoting = (select: "A" | "B") => {
- setSelected(select);
+ mutate(select);
};
- const [isSearchRestaurantModal, onToggleSearchRestaurantModal] = useToggle(true);
+ const { data: bookmarkCheck } = bookMarkCheckQuery;
+
+ const isBookmark = bookmarkCheck?.bookmarked || false;
+
+ if (isLoading) return 로딩중
;
+ if (isError) return 에러
;
+ if (!data) return ;
+ const { detail, title, titleA, titleB, region, imageA, imageB } = data;
return (
@@ -39,17 +58,24 @@ function Detail() {
/>
- {/* */}
+
@@ -78,6 +104,7 @@ const Container = styled.div`
const PageInner = styled.div`
padding: 20px;
border-top-left-radius: 20px;
+ border-bottom: none;
position: relative;
margin: 0 auto;
border-radius: 4px;
diff --git a/apps/jurumarble/src/app/vote/[id]/services/useCommentServices.ts b/apps/jurumarble/src/app/vote/[id]/services/useCommentServices.ts
new file mode 100644
index 00000000..d19ed6ec
--- /dev/null
+++ b/apps/jurumarble/src/app/vote/[id]/services/useCommentServices.ts
@@ -0,0 +1,77 @@
+import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import {
+ getCommentById,
+ postComment,
+ PostCommentRequest,
+ postHateComment,
+ postLikeComment,
+} from "lib/apis/comment";
+import { queryKeys, reactQueryKeys } from "lib/queryKeys";
+import React from "react";
+
+export default function useCommentServices(
+ voteId: number,
+ sortBy: "ByTime" | "ByPopularity",
+ commentType: "votes" | "drinks",
+ paging?: {
+ page: number;
+ size: number;
+ },
+) {
+ const queryClient = useQueryClient();
+ const {
+ data: comments,
+ isLoading,
+ isError,
+ fetchNextPage,
+ } = useInfiniteQuery(
+ reactQueryKeys.detailCommentList(
+ voteId,
+ commentType,
+ paging?.page ?? 0,
+ paging?.size ?? 10,
+ sortBy,
+ ),
+ ({ pageParam = 0 }) =>
+ getCommentById({
+ commentType,
+ paging: { page: pageParam, size: 100 },
+ sortBy: sortBy,
+ typeId: voteId,
+ }),
+ {
+ getNextPageParam: (lastPage, pages) => {
+ // @NOTE 백엔드에서 last 작동이 안되어 주석
+ if (lastPage.last) return undefined;
+ return pages.length + 1;
+ },
+ keepPreviousData: true,
+ cacheTime: 1000 * 60 * 5,
+ staleTime: 1000 * 60 * 5,
+ },
+ );
+
+ const { mutate: mutateLike } = useMutation(
+ (commentId: number) => postLikeComment(commentType, voteId, commentId),
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries([queryKeys.DETAIL_COMMENT_LIST]);
+ },
+ },
+ );
+
+ const { mutate: mutateHate } = useMutation(
+ (commentId: number) => postHateComment(commentType, voteId, commentId),
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries([queryKeys.DETAIL_COMMENT_LIST]);
+ },
+ },
+ );
+
+ const { mutate: mutateComment } = useMutation((body: PostCommentRequest) =>
+ postComment(commentType, voteId, body),
+ );
+
+ return { comments, isLoading, isError, fetchNextPage, mutateLike, mutateHate, mutateComment };
+}
diff --git a/apps/jurumarble/src/app/vote/[id]/services/useExecuteVoteService.ts b/apps/jurumarble/src/app/vote/[id]/services/useExecuteVoteService.ts
new file mode 100644
index 00000000..0cdb3a06
--- /dev/null
+++ b/apps/jurumarble/src/app/vote/[id]/services/useExecuteVoteService.ts
@@ -0,0 +1,37 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+
+import { AorB, getVotingCheck, postExecuteVote } from "lib/apis/vote";
+
+import { queryKeys, reactQueryKeys } from "lib/queryKeys";
+import { useState } from "react";
+
+export default function useExecuteVoteService(voteId: number) {
+ const [select, setSelect] = useState<{ choice: AorB | null }>({ choice: null });
+ const queryClient = useQueryClient();
+ const { mutate } = useMutation((choice: "A" | "B") => postExecuteVote(voteId, { choice }), {
+ onSuccess: () => {
+ queryClient.invalidateQueries([queryKeys.VOTE_DETAIL]);
+ queryClient.invalidateQueries([queryKeys.VOTING_CHECK]);
+ },
+ onError: () => {
+ alert("로그인 후 진행해주세요.");
+ },
+ });
+
+ const { data } = useQuery(reactQueryKeys.votingCheck(voteId), () => getVotingCheck(voteId), {
+ onSuccess: (data) => {
+ if (data.voted) {
+ setSelect({ choice: data.userChoice });
+ } else setSelect({ choice: null });
+ },
+ onError: () => {
+ setSelect({ choice: null });
+ },
+ enabled: !!voteId,
+ // @note 캐시를 사용하지 않는다.
+ cacheTime: 0,
+ staleTime: 0,
+ });
+
+ return { mutate, select, data };
+}
diff --git a/apps/jurumarble/src/app/vote/[id]/services/useVoteLoadService.ts b/apps/jurumarble/src/app/vote/[id]/services/useVoteLoadService.ts
new file mode 100644
index 00000000..973c93ff
--- /dev/null
+++ b/apps/jurumarble/src/app/vote/[id]/services/useVoteLoadService.ts
@@ -0,0 +1,16 @@
+import { useQuery } from "@tanstack/react-query";
+import { getVoteByVoteIdAPI } from "lib/apis/vote";
+
+import { reactQueryKeys } from "lib/queryKeys";
+
+export default function useVoteLoadService(voteId: number) {
+ const { data, isLoading, isError } = useQuery(
+ reactQueryKeys.voteDetail(voteId),
+ () => getVoteByVoteIdAPI(voteId),
+ {
+ enabled: !!voteId,
+ },
+ );
+
+ return { data, isLoading, isError };
+}
diff --git a/apps/jurumarble/src/app/vote/page.tsx b/apps/jurumarble/src/app/vote/page.tsx
index 2486e0e8..a52d2135 100644
--- a/apps/jurumarble/src/app/vote/page.tsx
+++ b/apps/jurumarble/src/app/vote/page.tsx
@@ -4,26 +4,25 @@ import BottomBar from "components/BottomBar";
import { Button } from "components/button";
import Header from "components/Header";
import { media } from "lib/styles";
-import { useRouter } from "next/navigation";
+import { useParams, useRouter } from "next/navigation";
import { EmptyAImg, ExImg1 } from "public/images";
import React, { useState } from "react";
import SvgIcDetail from "src/assets/icons/components/IcDetail";
import styled, { css } from "styled-components";
import useFlipAnimation from "./hooks/useFlipAnimation";
import useInfiniteMainListService from "./post/services/useGetVoteListService";
+import usePostBookmarkService from "./post/services/useBookmarkService";
import ChipContainer from "./[id]/components/ChipContainer";
import VoteDescription from "./[id]/components/VoteDescription";
import Path from "lib/Path";
-import useBookmarkService from "services/useBookmarkService";
+import useExecuteVoteService from "./[id]/services/useExecuteVoteService";
export type Drag = "up" | "down" | null;
function VoteHomePage() {
+ const params = useParams();
+
const router = useRouter();
- const [selected, setSelected] = useState<"A" | "B" | null>(null);
- const onMutateVoting = (select: "A" | "B") => {
- setSelected(select);
- };
const { isError, isLoading, mainVoteList, nowShowing, onChangeNowShowing } =
useInfiniteMainListService({
@@ -37,7 +36,12 @@ function VoteHomePage() {
const { title, imageA, imageB, titleA, titleB, detail, voteId, region } =
mainVoteList[nowShowing] || {};
- const { mutateBookMark, bookMarkCheckQuery } = useBookmarkService(voteId);
+ const { mutateBookMark, bookMarkCheckQuery } = usePostBookmarkService(voteId);
+
+ const { mutate, select } = useExecuteVoteService(voteId);
+ const onMutateVoting = (select: "A" | "B") => {
+ mutate(select);
+ };
const { data: bookmarkCheck } = bookMarkCheckQuery;
@@ -90,7 +94,7 @@ function VoteHomePage() {
titleB={titleB}
totalCountA={100}
totalCountB={100}
- select={selected}
+ select={select.choice}
onMutateVoting={onMutateVoting}
/>
router.push(`vote/${voteId}`)}>
diff --git a/apps/jurumarble/src/app/vote/post/services/useBookmarkService.ts b/apps/jurumarble/src/app/vote/post/services/useBookmarkService.ts
new file mode 100644
index 00000000..429c46d6
--- /dev/null
+++ b/apps/jurumarble/src/app/vote/post/services/useBookmarkService.ts
@@ -0,0 +1,33 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { getBookMarkCheckAPI, postBookmarkAPI } from "lib/apis/bookmark";
+import Path from "lib/Path";
+import { queryKeys, reactQueryKeys } from "lib/queryKeys";
+import { useRouter } from "next/navigation";
+import { toast } from "react-toastify";
+
+export default function usePostBookmarkService(voteId: number) {
+ const queryClient = useQueryClient();
+
+ const bookMarkCheckQuery = useQuery(
+ reactQueryKeys.bookmarkCheck(),
+ () => getBookMarkCheckAPI(voteId),
+ {
+ enabled: !!voteId,
+ },
+ );
+
+ const router = useRouter();
+ const { mutate: mutateBookMark } = useMutation(() => postBookmarkAPI(voteId), {
+ onSuccess: () => {
+ queryClient.invalidateQueries([queryKeys.BOOKMARK_CHECK]);
+ toast("북마크에서 해제되었어요");
+ },
+ onError: () => {
+ if (confirm("로그인이 필요한 서비스입니다.")) {
+ router.push(Path.VOTE_HOME);
+ }
+ },
+ });
+
+ return { mutateBookMark, bookMarkCheckQuery };
+}
diff --git a/apps/jurumarble/src/lib/apis/comment.ts b/apps/jurumarble/src/lib/apis/comment.ts
new file mode 100644
index 00000000..aeb3faf3
--- /dev/null
+++ b/apps/jurumarble/src/lib/apis/comment.ts
@@ -0,0 +1,93 @@
+import { http } from "./http/http";
+import { AorB } from "./vote";
+
+export interface PagingRequest {
+ page: number;
+ size: number;
+}
+
+export interface GetCommentRequest {
+ sortBy: "ByTime" | "ByPopularity";
+ paging: PagingRequest;
+ commentType: "votes" | "drinks";
+ typeId: number;
+}
+
+interface CommentResponse {
+ id: number;
+ userId: number;
+ voteId: number;
+ drinkId: number;
+ nickName: string;
+ parentId: number;
+ content: string;
+ imageUrlstring: string;
+ gender: string;
+ age: string;
+ mbti: string;
+ createdDate: string;
+ likeCount: number;
+ hateCount: number;
+ choice: AorB;
+ restaurant: {
+ restaurantName: string;
+ restaurantImage: string;
+ };
+}
+
+export interface GetCommentResponse {
+ content: CommentResponse[];
+ empty: boolean;
+ first: boolean;
+ last: boolean;
+ numberOfElements: number;
+ size: number;
+}
+
+export const getCommentById = async ({
+ commentType,
+ paging,
+ sortBy,
+ typeId,
+}: GetCommentRequest) => {
+ const response = await http.get(`api/${commentType}/${typeId}/comments`, {
+ params: {
+ sortBy,
+ page: paging.page,
+ size: paging.size,
+ },
+ });
+ return response.data;
+};
+
+export const postLikeComment = async (
+ commentType: "votes" | "drinks",
+ typeId: number,
+ commentId: number,
+) => {
+ const response = await http.post(`/api/${commentType}/${typeId}/comments/${commentId}/likers`);
+ return response.data;
+};
+
+export const postHateComment = async (
+ commentType: "votes" | "drinks",
+ typeId: number,
+ commentId: number,
+) => {
+ const response = await http.post(`/api/${commentType}/${typeId}/comments/${commentId}/haters`);
+ return response.data;
+};
+
+export interface PostCommentRequest {
+ content: string;
+ parentId?: number | null;
+}
+
+export const postComment = async (
+ commentType: "votes" | "drinks",
+ voteId: number,
+ body: PostCommentRequest,
+) => {
+ const response = await http.post(`/api/${commentType}/${voteId}/comments/create`, body);
+ return response.data;
+};
diff --git a/apps/jurumarble/src/lib/apis/vote.ts b/apps/jurumarble/src/lib/apis/vote.ts
index 41eb57dc..6fd5bde9 100644
--- a/apps/jurumarble/src/lib/apis/vote.ts
+++ b/apps/jurumarble/src/lib/apis/vote.ts
@@ -1,6 +1,8 @@
+import axios from "axios";
import { SERVER_URL } from "lib/constants";
import { SortType } from "src/types/common";
import { baseApi } from "./http/base";
+import { http } from "./http/http";
type VoteListSortType = Omit;
@@ -48,29 +50,26 @@ export const getVoteListAPI = async ({ page, size, sortBy, keyword }: GetVoteLis
return response.data;
};
-interface Writer {
- userImage: string | null;
- userGender: string;
- userAge: number;
- userMbti: string;
- nickName: string;
-}
-
export interface GetVoteByIdResponse {
- writer: Writer;
- voteCreatedDate: Date;
+ voteId: number;
+ postedUserId: number;
title: string;
+ detail: string;
+ filteredGender: string;
+ filteredAge: string;
+ filteredMbti: string;
+ votedCount: number;
+ voteType: string;
imageA: string;
imageB: string;
titleA: string;
titleB: string;
- description: string;
+ region: string;
}
export const getVoteByVoteIdAPI = async (voteId: number) => {
- const response = await fetch(`${SERVER_URL}api/votes/${voteId}`, {});
- const voteInfo = await response.json();
- return voteInfo.data;
+ const response = await baseApi.get(`api/votes/${voteId}`);
+ return response.data;
};
interface ModifyVoteRequest {
@@ -182,3 +181,19 @@ export const getVoteDrinkList = async (params: GetVoteDrinkListRequest) => {
});
return response.data.voteSlice;
};
+
+export const postExecuteVote = async (voteId: number, body: { choice: "A" | "B" | null }) => {
+ const response = await http.post(`api/votes/${voteId}/vote`, body);
+ return response.data;
+};
+
+export type AorB = "A" | "B";
+interface GetVotingCheckResponse {
+ userChoice: AorB | null;
+ voted: boolean;
+}
+
+export const getVotingCheck = async (voteId: number) => {
+ const response = await http.get(`api/votes/${voteId}/voted`);
+ return response.data;
+};
diff --git a/apps/jurumarble/src/lib/queryKeys.ts b/apps/jurumarble/src/lib/queryKeys.ts
index 34af1717..ae3f280b 100644
--- a/apps/jurumarble/src/lib/queryKeys.ts
+++ b/apps/jurumarble/src/lib/queryKeys.ts
@@ -6,6 +6,10 @@ export const queryKeys = {
RESTAURANT_LIST: "restaurantList" as const,
SEARCH_DRINK_LIST: "searchDrinkList" as const,
SEARCH_VOTE_DRINK_LIST: "searchVoteDrinkList" as const,
+ VOTE_DETAIL: "voteDetail" as const,
+ VOTING_CHECK: "votingCheck" as const,
+ DETAIL_COMMENT_LIST: "commentByVoteId" as const,
+ DETAIL_VOTE_COUNT: "voteCountByVoteId" as const,
};
export const reactQueryKeys = {
@@ -15,4 +19,14 @@ export const reactQueryKeys = {
userInfo: () => [queryKeys.USER_INFO],
voteList: (params: any) => [queryKeys.VOTE_LIST, ...params],
restaurantList: (params: any) => [queryKeys.RESTAURANT_LIST, ...params],
+ voteDetail: (voteId: number) => [queryKeys.VOTE_DETAIL, voteId] as const,
+ votingCheck: (id: number) => [queryKeys.VOTING_CHECK, id] as const,
+ detailCommentList: (
+ typeId: number,
+ commentType: "votes" | "drinks",
+ size?: number,
+ page?: number,
+ sortBy?: string,
+ ) => [queryKeys.DETAIL_COMMENT_LIST, typeId, commentType, size, page, sortBy] as const,
+ detailVoteCount: (id: number) => [queryKeys.DETAIL_VOTE_COUNT, id] as const,
};