diff --git a/src/apis/map/votePlace.ts b/src/apis/map/votePlace.ts new file mode 100644 index 0000000..5dbef34 --- /dev/null +++ b/src/apis/map/votePlace.ts @@ -0,0 +1,21 @@ +import { instance } from '../instance'; +import type { ApiEnvelope } from '@/types/api.type'; +import type { AddCandidatePlaceRequest, AddCandidatePlaceResponse, GetCandidatePlacesResponse } from '@/types/map/votePlace.type'; + +// 투표 후보지 목록 조회 +export const getCandidatePlaces = async (promiseId: string): Promise> => { + const response = await instance.get(`/promises/${promiseId}/candidates`); + return response.data; +}; + +// 투표 후보지 추가 +export const addCandidatePlace = async (promiseId: string, body: AddCandidatePlaceRequest): Promise> => { + const response = await instance.post(`/promises/${promiseId}/candidates`, body); + return response.data; +}; + +// 투표 후보지 제거 +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 diff --git a/src/pages/map/PromiseMap.tsx b/src/pages/map/PromiseMap.tsx index 0a1644d..1d6470b 100644 --- a/src/pages/map/PromiseMap.tsx +++ b/src/pages/map/PromiseMap.tsx @@ -18,6 +18,7 @@ import { useComment } from './hooks/useComment'; import type { GetCommentsParams } from '@/types/map/comment.type'; import CustomMarkerIcon from '@/assets/images/map/customMarkerIcon.svg'; import CommentIcon from '@/assets/images/map/commentIcon.svg'; +import { useGetCandidatePlaces } from './services/useVotePalce'; import { usePromiseDetail } from './services/usePromiseDetail'; const PromiseMap = () => { @@ -37,12 +38,8 @@ const PromiseMap = () => { const isMultipleVoting = promise?.isMultipleVoting ?? false; const { - markers, votedPlace, votedPlaces, - handleToggleAdd, - handleToggleVote, - getIsVoted, } = useVoteState({ isMultipleVoting }); const { @@ -63,7 +60,6 @@ const PromiseMap = () => { isSheetOpen, setIsSheetOpen, selectedPlace, - pendingPlace, selectedOverlay, setSelectedOverlay, handleMapClick, @@ -86,16 +82,15 @@ const PromiseMap = () => { if (!promise) return null; - const isPendingAdded = pendingPlace - ? markers.some(m => m.placeName === pendingPlace.placeName) - : false; - - const isVoted = getIsVoted(pendingPlace); const isConfirmed = false; // 확정된 장소 예정 + const {data: candidatePlacesResponse} = useGetCandidatePlaces(promiseId); + const candidatePlaces = candidatePlacesResponse?.data.candidates; + const candidatePlacesCount = candidatePlacesResponse?.data.count; + const handleGoVoteResult = () => { navigate(`/map/${promiseId}/vote`, { - state: { promise, markers, votedPlaces: [...votedPlaces], votedPlace }, + state: { promise, candidatesPlaces: candidatePlaces, votedPlaces: [...votedPlaces], votedPlace }, }); }; @@ -133,36 +128,46 @@ const PromiseMap = () => { }, ]} > - {markers.map((marker, i) => ( + {candidatePlaces?.map((candidatePlace, i) => ( <> {/* 선택된 마커는 숨김 */} {!( - selectedOverlay?.lat === marker.lat && - selectedOverlay?.lng === marker.lng + selectedOverlay?.lat === candidatePlace.latitude && + selectedOverlay?.lng === candidatePlace.longitude ) && ( setSelectedOverlay(marker)} + onClick={() => setSelectedOverlay({ + lat: candidatePlace.latitude, + lng: candidatePlace.longitude, + placeName: candidatePlace.name, + address: candidatePlace.address, + })} /> )} {/* 선택된 마커 위치에 말풍선 표시 */} - {selectedOverlay?.lat === marker.lat && - selectedOverlay?.lng === marker.lng && ( + {selectedOverlay?.lat === candidatePlace.latitude && + selectedOverlay?.lng === candidatePlace.longitude && ( handleOverlayOpen(marker)} + name={candidatePlace.name} + onClick={() => handleOverlayOpen({ + lat: candidatePlace.latitude, + lng: candidatePlace.longitude, + placeName: candidatePlace.name, + address: candidatePlace.address, + })} /> )} @@ -251,7 +256,7 @@ const PromiseMap = () => { {/* 채팅 버튼 */} {!isSheetOpen && !isCommentOpen && (
0 ? 'bottom-48' : 'bottom-35'}`} + className={`absolute right-6 z-50 ${candidatePlacesCount && candidatePlacesCount > 0 ? 'bottom-48' : 'bottom-35'}`} onClick={() => { setIsSheetOpen(false); setSelectedOverlay(null); @@ -302,15 +307,15 @@ const PromiseMap = () => { )} {/* 마커가 하나 이상이면 투표 바텀 시트 표시 */} - {markers.length > 0 && !isCommentMode && !isCommentOpen && ( + {candidatePlacesCount && candidatePlacesCount > 0 && !isCommentMode && !isCommentOpen && ( setIsSheetOpen(false)} - count={markers.length} + count={candidatePlacesCount} promiseId={promiseId} promise={promise} onGoResult={handleGoVoteResult} - markers={markers} + candidatesPlaces={candidatePlaces ?? []} votedPlaces={[...votedPlaces]} votedPlace={votedPlace} /> @@ -319,19 +324,11 @@ const PromiseMap = () => { {/* 장소 선택 시 상세 바텀 시트 */} {selectedPlace && !isCommentMode && ( - pendingPlace && handleToggleAdd(isAdded, pendingPlace) - } - isVoted={isVoted} - onToggleVote={isVotedNext => - pendingPlace && handleToggleVote(isVotedNext, pendingPlace) - } + selectedPlace={selectedPlace} isConfirmed={isConfirmed} /> )} diff --git a/src/pages/map/components/LocationBottomSheet.tsx b/src/pages/map/components/LocationBottomSheet.tsx index cc17855..9c74012 100644 --- a/src/pages/map/components/LocationBottomSheet.tsx +++ b/src/pages/map/components/LocationBottomSheet.tsx @@ -1,39 +1,77 @@ +import axios from "axios"; + import BottomSheet from '@/components/common/BottomSheet'; import MemberIcon from '@/assets/images/memberIcon.svg'; 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 type { CandidatePlace } from '@/types/map/votePlace.type'; interface LocationBottomSheetProps { isOpen: boolean; onClose: () => void; - placeName: string; - address: string; - proposedBy: string; - isAdded: boolean; - onToggleAdd: (isAdded: boolean) => void; - isVoted: boolean; - onToggleVote: (isVoted: boolean) => void; + selectedPlace: { + placeName: string; + address: string; + proposedBy: string; + lat: number; + lng: number; + category?: string; + }; + candidatesPlaces: CandidatePlace[]; isConfirmed: boolean; + promiseId?: string; } const LocationBottomSheet = ({ isOpen, onClose, - placeName, - address, - proposedBy, - isAdded, - onToggleAdd, - isVoted, - onToggleVote, + selectedPlace, + candidatesPlaces, isConfirmed, + promiseId, }: LocationBottomSheetProps) => { - const isError = placeName === '선택한 위치'; + const isError = selectedPlace.placeName === '선택한 위치'; 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 selectedPlaceId = candidatesPlaces.find(candidatePlace => candidatePlace.name === selectedPlace.placeName && candidatePlace.address === selectedPlace.address)?.id; + + const {mutate: addCandidatePlaceMutate} = useAddCandidatePlace(promiseId); + + const addCandidatePlaceHandler = () => { + addCandidatePlaceMutate({ + name: selectedPlace.placeName, + address: selectedPlace.address, + latitude: selectedPlace.lat, + longitude: selectedPlace.lng, + category: selectedPlace.category || "", + }); + }; + + + const {mutate: deleteCandidatePlaceMutate} = useDeleteCandidatePlace(promiseId, selectedPlaceId); + + const deleteCandidatePlaceHandler = () => { + deleteCandidatePlaceMutate(undefined, { + onError: error => { + if (axios.isAxiosError(error)) { + const status = error.response?.status; + if (status === 403) { + alert("후보지는 등록한 본인만 제거할 수 있어요."); + } + } + } + }); + } + + + console.log(selectedPlace); + return (

- {placeName} + {selectedPlace.placeName}

- {proposedBy} 님이 제안 + {selectedPlace.proposedBy} 님이 제안

@@ -93,14 +131,14 @@ const LocationBottomSheet = ({ className={`w-9 h-9 p-1.5 rounded-full cursor-pointer ${ isVoted ? 'bg-[#C6C6C6]' : 'bg-[#00408E]' }`} - onClick={() => onToggleVote(!isVoted)} + onClick={() => {}} >
)}
onToggleAdd(!isAdded)} + onClick={() => isAdded ? deleteCandidatePlaceHandler() : addCandidatePlaceHandler()} >

- {`📍 ${address}\n ⏰ \n💸`} + {`📍 ${selectedPlace.address}\n ⏰ \n💸`}

{/* 확정 시 하단 안내 문구 */} diff --git a/src/pages/map/components/VoteBottomSheet.tsx b/src/pages/map/components/VoteBottomSheet.tsx index 7e3afe4..17f3ad1 100644 --- a/src/pages/map/components/VoteBottomSheet.tsx +++ b/src/pages/map/components/VoteBottomSheet.tsx @@ -1,7 +1,7 @@ -import type { Marker } from '@/types/map'; import VoteStateBox from './VoteStateBox'; import BottomButton from '@/components/common/BottomButton'; import BottomSheet from '@/components/common/BottomSheet'; +import type { CandidatePlace } from '@/types/map/votePlace.type'; interface VoteBottomSheetProps { isOpen: boolean; @@ -10,7 +10,7 @@ interface VoteBottomSheetProps { promiseId: string | undefined; promise: any; // 타입 지정 예정 onGoResult: () => void; // 장소 결정하기 버튼 핸들러 - markers: Marker[]; + candidatesPlaces: CandidatePlace[]; votedPlaces: string[]; // 복수 투표된 장소 키 목록 votedPlace: string | null; // 단일 투표된 장소 키 } @@ -21,20 +21,20 @@ const VoteBottomSheet = ({ count, promise, onGoResult, - markers, + candidatesPlaces, votedPlaces, votedPlace, }: VoteBottomSheetProps) => { // 마커별 득표 수 계산 - const getVoteCount = (marker: Marker): number => { - const key = `${marker.lat}_${marker.lng}`; + const getVoteCount = (candidatePlace: CandidatePlace): number => { + const key = `${candidatePlace.latitude}_${candidatePlace.longitude}`; if (promise?.isMultipleVoting) { return votedPlaces.filter(k => k === key).length; } return votedPlace === key ? 1 : 0; }; - const maxVote = Math.max(...markers.map(m => getVoteCount(m)), 0); + const maxVote = Math.max(...candidatesPlaces.map(m => getVoteCount(m)), 0); return (
- {markers.map((marker, i) => { - const vote = getVoteCount(marker); + {candidatesPlaces.map((candidatePlace, i) => { + const vote = getVoteCount(candidatePlace); return ( 0} - name={marker.placeName} - distance={marker.address} + name={candidatePlace.name} + distance={candidatePlace.address} vote={vote} /> ); diff --git a/src/pages/map/hooks/useMapSheet.ts b/src/pages/map/hooks/useMapSheet.ts index 9357289..85e4448 100644 --- a/src/pages/map/hooks/useMapSheet.ts +++ b/src/pages/map/hooks/useMapSheet.ts @@ -98,7 +98,7 @@ export const useMapSheet = ({ result[0].road_address?.address_name ?? result[0].address.address_name; findNearestPlace(lat, lng, placeName => { setPendingPlace({ lat, lng, placeName, address }); - setSelectedPlace({ placeName, address, proposedBy: '나' }); + setSelectedPlace({ placeName, address, proposedBy: '나', lat, lng }); setIsSheetOpen(true); }); }); @@ -117,6 +117,8 @@ export const useMapSheet = ({ placeName: marker.placeName, address: marker.address, proposedBy: '나', + lat: marker.lat, + lng: marker.lng, }); setIsSheetOpen(true); setSelectedOverlay(null); diff --git a/src/pages/map/services/useVotePalce.ts b/src/pages/map/services/useVotePalce.ts new file mode 100644 index 0000000..0ebd5a0 --- /dev/null +++ b/src/pages/map/services/useVotePalce.ts @@ -0,0 +1,59 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { addCandidatePlace, deleteCandidatePlace, getCandidatePlaces } from '@/apis/map/votePlace'; +import type { AddCandidatePlaceRequest } from '@/types/map/votePlace.type'; + +// 투표 후보지 목록 조회 +export const useGetCandidatePlaces = (promiseId?: string) => { + return useQuery({ + queryKey: ['candidatePlaces', promiseId], + queryFn: () => { + if (!promiseId) { + return Promise.reject(new Error("Promise ID is required")); + } + return getCandidatePlaces(promiseId); + }, + enabled: !!promiseId, + }); +}; + +// 투표 후보지 추가 +export const useAddCandidatePlace = (promiseId?: string) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (body: AddCandidatePlaceRequest) => { + if (!promiseId) { + return Promise.reject(new Error("Promise ID is required")); + } else { + return addCandidatePlace(promiseId, body); + } + }, + onSuccess: res => { + console.log('투표 후보지 추가 성공: ', res); + queryClient.invalidateQueries({ queryKey: ['candidatePlaces', promiseId] }); + }, + onError: error => { console.error('투표 후보지 추가 실패: ', error); }, + }); +}; + +// 투표 후보지 제거 +export const useDeleteCandidatePlace = (promiseId?: string, candidateId?: number) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => { + if (!promiseId || !candidateId) { + return Promise.reject(new Error("Promise ID is required")); + } else { + return deleteCandidatePlace(promiseId, candidateId); + } + }, + onSuccess: res => { + console.log('투표 후보지 제거 성공: ', res); + queryClient.invalidateQueries({ queryKey: ['candidatePlaces', promiseId] }); + }, + onError: error => { + console.error('투표 후보지 제거 실패: ', error); + }, + }) +} \ No newline at end of file diff --git a/src/types/map.ts b/src/types/map.ts index d0a8038..bb175b5 100644 --- a/src/types/map.ts +++ b/src/types/map.ts @@ -11,6 +11,9 @@ export interface SelectedPlace { placeName: string; address: string; proposedBy: string; + lat: number; + lng: number; + category?: string; } // 투표 결과 카드 상태 diff --git a/src/types/map/votePlace.type.ts b/src/types/map/votePlace.type.ts new file mode 100644 index 0000000..677c8c7 --- /dev/null +++ b/src/types/map/votePlace.type.ts @@ -0,0 +1,36 @@ +// 투표 후보지 리스트 반환 +export interface CandidatePlace { + id: number; + name: string; + category: string; + latitude: number; + longitude: number; + address: string; + distance: number; + isConfirmed: boolean; +}; + +export interface GetCandidatePlacesResponse { + candidates: CandidatePlace[]; + count: number; +}; + +// 투표 후보지 추가 +export interface AddCandidatePlaceRequest { + name: string; + address: string; + latitude: number; + longitude: number; + category: string; +}; + +export interface AddCandidatePlaceResponse { + id: number; + name: string; + category: string; + latitude: number; + longitude: number; + address: string; + distance: number; + isConfirmed: boolean; +};