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
38 changes: 38 additions & 0 deletions src/apis/apiHooks/usePromiseInvite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useMutation } from "@tanstack/react-query";
import { postJoinPromise, postPromiseInviteCode } from "../promise/invite";

// 약속 초대 코드 생성
export const usePostPromiseInviteCode = () => {
return useMutation({
mutationFn: async (promiseId: string) => {
const response = await postPromiseInviteCode(promiseId);
return response.data;
},
onSuccess: response => {
const inviteCode = response.inviteCode;
const promiseId = response.promiseId;
const invitationLink = `${window.location.origin}/map/${promiseId}?inviteCode=${inviteCode}`;
navigator.clipboard.writeText(invitationLink);
alert("초대 링크가 복사되었습니다.");
},
onError: error => {
console.error('약속 초대 코드 생성 실패: ', error);
alert("초대 링크 복사에 실패했습니다.");
}
});
};

// 초대 코드를 통한 약속 참여 요청
export const usePostJoinPromise = () => {
return useMutation({
mutationFn: (inviteCode: string) => postJoinPromise(inviteCode),
onSuccess: () => {
console.log('약속 참여 성공');
sessionStorage.removeItem('inviteCode');
sessionStorage.removeItem('redirectPage');
},
onError: (error) => {
console.error('약속 참여 실패: ', error);
}
});
};
15 changes: 15 additions & 0 deletions src/apis/promise/invite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { instance } from '@/apis/instance';
import type { ApiEnvelope, ApiEnvelopeNullable } from '@/types/api.type';
import type { PostPromiseInviteCodeResponse } from '@/types/promise/invite.type';

// 약속 초대 코드 생성
export const postPromiseInviteCode = async (promiseId: string): Promise<ApiEnvelope<PostPromiseInviteCodeResponse>> => {
const response = await instance.post(`/promises/${promiseId}/invite`);
return response.data;
};

