Skip to content

Commit 97982c3

Browse files
authored
Merge pull request #219 from prgrms-web-devcourse-final-project/feat/189-SSE-connect
[feat] SSE 연결
2 parents 707ce61 + b9e0354 commit 97982c3

File tree

13 files changed

+435
-35
lines changed

13 files changed

+435
-35
lines changed

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
"@tailwindcss/vite": "^4.0.6",
1515
"@tanstack/react-query": "^5.66.0",
1616
"@tanstack/react-query-devtools": "^5.66.9",
17+
"@types/event-source-polyfill": "^1.0.5",
1718
"@types/node": "^22.13.4",
1819
"@types/youtube": "^0.1.0",
1920
"axios": "^1.7.9",
2021
"dayjs": "^1.11.13",
22+
"event-source-polyfill": "^1.0.31",
2123
"framer-motion": "^12.4.7",
2224
"lodash": "^4.17.21",
2325
"lottie-react": "^2.4.1",

src/App.tsx

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Layout from '@/layouts/Layout';
44
import Landing from '@/pages/landing/Landing';
55
import Modal from '@/components/Modal';
66
import Home from '@/pages/home/Home';
7-
import ChatConnectLoadingSheet from '@/components/ChatConnectLoadingSheet';
7+
import ChatConnectLoadingSheet from '@/components/ChatConnectLoadingSheet/ChatConnectLoadingSheet';
88
import Chat from '@/pages/chat/Chat';
99
import ChatRoom from '@/pages/chat/ChatRoom';
1010
import NotFound from '@/pages/NotFound';
@@ -15,10 +15,6 @@ import UserProfile from '@/pages/userprofile/UserProfile';
1515
import PrivateRoute from './routes/PrivateRoute';
1616
import EditProfile from '@/pages/editprofile/EditProfile';
1717
import BlockList from '@/pages/blocklist/BlockList';
18-
import { useEffect } from 'react';
19-
import { loadYouTubeAPI } from './utils/youtubeApiLoader';
20-
import { useSpotifyAuth } from './hooks/useSpotifyAuth';
21-
import { useYouTubeStore } from './store/youtubeStore';
2218
import YouTubeAudioPlayer from './components/YouTubeAudioPlayer';
2319

2420
// TODO: 테스트용 나중에 지우기
@@ -27,27 +23,17 @@ import TestLoginModal from '@/components/testLogin/TestLoginModal';
2723
import { useSheetStore } from './store/sheetStore';
2824
import AnimatedLayout from '@/layouts/AnimatedLayout';
2925
import KaKaoRedirection from '@/components/KaKaoRedirection';
26+
import { useSSE } from '@/hooks/useSSE';
27+
import { useYotube } from '@/hooks/useYoutube';
3028

