Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/apis/map/votePlace.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { instance } from '../instance';
import type { ApiEnvelope, ApiEnvelopeNullable } from '@/types/api.type';
import type { AddCandidatePlaceRequest, AddCandidatePlaceResponse, GetCandidatePlacesResponse, PostVoteRequest } from '@/types/map/votePlace.type';
import type { AddCandidatePlaceRequest, AddCandidatePlaceResponse, ConfirmPlaceRequest, GetCandidatePlacesResponse, PostVoteRequest } from '@/types/map/votePlace.type';

// 투표 후보지 목록 조회
export const getCandidatePlaces = async (promiseId: string): Promise<ApiEnvelope<GetCandidatePlacesResponse>> => {
Expand Down Expand Up @@ -31,3 +31,15 @@ export const deleteVote = async (promiseId: string, candidateId: number): Promis
const response = await instance.delete(`/promises/${promiseId}/votes/${candidateId}`);
return response.data;
};

// 장소 확정
export const confirmPlace = async (promiseId: string, body: ConfirmPlaceRequest): Promise<ApiEnvelopeNullable<null>> => {
const response = await instance.post(`/promises/${promiseId}/confirmed`, body);
return response.data;
};

// 재투표
export const postRevote = async (promiseId: string): Promise<ApiEnvelopeNullable<null>> => {
const response = await instance.post(`/promises/${promiseId}/revote`);
return response.data;
};
42 changes: 25 additions & 17 deletions src/pages/map/ConfirmedResult.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import { useState } from 'react';
import { useLocation } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import CandidatesCard from './components/CandidatesCard';
import CommonModal from '@/components/modal/CommonModal';
import PromiseStatusBadge from '@/components/common/PromiseStatusBadge';
import Header from '@/components/layout/Header';
import { usePromiseDetail } from './services/usePromiseDetail';
import { useGetCandidatePlaces } from './services/useVotePalce';

const ConfirmedResult = () => {
const { state } = useLocation();
const promise = state?.promise;
const candidate = state?.confirmedCandidate;
const { promiseId } = useParams();
const parsedPromiseId = Number(promiseId);
const { data: promise } = usePromiseDetail(parsedPromiseId);
const { data: candidatePlacesResponse } = useGetCandidatePlaces(promiseId);
const candidate = candidatePlacesResponse?.data.candidates.find(c => c.isConfirmed);

const [isCalendarModalOpen, setIsCalendarModalOpen] = useState(false);

// 약속 날짜가 오늘인지 여부
const today =
new Date().toDateString() === new Date(promise.date).toDateString();
if (!promise || !candidate) return null;

const today = new Date().toDateString() === new Date(promise.promisedAt).toDateString();
const time = new Date(promise.promisedAt).toLocaleTimeString('ko-KR', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
});