// 초대 코드를 통한 약속 참여 요청
export const postJoinPromise = async (inviteCode: string): Promise<ApiEnvelopeNullable<null>> => {
const response = await instance.post(`/promises/invite/${inviteCode}`);
return response.data;
};
4 changes: 4 additions & 0 deletions src/pages/home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ const Home = () => {
{activePromises.map(promise => (
<PromiseCard
key={promise.id}
promiseId={promise.id}
promiseStatus={promise.promiseStatus}
title={promise.title}
date={formatDate(promise.promisedAt, promise.dayOfWeek)}
memberCount={promise.memberCount}
isLeader={promise.isLeader}
onClick={() =>
navigate(`/map/${promise.id}`, { state: { promise } })
}
Expand All @@ -62,10 +64,12 @@ const Home = () => {
{pastPromises.map(promise => (
<PromiseCard
key={promise.id}
promiseId={promise.id}
promiseStatus={promise.promiseStatus}
title={promise.title}
date={formatDate(promise.promisedAt, promise.dayOfWeek)}
memberCount={promise.memberCount}
isPast={true}
onClick={() =>
navigate(`/map/${promise.id}`, { state: { promise } })
}
Expand Down
38 changes: 31 additions & 7 deletions src/pages/home/components/PromiseCard.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
import { usePostPromiseInviteCode } from '@/apis/apiHooks/usePromiseInvite';
import CardMemberIcon from '@/assets/images/cardMemberIcon.svg';
import CardDetailButtonIcon from '@/assets/images/home/cardDetailButtonIcon.svg';
import MemberInvitePlusIcon from '@/assets/images/home/memberInvitePlusIcon.svg';
import PromiseStatusBadge from '@/components/common/PromiseStatusBadge';

interface PromiseCardProps {
promiseId: number;
promiseStatus: string;
title: string;
date: string;
memberCount: number;
isLeader?: boolean;
isPast?: boolean;
onClick?: () => void;
}

const PromiseCard = ({
promiseId,
promiseStatus,
title,
date,
memberCount,
isLeader,
isPast,
onClick,
}: PromiseCardProps) => {
const isCompleted = promiseStatus === '확정 완료';
const isCompleted = promiseStatus === '확정 완료' || isPast;

const { mutate: postPromiseInviteCodeHandler } = usePostPromiseInviteCode();

return (
<div
Expand All @@ -40,12 +49,14 @@ const PromiseCard = ({
{!isCompleted &&
(memberCount === 1 ? (
<div className="flex w-full justify-between gap-15">
<div className="flex items-center p-1.5 w-full bg-[#EAF2FF] border border-[#C0D7FD] rounded-[10px]">
<img src={MemberInvitePlusIcon} />
<p className="text-[#00408E] font-Pretendard font-regular text-[0.75rem] leading-4.2">
친구를 초대하기
</p>
</div>
{isLeader && (
<div onClick={(e) => {e.stopPropagation(); postPromiseInviteCodeHandler(promiseId.toString())}} className="flex items-center p-1.5 bg-[#EAF2FF] border border-[#C0D7FD] rounded-[10px]">
<img src={MemberInvitePlusIcon} />
<p className="text-[#00408E] font-Pretendard font-regular text-[0.75rem] leading-4.2">
친구를 초대하기
</p>
</div>
)}
<div className="w-7.5 h-7.5 shrink-0 flex items-center justify-center bg-[#E4E4E4] rounded-full">
<img src={CardDetailButtonIcon} />
</div>
Expand All @@ -63,6 +74,19 @@ const PromiseCard = ({
</div>
</div>
))}
{isPast && (
<div className="flex justify-between items-center">
<div className="flex items-center gap-1.25">
<img src={CardMemberIcon} />
<p className="text-[#111111] font-Pretendard font-regular text-[0.875rem] leading-5">
{memberCount}명
</p>
</div>
<div className="w-7.5 h-7.5 flex items-center justify-center bg-[#E4E4E4] rounded-full">
<img src={CardDetailButtonIcon} />
</div>
</div>
)}
</div>
);
};
Expand Down
9 changes: 9 additions & 0 deletions src/pages/login/components/OAuthCallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ const OAuthCallback = () => {
}

login(accessToken);

// 초대 링크로 왔었으면
const inviteCode = sessionStorage.getItem('inviteCode');
const redirectPage = sessionStorage.getItem('redirectPage');

if (inviteCode && redirectPage) {
navigate(`${redirectPage}?inviteCode=${inviteCode}`, { replace: true });
return;
}
navigate('/home', { replace: true });
}, []);

Expand Down
60 changes: 49 additions & 11 deletions src/pages/map/PromiseMap.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useEffect, useRef, useState } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import {
CustomOverlayMap,
Map,
Expand All @@ -20,6 +20,8 @@ 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';
import { useAuthStore } from '@/stores/authStore';
import { usePostJoinPromise } from '@/apis/apiHooks/usePromiseInvite';

const PromiseMap = () => {
const [bounds, setBounds] = useState<GetCommentsParams>({
Expand All @@ -29,11 +31,31 @@ const PromiseMap = () => {
maxLng: 127.04,
});

const [searchParams] = useSearchParams();
const inviteCode = searchParams.get('inviteCode');
const navigate = useNavigate();
const { promiseId } = useParams();
const parsedPromiseId = Number(promiseId);
const { data: promise } = usePromiseDetail(parsedPromiseId);

const {accessToken} = useAuthStore();

const { mutate: joinPromise } = usePostJoinPromise();

useEffect(() => {
if (!inviteCode) return; // inviteCode 없으면 그냥 일반 접근

if (!accessToken) {
// 로그인 안 됐으면 저장하고 로그인 페이지로
sessionStorage.setItem('inviteCode', inviteCode);
sessionStorage.setItem('redirectPage', `/map/${promiseId}`);
navigate('/login');
return;
}

joinPromise(inviteCode);
}, [inviteCode, accessToken]);

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

Expand Down Expand Up @@ -70,19 +92,34 @@ const PromiseMap = () => {
openCommentId,
});

const [showToast, setShowToast] = useState(true);
const [showToast, setShowToast] = useState(false);
const hasCheckedInitial = useRef(false);
const [isCardExpanded, setIsCardExpanded] = useState(false);

useEffect(() => {
const timer = setTimeout(() => setShowToast(false), 2000);
return () => clearTimeout(timer);
}, []);

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

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

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

useEffect(() => {
if (!candidatePlacesResponse) return;
if (hasCheckedInitial.current) return;

hasCheckedInitial.current = true;

if (candidatePlacesCount === 0) {
setShowToast(true);
}
}, [candidatePlacesResponse, candidatePlacesCount]);

useEffect(() => {
if (!showToast) return;
const timer = setTimeout(() => setShowToast(false), 2000);
return () => clearTimeout(timer);
}, [showToast]);

const handleGoVoteResult = () => {
navigate(`/map/${promiseId}/vote`, {
Expand All @@ -96,6 +133,7 @@ const PromiseMap = () => {
};

if (!promise) return null;

const isLeader = promise.isLeader;

return (
Expand Down Expand Up @@ -319,12 +357,12 @@ const PromiseMap = () => {
)}

{/* 마커 없을 때 안내 토스트 */}
{showToast && isOnline && !isCommentMode && (
{showToast && isOnline && candidatePlacesCount === 0 && (
<>
<div className="fixed inset-x-0 top-0 bottom-24 bg-[rgba(17,17,17,0.40)] backdrop-blur-sm flex items-center justify-center z-50" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-50">
<ToastMessage
title="장소가 없어요"
title="추가한 장소가 없어요"
subTitle="지도를 눌러 추가해 보세요"
/>
</div>
Expand Down
5 changes: 2 additions & 3 deletions src/pages/map/VoteResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ import CandidateVoteMemberIcon from '@/assets/images/candidateVoteMemberIcon.svg
import RevoteMessageIcon from '@/assets/images/revoteMessageIcon.svg';

const VoteResult = () => {
const isCreator = false; // 실제 생성자 구분 교체 예정

const navigate = useNavigate();
const { state } = useLocation();
const promise = state?.promise;
const markers = state?.markers ?? [];
const markers = state?.candidatesPlaces ?? [];
const votedPlaces: string[] = state?.votedPlaces ?? [];
const votedPlace: string | null = state?.votedPlace ?? null;
const isCreator = promise?.isLeader ?? false;

const {
sortedCandidates,
Expand Down
1 change: 1 addition & 0 deletions src/pages/promise/Promise.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const Promise = () => {
{upcomingConfirmedPromises.map(promise => (
<PromiseCard
key={promise.id}
promiseId={promise.id}
promiseStatus={promise.promiseStatus}
title={promise.title}
date={formatDate(promise.promisedAt, promise.dayOfWeek)}
Expand Down
5 changes: 4 additions & 1 deletion src/stores/authStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ export const useAuthStore = create<AuthState>()(
set => ({
accessToken: null,
login: accessToken => set({ accessToken }),
logout: () => set({ accessToken: null }),
logout: () => {
set({ accessToken: null });
sessionStorage.removeItem('auth');
},
}),
{
name: 'auth',
Expand Down
2 changes: 1 addition & 1 deletion src/types/map/votePlace.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface CandidatePlace {

export interface GetCandidatePlacesResponse {
candidates: CandidatePlace[];
count: number;
candidateCount: number;
};

// 투표 후보지 추가
Expand Down
5 changes: 5 additions & 0 deletions src/types/promise/invite.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 초대 코드 생성 응답 데이터
export interface PostPromiseInviteCodeResponse {
promiseId: number;
inviteCode: string;
}
1 change: 1 addition & 0 deletions src/types/promise/promise.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface PromiseItem {
promisedAt: string;
dayOfWeek: string;
memberCount: number;
isLeader: boolean;
}

// 약속 목록 조회 요청 파라미터
Expand Down
Loading