From 942cffab904fb39dfbac81bc775d2ce8ccc98c70 Mon Sep 17 00:00:00 2001 From: Jun279 <33.beautifulboy@gmail.com> Date: Fri, 8 May 2026 19:28:26 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=EC=95=BD=EC=86=8D=20=EC=B4=88=EB=8C=80=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=83=9D=EC=84=B1=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/apiHooks/usePromiseInvite.ts | 23 ++++++++++++++ src/apis/promise/invite.ts | 9 ++++++ src/pages/home/Home.tsx | 4 +++ src/pages/home/components/PromiseCard.tsx | 38 ++++++++++++++++++----- src/pages/map/PromiseMap.tsx | 4 +-- src/types/promise/invite.type.ts | 5 +++ src/types/promise/promise.type.ts | 1 + 7 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 src/apis/apiHooks/usePromiseInvite.ts create mode 100644 src/apis/promise/invite.ts create mode 100644 src/types/promise/invite.type.ts diff --git a/src/apis/apiHooks/usePromiseInvite.ts b/src/apis/apiHooks/usePromiseInvite.ts new file mode 100644 index 0000000..b4e3190 --- /dev/null +++ b/src/apis/apiHooks/usePromiseInvite.ts @@ -0,0 +1,23 @@ +import { useMutation } from "@tanstack/react-query"; +import { 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("초대 링크 복사에 실패했습니다."); + } + }); +}; \ No newline at end of file diff --git a/src/apis/promise/invite.ts b/src/apis/promise/invite.ts new file mode 100644 index 0000000..71e5102 --- /dev/null +++ b/src/apis/promise/invite.ts @@ -0,0 +1,9 @@ +import { instance } from '@/apis/instance'; +import type { ApiEnvelope } from '@/types/api.type'; +import type { PostPromiseInviteCodeResponse } from '@/types/promise/invite.type'; + +// 약속 초대 코드 생성 +export const postPromiseInviteCode = async (promiseId: string): Promise> => { + const response = await instance.post(`/promises/${promiseId}/invite`); + return response.data; +}; diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx index eefb629..d0abf65 100644 --- a/src/pages/home/Home.tsx +++ b/src/pages/home/Home.tsx @@ -42,10 +42,12 @@ const Home = () => { {activePromises.map(promise => ( navigate(`/map/${promise.id}`, { state: { promise } }) } @@ -62,10 +64,12 @@ const Home = () => { {pastPromises.map(promise => ( navigate(`/map/${promise.id}`, { state: { promise } }) } diff --git a/src/pages/home/components/PromiseCard.tsx b/src/pages/home/components/PromiseCard.tsx index afbfa2c..396f61c 100644 --- a/src/pages/home/components/PromiseCard.tsx +++ b/src/pages/home/components/PromiseCard.tsx @@ -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 (
-
- -

- 친구를 초대하기 -