return (
<div className="flex flex-col gap-3">
Expand Down Expand Up @@ -42,7 +51,7 @@ const ConfirmedResult = () => {
{promise.dayOfWeek}요일 {promise.title}
</p>
<p className="text-[#FFFFFF] font-Pretendard font-regular text-[1rem] leading-4">
19:00
{time}
</p>
</div>
</div>
Expand All @@ -56,9 +65,12 @@ const ConfirmedResult = () => {
name={candidate.name}
distance={candidate.distance}
address={candidate.address}
createMember={candidate.createMember}
voteMember={candidate.voteMember}
voteCount={candidate.voteCount}
createMember={candidate.voteInfo.creator.nickname}
voteMember={candidate.voteInfo.voters
.filter(v => v.userId !== candidate.voteInfo.creator.userId)
.map(v => v.nickname)
.join(', ')}
voteCount={candidate.voteInfo.voteCount}
memberCount={promise.memberCount}
/>
</div>
Expand All @@ -76,7 +88,6 @@ const ConfirmedResult = () => {
) : (
<button
className="flex-1 py-4 border border-[#C6C6C6] rounded-[10px] bg-[#FFFFFF] active:bg-[#00408E]"
// 예약 확인 연결 예정
>
<p className="text-[#111111] font-Pretendard font-normal text-[1rem] leading-4 active:text-[#FFFFFF]">
예약 확인
Expand All @@ -86,7 +97,6 @@ const ConfirmedResult = () => {

<button
className="flex-1 py-4 border border-[#C6C6C6] rounded-[10px] bg-[#FFFFFF] active:bg-[#00408E]"
// 길찾기 링크 연결 예정
>
<p className="text-[#111111] font-Pretendard font-normal text-[1rem] leading-4 active:text-[#FFFFFF]">
길찾기
Expand All @@ -103,9 +113,7 @@ const ConfirmedResult = () => {
subText="약속을 캘린더에 저장하려면 설정에서 권한을 허용해 주세요"
confirmText="설정 열기"
closeText="취소"
onConfirm={() => {
setIsCalendarModalOpen(true);
}}
onConfirm={() => setIsCalendarModalOpen(true)}
onClose={() => setIsCalendarModalOpen(false)}
/>
</div>
Expand All @@ -114,4 +122,4 @@ const ConfirmedResult = () => {
);
};

export default ConfirmedResult;
export default ConfirmedResult;
12 changes: 2 additions & 10 deletions src/pages/map/PromiseMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import CommentBottomSheet from './components/CommentBottomSheet';
import LocationMarker from './components/LocationMarker';
import ToastMessage from '@/components/common/ToastMessage';
import { useMapLocation } from './hooks/useMapLocation';
import { useVoteState } from './hooks/useVoteState';
import { useMapSheet } from './hooks/useMapSheet';
import { useComment } from './hooks/useComment';
import type { GetCommentsParams } from '@/types/map/comment.type';
Expand Down Expand Up @@ -57,9 +56,6 @@ const PromiseMap = () => {
}, [inviteCode, accessToken]);

const { center, isOnline } = useMapLocation();
const isMultipleVoting = promise?.isMultipleVoting ?? false;

const { votedPlace, votedPlaces } = useVoteState({ isMultipleVoting });

const {
isCommentMode,
Expand Down Expand Up @@ -96,13 +92,13 @@ const PromiseMap = () => {
const hasCheckedInitial = useRef(false);
const [isCardExpanded, setIsCardExpanded] = useState(false);

const isConfirmed = false; // 확정된 장소 예정

const { data: candidatePlacesResponse } = useGetCandidatePlaces(promiseId);
const candidatePlaces = candidatePlacesResponse?.data.candidates;
const candidatePlacesCount = candidatePlacesResponse?.data.candidateCount;

console.log('candidatePlacesResponse: ', candidatePlacesResponse);

const isConfirmed = candidatePlaces?.some(c => c.isConfirmed) ?? false;

useEffect(() => {
if (!candidatePlacesResponse) return;
Expand Down Expand Up @@ -384,12 +380,8 @@ const PromiseMap = () => {
isOpen={!isSheetOpen}
onClose={() => setIsSheetOpen(false)}
count={candidatePlacesCount}
promiseId={promiseId}
promise={promise}
onGoResult={handleGoVoteResult}
candidatesPlaces={candidatePlaces ?? []}
votedPlaces={[...votedPlaces]}
votedPlace={votedPlace}
/>
)}

Expand Down
12 changes: 6 additions & 6 deletions src/pages/map/VoteResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ const VoteResult = () => {
handleVoteSubmit,
handleVoteCancel,
handleSelect,
handleConfirm
} = useVoteResult({
candidatePlaces,
isMultipleVoting: promise?.isMultipleVoting ?? true,
isCreator,
promiseId,
});

return (
<div className="flex flex-col gap-3">
<Header title="장소 결정" />
<Header title={isCreator ? '장소 결정' : '장소 투표'} />
<div className="flex flex-col gap-5">
<div className="flex justify-between px-4">
<div className="flex flex-col gap-1">
Expand Down Expand Up @@ -117,11 +117,11 @@ const VoteResult = () => {
questionText="이 장소를 확정할까요?"
mainText={confirmedCandidate.name}
subText="확정 시 모든 멤버에게 알림이 전송됩니다"
onConfirm={() => {
onConfirm={async () => {
if (!confirmedCandidate) return;
await handleConfirm(confirmedCandidate.id);
setIsConfirmModalOpen(false);
navigate(`/map/${promise?.id}/confirmed`, {
state: { promise, confirmedCandidate },
});
navigate(`/map/${promiseId}/confirmed`);
}}
onClose={() => setIsConfirmModalOpen(false)}
confirmText="확정하기"
Expand Down
20 changes: 2 additions & 18 deletions src/pages/map/components/VoteBottomSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,18 @@ interface VoteBottomSheetProps {
isOpen: boolean;
onClose: () => void;
count: number; // 투표 중인 장소 수
promiseId: string | undefined;
promise: any; // 타입 지정 예정
onGoResult: () => void; // 장소 결정하기 버튼 핸들러
candidatesPlaces: CandidatePlace[];
votedPlaces: string[]; // 복수 투표된 장소 키 목록
votedPlace: string | null; // 단일 투표된 장소 키
}

const VoteBottomSheet = ({
isOpen,
onClose,
count,
promise,
onGoResult,
candidatesPlaces,
votedPlaces,
votedPlace,
}: VoteBottomSheetProps) => {
// 마커별 득표 수 계산
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(...candidatesPlaces.map(m => getVoteCount(m)), 0);
const maxVote = Math.max(...candidatesPlaces.map(c => c.voteInfo.voteCount), 0);

return (
<BottomSheet
Expand All @@ -48,7 +32,7 @@ const VoteBottomSheet = ({
</p>
<div className="flex flex-col overflow-y-auto min-h-45 max-h-45 gap-2.5">
{candidatesPlaces.map((candidatePlace, i) => {
const vote = getVoteCount(candidatePlace);
const vote = candidatePlace.voteInfo.voteCount;
return (
<VoteStateBox
key={i}
Expand Down
64 changes: 43 additions & 21 deletions src/pages/map/hooks/useVoteResult.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { useState } from 'react';
import { usePostVote, useDeleteVote } from '../services/useVotePalce';
import { usePostVote, useDeleteVote, useConfirmPlace, usePostRevote } from '../services/useVotePalce';
import type { CandidatePlace } from '@/types/map/votePlace.type';
import type { Candidate, CardStatus } from '@/types/map';

interface UseVoteResultProps {
candidatePlaces: CandidatePlace[];
isMultipleVoting: boolean;
isCreator: boolean;
promiseId?: string;
}

Expand All @@ -17,14 +16,19 @@ export const useVoteResult = ({
}: UseVoteResultProps) => {
const { mutateAsync: postVote } = usePostVote(promiseId);
const { mutateAsync: deleteVote } = useDeleteVote(promiseId);
const { mutateAsync: confirmPlace } = useConfirmPlace(promiseId);
const { mutateAsync: revote } = usePostRevote(promiseId);

const candidates: Candidate[] = candidatePlaces.map(c => ({
id: c.id,
name: c.name,
distance: c.distance,
address: c.address,
createMember: c.voteInfo.creator.nickname,
voteMember: c.voteInfo.voters.map(v => v.nickname).join(', '),
voteMember: c.voteInfo.voters
.filter(v => v.userId !== c.voteInfo.creator.userId)
.map(v => v.nickname)
.join(', '),
voteCount: c.voteInfo.voteCount,
}));

Expand All @@ -40,41 +44,50 @@ export const useVoteResult = ({

const [isRevote, setIsRevote] = useState(false);
const isRevoteTie = false;
const [hasVoted, setHasVoted] = useState(false);
const [myVote, setMyVote] = useState<number[]>([]);

// isMyVote로 초기 투표 상태 세팅
const initialMyVote = candidatePlaces
.filter(c => c.voteInfo.isMyVote)
.map(c => c.id);

const [hasVoted, setHasVoted] = useState(initialMyVote.length > 0);
const [myVote, setMyVote] = useState<number[]>(initialMyVote);
const [previousVote, setPreviousVote] = useState<number[]>(initialMyVote);
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
const [confirmedCandidate, setConfirmedCandidate] =
useState<Candidate | null>(null);
const [confirmedCandidate, setConfirmedCandidate] = useState<Candidate | null>(null);

const handleSelect = (id: number) => {
if (isMultipleVoting) {
setMyVote(prev =>
prev.includes(id) ? prev.filter(v => v !== id) : [...prev, id],
);
setMyVote(prev => prev.includes(id) ? prev.filter(v => v !== id) : [...prev, id]);
} else {
setMyVote(prev => (prev.includes(id) ? [] : [id]));
setMyVote(prev => prev.includes(id) ? [] : [id]);
}
};

// 단일/복수 투표 제출
// 기존 투표 취소 후 새로 투표
const handleVoteSubmit = async () => {
if (myVote.length === 0) return;
try {
if (previousVote.length > 0) {
await Promise.all(previousVote.map(id => deleteVote(id)));
}
await Promise.all(myVote.map(id => postVote({ candidateId: id })));
setPreviousVote(myVote);
setHasVoted(true);
} catch {
// 에러는 usePostVote onError에서 처리
// 에러는 usePostVote onError에서 처리
}
};

// 단일/복수 투표 취소
// 투표 취소
const handleVoteCancel = async () => {
try {
await Promise.all(myVote.map(id => deleteVote(id)));
setPreviousVote([]);
setHasVoted(false);
setMyVote([]);
} catch {
// 에러는 useDeleteVote onError에서 처리
// 에러는 useDeleteVote onError에서 처리
}
};

Expand All @@ -96,19 +109,27 @@ export const useVoteResult = ({

const buttonDisabled = isRevote ? false : !hasVote;

const handleConfirmClick = () => {
const handleConfirmClick = async () => {
if (isTie && !isRevote && !isRevoteTie) {
await revote(); // 재투표
setIsRevote(true);
} else {
const target =
myVote.length > 0
? candidates.find(c => c.id === myVote[0])
: topCandidates[0];
const target = myVote.length > 0
? candidates.find(c => c.id === myVote[0])
: topCandidates[0];
setConfirmedCandidate(target ?? null);
setIsConfirmModalOpen(true);
}
};

// 모달에서 확정하기 누를 때 호출할 함수
const handleConfirm = async (candidateId: number) => {
try {
await confirmPlace({ candidateId });
} catch {}
};


return {
sortedCandidates,
myVote,
Expand All @@ -125,5 +146,6 @@ export const useVoteResult = ({
handleVoteSubmit,
handleVoteCancel,
handleSelect,
handleConfirm
};
};
};
Loading
Loading