3129
function App() {
3230
const location = useLocation();
3331

3432
const { isAuthenticated } = useAuthStore();
35-
const spotifyAuth = useSpotifyAuth();
36-
const { isChatLoadingSheetOpen } = useSheetStore();
37-
// soundlink 로그인한 경우에만 spotify 로그인 후 토큰 가져오기
38-
useEffect(() => {
39-
if (isAuthenticated) {
40-
console.log('Spotify Auth Initialized:', spotifyAuth);
41-
}
42-
}, [isAuthenticated]);
33+
const { isRequestSendingSheetOpen, isRequestReceivingSheetOpen } = useSheetStore();
4334

44-
const { setApiReady } = useYouTubeStore();
45-
46-
useEffect(() => {
47-
loadYouTubeAPI().then(() => {
48-
setApiReady();
49-
}); // 앱이 처음 실행될 때 API 로드
50-
}, []);
35+
useSSE(); // SSE연결
36+
useYotube();
5137

5238
return (
5339
<>
@@ -89,7 +75,8 @@ function App() {
8975
</Routes>
9076
</AnimatedLayout>
9177
<Modal />
92-
{isChatLoadingSheetOpen && <ChatConnectLoadingSheet />}
78+
{isRequestSendingSheetOpen && <ChatConnectLoadingSheet type="sending" />}
79+
{isRequestReceivingSheetOpen && <ChatConnectLoadingSheet type="receiving" />}
9380
<YouTubeAudioPlayer playerId="1" />
9481
<YouTubeAudioPlayer playerId="2" />
9582
<YouTubeAudioPlayer playerId="3" />

src/apis/chat.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ export const cancelChatRequest = async (emotionRecordId: number) => {
1616
return data;
1717
};
1818
//채팅방 생성
19-
export const createChatroom = async (emotionRecordId: number, requestNickname: string) => {
20-
const { data } = await axiosInstance.post(
21-
`/chat/create?recordId=${emotionRecordId}&requestNickname=${requestNickname}`,
22-
{},
23-
);
19+
export const createChatroom = async (recordId: number, requestNickname: string) => {
20+
const { data } = await axiosInstance.post(`/chat/create`, null, {
21+
params: {
22+
recordId,
23+
requestNickname,
24+
},
25+
});
2426
return data;
2527
};
2628
//채팅방 닫기
@@ -45,3 +47,15 @@ export const loadChatRoomDetail = async (chatRoomId: number) => {
4547
});
4648
return data;
4749
};
50+
51+
// 채팅 거절
52+
export const postRejectChat = async (emotionRecordId: number, requestNickname: string) => {
53+
const { data } = await axiosInstance.post('/chat/request/reject', null, {
54+
params: {
55+
emotionRecordId,
56+
requestNickname,
57+
},
58+
});
59+
60+
return data;
61+
};
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import logo from '@/assets/icons/logo.svg';
2+
import MusicAnimation from '@components/MusicAnimation';
3+
import { useEffect, useState } from 'react';
4+
import { formattedTime } from '@/utils/formattedTime';
5+
import ChatRequestFailSheet from '@/components/ChatConnectLoadingSheet/ChatRequestFailSheet';
6+
import ChatRequestMessage from '@/components/ChatConnectLoadingSheet/ChatRequestMessage';
7+
import ChatRequestButton from '@/components/ChatConnectLoadingSheet/ChatRequestButton';
8+
import { useSheetStore } from '@/store/sheetStore';
9+
10+
export default function ChatConnectLoadingSheet({ type }: { type: 'sending' | 'receiving' }) {
11+
const { isChatConnectFail, setChatConnectFail } = useSheetStore();
12+
const [timeLeft, setTimeLeft] = useState(60); // 남은 시간
13+
//타이머 60초
14+
useEffect(() => {
15+
const endTime = new Date().getTime() + 60 * 1000; // 현재 시간 + 60초
16+
17+
const interval = setInterval(() => {
18+
const diff = Math.max(0, Math.ceil((endTime - new Date().getTime()) / 1000)); // 남은 초 계산
19+
20+
setTimeLeft(diff);
21+
22+
if (diff <= 0) {
23+
clearInterval(interval);
24+
setTimeLeft(0); // 0초로 고정
25+
}
26+
}, 1000);
27+
28+
return () => {
29+
clearInterval(interval);
30+
setChatConnectFail(false);
31+
}; //interval 정리
32+
}, []);
33+
34+
// 남은 시간이 0이되면 연결실패 처리
35+
useEffect(() => {
36+
if (timeLeft === 0) {
37+
setChatConnectFail(true);
38+
}
39+
}, [timeLeft]);
40+
41+
if (isChatConnectFail) {
42+
return <ChatRequestFailSheet />;
43+
}
44+
return (
45+
<div className="fixed inset-0 flex justify-center items-center z-50">
46+
<div className="relative w-full max-w-[600px] h-screen px-3 bg-background flex flex-col justify-center items-center">
47+
<img src={logo} className="w-[186px] h-[40px] mb-5" alt="로고" />
48+
<ChatRequestMessage type={type} />
49+
50+
<MusicAnimation />
51+
<p className="body-large-b text-primary-normal mt-[120px] mb-2">
52+
{formattedTime(timeLeft)}
53+
</p>
54+
<ChatRequestButton type={type} />
55+
</div>
56+
</div>
57+
);
58+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { cancelChatRequest, createChatroom, postRejectChat } from '@/apis/chat';
2+
import Button from '@/components/Button';
3+
import { useModalStore } from '@/store/modalStore';
4+
import { useSheetStore } from '@/store/sheetStore';
5+
import { useNavigate } from 'react-router';
6+
7+
function ChatRequestButton({ type }: { type: 'sending' | 'receiving' }) {
8+
const navigate = useNavigate();
9+
const { requesterInfo, currentRecord, closeAllSheets, closeSheet } = useSheetStore(); // 시트관리
10+
const { openModal, closeModal } = useModalStore(); // 모달관리
11+
12+
//채팅 요청 취소(요청 보낸 사람)
13+
const cancel = async () => {
14+
if (!currentRecord?.recordId) {
15+
console.log('record가 존재하지 않습니다');
16+
return;
17+
}
18+
try {
19+
console.log(currentRecord);
20+
const { code } = await cancelChatRequest(currentRecord.recordId);
21+
if (code === 200) {
22+
console.log('취소 요청 성공');
23+
closeSheet('isRequestSendingSheetOpen');
24+
} else {
25+
throw new Error('취소 요청 실패');
26+
}
27+
} catch (error) {
28+
console.error(error);
29+
closeSheet('isRequestSendingSheetOpen'); // 취소 요청 실패시 창 닫기
30+
}
31+
};
32+
33+
//채팅방 생성 (요청 받는 사람 입장에서 생성?)
34+
//sse로 받은 recordId로 채팅방 생성
35+
//sse로 받은 상대 정보로 '보내는 사람' 바꾸기
36+
const createChat = async () => {
37+
try {
38+
const { code, data } = await createChatroom(
39+
requesterInfo.emotionRecordId as number,
40+
requesterInfo.nickname,
41+
);
42+
if (code === 200) {
43+
const chatRoomId = data.chatRoomId;
44+
navigate(`/chatroom/${chatRoomId}`);
45+
closeAllSheets();
46+
} else {
47+
throw new Error('이미 취소된 요청입니다.');
48+
}
49+
//409 이면 이미 채팅방 있음 => 기존 채팅방으로
50+
} catch (error) {
51+
console.log(error);
52+
openModal({
53+
title: '이미 취소된 요청입니다',
54+
onConfirm: () => {
55+
closeModal();
56+
closeSheet('isRequestReceivingSheetOpen');
57+
},
58+
});
59+
}
60+
};
61+
62+
//채팅 요정 거절
63+
//거절 하면 거절한 사람은 바로 시트 닫기.
64+
//요청 거절당한 사람은 connectFail = true
65+
const refuseRequest = async () => {
66+
try {
67+
const { code } = await postRejectChat(
68+
requesterInfo.emotionRecordId as number,
69+
requesterInfo.nickname,
70+
);
71+
if (code === 200) {
72+
console.log('거절 성공');
73+
closeSheet('isRequestReceivingSheetOpen');
74+
} else {
75+
throw new Error('요청 취소 실패');
76+
}
77+
} catch (error) {
78+
console.error(error);
79+
closeSheet('isRequestReceivingSheetOpen');
80+
}
81+
};
82+
83+
if (type === 'sending') {
84+
return (
85+
<button
86+
onClick={cancel}
87+
className="caption-r text-gray-50 cursor-pointer border-b border-gray-50"
88+
>
89+
취소하기
90+
</button>
91+
);
92+
} else {
93+
return (
94+
<div className="absolute bottom-10 flex gap-[6px] px-3 w-full">
95+
<Button onClick={createChat} variant="primary">
96+
수락하기
97+
</Button>
98+
<Button onClick={refuseRequest} variant="secondary">
99+
거절하기
100+
</Button>
101+
</div>
102+
);
103+
}
104+
}
105+
106+
export default ChatRequestButton;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import logo from '@/assets/icons/logo.svg';
2+
import Button from '@/components/Button';
3+
import sad from '@/assets/icons/sad-icon.svg';
4+
import { useNavigate } from 'react-router';
5+
import { useSheetStore } from '@/store/sheetStore';
6+
7+
function ChatRequestFailSheet() {
8+
const navigate = useNavigate();
9+
const { currentRecord, closeSheet } = useSheetStore();
10+
const handleClickToHome = () => {
11+
closeSheet('isRequestSendingSheetOpen');
12+
closeSheet('isRequestReceivingSheetOpen');
13+
navigate('/home');
14+
};
15+
return (
16+
<div className="fixed inset-0 flex justify-center items-center z-50">
17+
<div className="relative w-full max-w-[600px] h-screen px-3 bg-background flex flex-col justify-center items-center">
18+
<img src={logo} className="w-[186px] h-[40px] mb-5" alt="로고" />
19+
<p className="h4-b text-center text-gray-50 mb-2">
20+
<span className="text-primary-normal">{currentRecord?.nickName}</span>
21+
<span>님과 연결되지 않았어요</span>
22+
</p>
23+
24+
<p className="caption-r text-center text-gray-60 mb-5">
25+
괜찮아요! 새로운 감정을 공유할 <br /> 다른 사람을 찾아볼까요?
26+
</p>
27+
<img src={sad} alt="연결실패" />
28+
29+
<div className="absolute bottom-10 flex gap-[6px] px-3 w-full">
30+
<Button onClick={handleClickToHome} variant="primary">
31+
홈으로 가기
32+
</Button>
33+
</div>
34+
</div>
35+
</div>
36+
);
37+
}
38+
39+
export default ChatRequestFailSheet;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useSheetStore } from '@/store/sheetStore';
2+
3+
interface ChatRequestMessageProps {
4+
type: 'sending' | 'receiving';
5+
}
6+
7+
function ChatRequestMessage({ type }: ChatRequestMessageProps) {
8+
const { requesterInfo } = useSheetStore();
9+
const { currentRecord } = useSheetStore();
10+
if (type === 'sending') {
11+
return (
12+
<>
13+
<p className="h4-b text-center text-gray-50 mb-2">
14+
<span className="text-primary-normal">{currentRecord?.nickName || '테스트'}</span>님에게
15+
대화 요청 중...
16+
</p>
17+
<p className="caption-r text-center text-gray-60 mb-5">
18+
곧 새로운 연결이 시작됩니다. <br /> 잠시만 기다려주세요!
19+
</p>
20+
</>
21+
);
22+
} else {
23+
return (
24+
<>
25+
<>
26+
<p className="h4-b text-center text-gray-50 mb-2">
27+
<span className="text-primary-normal">{requesterInfo?.nickname || '테스트'}</span> 님이
28+
대화를 요청했어요
29+
</p>
30+
<p className="caption-r text-center text-gray-60 mb-5">함께 이야기해 볼까요?</p>
31+
</>
32+
</>
33+
);
34+
}
35+
}
36+
37+
export default ChatRequestMessage;

0 commit comments

Comments
 (0)