diff --git a/src/apis/map/votePlace.ts b/src/apis/map/votePlace.ts index 5dbef34..65f9b58 100644 --- a/src/apis/map/votePlace.ts +++ b/src/apis/map/votePlace.ts @@ -1,6 +1,6 @@ import { instance } from '../instance'; -import type { ApiEnvelope } from '@/types/api.type'; -import type { AddCandidatePlaceRequest, AddCandidatePlaceResponse, GetCandidatePlacesResponse } from '@/types/map/votePlace.type'; +import type { ApiEnvelope, ApiEnvelopeNullable } from '@/types/api.type'; +import type { AddCandidatePlaceRequest, AddCandidatePlaceResponse, GetCandidatePlacesResponse, PostVoteRequest } from '@/types/map/votePlace.type'; // 투표 후보지 목록 조회 export const getCandidatePlaces = async (promiseId: string): Promise> => { @@ -18,4 +18,16 @@ export const addCandidatePlace = async (promiseId: string, body: AddCandidatePla export const deleteCandidatePlace = async (promiseId: string, candidateId: number): Promise> => { const response = await instance.delete(`/promises/${promiseId}/candidates/${candidateId}`); return response.data; -} \ No newline at end of file +} + +// 장소 투표 +export const postVote = async (promiseId: string, body: PostVoteRequest): Promise> => { + const response = await instance.post(`/promises/${promiseId}/votes`, body); + return response.data; +}; + +// 장소 투표 취소 +export const deleteVote = async (promiseId: string, candidateId: number): Promise> => { + const response = await instance.delete(`/promises/${promiseId}/votes/${candidateId}`); + return response.data; +}; diff --git a/src/pages/map/components/LocationBottomSheet.tsx b/src/pages/map/components/LocationBottomSheet.tsx index 9c74012..f070398 100644 --- a/src/pages/map/components/LocationBottomSheet.tsx +++ b/src/pages/map/components/LocationBottomSheet.tsx @@ -6,7 +6,7 @@ import PlusIcon from '@/assets/images/plusIcon.svg'; import NomineeMinusIcon from '@/assets/images/map/nomineeMinusIcon.svg'; import VoteIcon from '@/assets/images/map/voteIcon.svg'; import WarningIcon from '@/assets/images/warningIcon.svg'; -import { useAddCandidatePlace, useDeleteCandidatePlace } from '../services/useVotePalce'; +import { useAddCandidatePlace, useDeleteCandidatePlace, useDeleteVote, usePostVote } from '../services/useVotePalce'; import type { CandidatePlace } from '@/types/map/votePlace.type'; interface LocationBottomSheetProps { @@ -38,7 +38,7 @@ const LocationBottomSheet = ({ const height = isError ? 'h-60' : isConfirmed ? 'h-60' : 'h-45'; const isAdded = candidatesPlaces.some(candidatePlace => candidatePlace.name === selectedPlace.placeName && candidatePlace.address === selectedPlace.address); - const isVoted = candidatesPlaces.some(candidatePlace => candidatePlace.name === selectedPlace.placeName && candidatePlace.address === selectedPlace.address); + const isVoted = candidatesPlaces.find(candidatePlace => candidatePlace.name === selectedPlace.placeName && candidatePlace.address === selectedPlace.address)?.voteInfo.isMyVote; const selectedPlaceId = candidatesPlaces.find(candidatePlace => candidatePlace.name === selectedPlace.placeName && candidatePlace.address === selectedPlace.address)?.id; const {mutate: addCandidatePlaceMutate} = useAddCandidatePlace(promiseId); @@ -69,8 +69,21 @@ const LocationBottomSheet = ({ }); } + const {mutate: postVoteMutate} = usePostVote(promiseId); + const postVoteHandler = () => { + if (!selectedPlaceId) return; + console.log('selectedPlaceId: ', selectedPlaceId); + console.log("Clicked"); + + postVoteMutate({ + candidateId: selectedPlaceId, + }); + }; + + const {mutate: deleteVoteMutate} = useDeleteVote(promiseId, selectedPlaceId); console.log(selectedPlace); + console.log(candidatesPlaces) return ( {}} + onClick={() => isVoted ? deleteVoteMutate() : postVoteHandler()} > diff --git a/src/pages/map/services/useVotePalce.ts b/src/pages/map/services/useVotePalce.ts index 0ebd5a0..270335d 100644 --- a/src/pages/map/services/useVotePalce.ts +++ b/src/pages/map/services/useVotePalce.ts @@ -1,6 +1,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { addCandidatePlace, deleteCandidatePlace, getCandidatePlaces } from '@/apis/map/votePlace'; -import type { AddCandidatePlaceRequest } from '@/types/map/votePlace.type'; +import { addCandidatePlace, deleteCandidatePlace, deleteVote, getCandidatePlaces, postVote } from '@/apis/map/votePlace'; +import type { AddCandidatePlaceRequest, PostVoteRequest } from '@/types/map/votePlace.type'; +import axios from 'axios'; // 투표 후보지 목록 조회 export const useGetCandidatePlaces = (promiseId?: string) => { @@ -56,4 +57,54 @@ export const useDeleteCandidatePlace = (promiseId?: string, candidateId?: number console.error('투표 후보지 제거 실패: ', error); }, }) -} \ No newline at end of file +}; + +// 장소 투표 +export const usePostVote = (promiseId?: string) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (body: PostVoteRequest) => { + if (!promiseId) { + return Promise.reject(new Error("Promise ID is required")); + } + return postVote(promiseId, body); + }, + onSuccess: () => { + alert("투표가 완료되었습니다."); + queryClient.invalidateQueries({ queryKey: ['candidatePlaces', promiseId] }); + }, + onError: (error) => { + if (axios.isAxiosError(error)) { + const status = error.response?.status; + if (status === 409) { + const message = error.response?.data.message; + if (message) { + alert(message); + } + } + } + }, + }); +}; + +// 장소 투표 취소 +export const useDeleteVote = (promiseId?: string, candidateId?: number) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => { + if (!promiseId || !candidateId) { + return Promise.reject(new Error("Promise ID is required")); + } + return deleteVote(promiseId, candidateId); + }, + onSuccess: () => { + alert("투표가 취소되었습니다."); + queryClient.invalidateQueries({ queryKey: ['candidatePlaces', promiseId] }); + }, + onError: (error) => { + console.error('투표 취소 실패: ', error); + } + }) +}; diff --git a/src/types/map/votePlace.type.ts b/src/types/map/votePlace.type.ts index 1a7bfad..3eb33c8 100644 --- a/src/types/map/votePlace.type.ts +++ b/src/types/map/votePlace.type.ts @@ -1,4 +1,15 @@ // 투표 후보지 리스트 반환 +export interface VoteInfo { + creator: { + userId: number; + nickname: string; + }; + voteCount: number; + voters: { userId: number; nickname: string }[]; + isMyVote: boolean; + isMyCandidate: boolean; +} + export interface CandidatePlace { id: number; name: string; @@ -8,11 +19,13 @@ export interface CandidatePlace { address: string; distance: number; isConfirmed: boolean; + voteInfo: VoteInfo; }; export interface GetCandidatePlacesResponse { candidates: CandidatePlace[]; candidateCount: number; + totalMemberCount: number; }; // 투표 후보지 추가 @@ -34,3 +47,8 @@ export interface AddCandidatePlaceResponse { distance: number; isConfirmed: boolean; }; + +// 장소 투표 +export interface PostVoteRequest { + candidateId: number; +};