-
+ {isLeader && ( +
{e.stopPropagation(); postPromiseInviteCodeHandler(promiseId.toString())}} className="flex items-center p-1.5 bg-[#EAF2FF] border border-[#C0D7FD] rounded-[10px]"> + +

+ 친구를 초대하기 +

+
+ )}
@@ -63,6 +74,19 @@ const PromiseCard = ({
))} + {isPast && ( +
+
+ +

+ {memberCount}명 +

+
+
+ +
+
+ )} ); }; diff --git a/src/pages/map/PromiseMap.tsx b/src/pages/map/PromiseMap.tsx index 1d6470b..86eea25 100644 --- a/src/pages/map/PromiseMap.tsx +++ b/src/pages/map/PromiseMap.tsx @@ -80,8 +80,6 @@ const PromiseMap = () => { return () => clearTimeout(timer); }, []); - if (!promise) return null; - const isConfirmed = false; // 확정된 장소 예정 const {data: candidatePlacesResponse} = useGetCandidatePlaces(promiseId); @@ -94,6 +92,8 @@ const PromiseMap = () => { }); }; + if (!promise) return null; + return (
Date: Sun, 10 May 2026 01:53:43 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=E2=9C=A8=20=EC=83=9D=EC=84=B1=EC=9E=90=20?= =?UTF-8?q?=EA=B5=AC=EB=B6=84=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/map/VoteResult.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/map/VoteResult.tsx b/src/pages/map/VoteResult.tsx index 618beb2..bbd4c7c 100644 --- a/src/pages/map/VoteResult.tsx +++ b/src/pages/map/VoteResult.tsx @@ -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, From 916967be4549565307132ea3c7f0559d9a9c258a Mon Sep 17 00:00:00 2001 From: a-neey <2371429@hansung.ac.kr> Date: Sun, 10 May 2026 02:47:16 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=94=A5=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=84=B8=EC=85=98=20=EC=8A=A4=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=EC=A7=80=20=EB=B9=84=EC=9A=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stores/authStore.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts index 4c43fbb..43c1495 100644 --- a/src/stores/authStore.ts +++ b/src/stores/authStore.ts @@ -12,7 +12,10 @@ export const useAuthStore = create()( set => ({ accessToken: null, login: accessToken => set({ accessToken }), - logout: () => set({ accessToken: null }), + logout: () => { + set({ accessToken: null }); + sessionStorage.removeItem('auth'); + }, }), { name: 'auth', From 0b0c58eda9ceddbf1dfdd45cb23cf0da7e0960f2 Mon Sep 17 00:00:00 2001 From: Jun279 <33.beautifulboy@gmail.com> Date: Mon, 11 May 2026 14:09:49 +0900 Subject: [PATCH 4/5] =?UTF-8?q?Feat:=20=EC=95=BD=EC=86=8D=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EA=B4=80=EB=A0=A8=20=ED=86=B5=EC=8B=A0=20=EC=9E=91?= =?UTF-8?q?=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/apiHooks/usePromiseInvite.ts | 19 +++++- src/apis/promise/invite.ts | 8 ++- src/pages/login/components/OAuthCallback.tsx | 9 +++ src/pages/map/PromiseMap.tsx | 61 ++++++++++++++++---- src/types/map/votePlace.type.ts | 2 +- 5 files changed, 83 insertions(+), 16 deletions(-) diff --git a/src/apis/apiHooks/usePromiseInvite.ts b/src/apis/apiHooks/usePromiseInvite.ts index b4e3190..aadc5ae 100644 --- a/src/apis/apiHooks/usePromiseInvite.ts +++ b/src/apis/apiHooks/usePromiseInvite.ts @@ -1,5 +1,5 @@ import { useMutation } from "@tanstack/react-query"; -import { postPromiseInviteCode } from "../promise/invite"; +import { postJoinPromise, postPromiseInviteCode } from "../promise/invite"; // 약속 초대 코드 생성 export const usePostPromiseInviteCode = () => { @@ -20,4 +20,19 @@ export const usePostPromiseInviteCode = () => { alert("초대 링크 복사에 실패했습니다."); } }); -}; \ No newline at end of file +}; + +// 초대 코드를 통한 약속 참여 요청 +export const usePostJoinPromise = () => { + return useMutation({ + mutationFn: (inviteCode: string) => postJoinPromise(inviteCode), + onSuccess: () => { + console.log('약속 참여 성공'); + sessionStorage.removeItem('inviteCode'); + sessionStorage.removeItem('redirectPage'); + }, + onError: (error) => { + console.error('약속 참여 실패: ', error); + } + }); +}; diff --git a/src/apis/promise/invite.ts b/src/apis/promise/invite.ts index 71e5102..5772701 100644 --- a/src/apis/promise/invite.ts +++ b/src/apis/promise/invite.ts @@ -1,5 +1,5 @@ import { instance } from '@/apis/instance'; -import type { ApiEnvelope } from '@/types/api.type'; +import type { ApiEnvelope, ApiEnvelopeNullable } from '@/types/api.type'; import type { PostPromiseInviteCodeResponse } from '@/types/promise/invite.type'; // 약속 초대 코드 생성 @@ -7,3 +7,9 @@ export const postPromiseInviteCode = async (promiseId: string): Promise> => { + const response = await instance.post(`/promises/invite/${inviteCode}`); + return response.data; +}; diff --git a/src/pages/login/components/OAuthCallback.tsx b/src/pages/login/components/OAuthCallback.tsx index eba3972..d7da184 100644 --- a/src/pages/login/components/OAuthCallback.tsx +++ b/src/pages/login/components/OAuthCallback.tsx @@ -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 }); }, []); diff --git a/src/pages/map/PromiseMap.tsx b/src/pages/map/PromiseMap.tsx index e48e209..7288f1a 100644 --- a/src/pages/map/PromiseMap.tsx +++ b/src/pages/map/PromiseMap.tsx @@ -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, @@ -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({ @@ -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; @@ -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`, { @@ -96,7 +133,7 @@ const PromiseMap = () => { }; if (!promise) return null; - + const isLeader = promise.isLeader; return ( @@ -320,12 +357,12 @@ const PromiseMap = () => { )} {/* 마커 없을 때 안내 토스트 */} - {showToast && isOnline && !isCommentMode && ( + {showToast && isOnline && candidatePlacesCount === 0 && ( <>
diff --git a/src/types/map/votePlace.type.ts b/src/types/map/votePlace.type.ts index 677c8c7..1a7bfad 100644 --- a/src/types/map/votePlace.type.ts +++ b/src/types/map/votePlace.type.ts @@ -12,7 +12,7 @@ export interface CandidatePlace { export interface GetCandidatePlacesResponse { candidates: CandidatePlace[]; - count: number; + candidateCount: number; }; // 투표 후보지 추가 From 96264881d4a1764fd21011b4c280115c0b0f5e74 Mon Sep 17 00:00:00 2001 From: Jun279 <33.beautifulboy@gmail.com> Date: Mon, 11 May 2026 14:17:37 +0900 Subject: [PATCH 5/5] =?UTF-8?q?Fix:=20PromiseCard=20promiseId=20props=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/promise/Promise.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/promise/Promise.tsx b/src/pages/promise/Promise.tsx index 1be21e2..38cbc58 100644 --- a/src/pages/promise/Promise.tsx +++ b/src/pages/promise/Promise.tsx @@ -32,6 +32,7 @@ const Promise = () => { {upcomingConfirmedPromises.map(promise => (