From 5894cb7714fb287fb78e4f17de97c0f8ed0056e6 Mon Sep 17 00:00:00 2001 From: Jun279 <33.beautifulboy@gmail.com> Date: Wed, 6 May 2026 22:32:54 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Feat:=20=EC=95=BD=EC=86=8D=20=EC=9E=A5?= =?UTF-8?q?=EC=86=8C=20=ED=88=AC=ED=91=9C=20=ED=9B=84=EB=B3=B4=EC=A7=80=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=86=B5=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/map/votePlace.ts | 21 +++++ src/pages/map/PromiseMap.tsx | 68 ++++++++-------- .../map/components/LocationBottomSheet.tsx | 78 ++++++++++++++----- src/pages/map/components/VoteBottomSheet.tsx | 19 ++--- src/pages/map/hooks/useMapSheet.ts | 4 +- src/pages/map/services/useVotePalce.ts | 59 ++++++++++++++ src/types/map.ts | 3 + src/types/map/votePlace.type.ts | 36 +++++++++ 8 files changed, 223 insertions(+), 65 deletions(-) create mode 100644 src/apis/map/votePlace.ts create mode 100644 src/pages/map/services/useVotePalce.ts create mode 100644 src/types/map/votePlace.type.ts 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 e22f893..6e8ffb5 100644 --- a/src/pages/map/PromiseMap.tsx +++ b/src/pages/map/PromiseMap.tsx @@ -19,6 +19,7 @@ import type { GetCommentsParams } from '@/types/map/comment.type'; import MapMemberIcon from '@/assets/images/map/mapMemberIcon.svg'; import CustomMarkerIcon from '@/assets/images/map/customMarkerIcon.svg'; import CommentIcon from '@/assets/images/map/commentIcon.svg'; +import { useGetCandidatePlaces } from './services/useVotePalce'; const PromiseMap = () => { const [bounds, setBounds] = useState({ @@ -40,9 +41,6 @@ const PromiseMap = () => { markers, votedPlace, votedPlaces, - handleToggleAdd, - handleToggleVote, - getIsVoted, } = useVoteState({ isMultipleVoting }); const { @@ -63,7 +61,6 @@ const PromiseMap = () => { isSheetOpen, setIsSheetOpen, selectedPlace, - pendingPlace, selectedOverlay, setSelectedOverlay, handleMapClick, @@ -86,16 +83,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 +129,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, + })} /> )} @@ -252,7 +258,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); @@ -303,15 +309,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} /> @@ -320,19 +326,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..aa9b739 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, isError: isAddCandidatePlaceError} = 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..e3e0309 100644 --- a/src/pages/map/components/VoteBottomSheet.tsx +++ b/src/pages/map/components/VoteBottomSheet.tsx @@ -2,6 +2,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 +11,7 @@ interface VoteBottomSheetProps { promiseId: string | undefined; promise: any; // 타입 지정 예정 onGoResult: () => void; // 장소 결정하기 버튼 핸들러 - markers: Marker[]; + candidatesPlaces: CandidatePlace[]; votedPlaces: string[]; // 복수 투표된 장소 키 목록 votedPlace: string | null; // 단일 투표된 장소 키 } @@ -21,20 +22,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; +}; From dbef7e39a373d06760932755a8b711b59fc1566b Mon Sep 17 00:00:00 2001 From: Jun279 <33.beautifulboy@gmail.com> Date: Thu, 7 May 2026 15:54:00 +0900 Subject: [PATCH 2/2] =?UTF-8?q?FIX:=20=EC=95=88=EC=93=B0=EB=8A=94=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/map/PromiseMap.tsx | 1 - src/pages/map/components/LocationBottomSheet.tsx | 2 +- src/pages/map/components/VoteBottomSheet.tsx | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/map/PromiseMap.tsx b/src/pages/map/PromiseMap.tsx index 6e8ffb5..a88723f 100644 --- a/src/pages/map/PromiseMap.tsx +++ b/src/pages/map/PromiseMap.tsx @@ -38,7 +38,6 @@ const PromiseMap = () => { const isMultipleVoting = promise?.isMultipleVoting ?? false; const { - markers, votedPlace, votedPlaces, } = useVoteState({ isMultipleVoting }); diff --git a/src/pages/map/components/LocationBottomSheet.tsx b/src/pages/map/components/LocationBottomSheet.tsx index aa9b739..9c74012 100644 --- a/src/pages/map/components/LocationBottomSheet.tsx +++ b/src/pages/map/components/LocationBottomSheet.tsx @@ -41,7 +41,7 @@ const LocationBottomSheet = ({ 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, isError: isAddCandidatePlaceError} = useAddCandidatePlace(promiseId); + const {mutate: addCandidatePlaceMutate} = useAddCandidatePlace(promiseId); const addCandidatePlaceHandler = () => { addCandidatePlaceMutate({ diff --git a/src/pages/map/components/VoteBottomSheet.tsx b/src/pages/map/components/VoteBottomSheet.tsx index e3e0309..17f3ad1 100644 --- a/src/pages/map/components/VoteBottomSheet.tsx +++ b/src/pages/map/components/VoteBottomSheet.tsx @@ -1,4 +1,3 @@ -import type { Marker } from '@/types/map'; import VoteStateBox from './VoteStateBox'; import BottomButton from '@/components/common/BottomButton'; import BottomSheet from '@/components/common/BottomSheet';