Skip to content

Commit

Permalink
[Feat]qfeed 99 -채팅 api 연동 (#65)
Browse files Browse the repository at this point in the history
* fix: qfeed-99 채팅 한글 두번 전송 되는 오류 해결, 커서 위치 변경

* fix: qfeed-99 firebase api 키 추가

* feat: qfeed-99 채팅api 연동

* fix: qfeed-99 merge

* fix: qfeed-99 패치 방법 변경

* feat: qfeed-99 채팅 리스트 연동 후 구현

* feat: qfeed-99 채팅 내역 조회 api 연동 완료

* feat: qfeed-99 채팅 내역 띄우기 완료

* feat: qfeed-99 채팅웹소켓 통신 메세지 전송 기능 완료, isMine 적용

* fix: qfeed-99 구독 경로 수정

* fix: qfeed-99 merge

* fix: qfeed-99 채팅 조회, 전송 완료
  • Loading branch information
ahnjongin authored Dec 8, 2024
1 parent 88189f9 commit aeff5f4
Show file tree
Hide file tree
Showing 17 changed files with 360 additions and 154 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.13.5",
"@emotion/styled": "^11",
"@stomp/stompjs": "^7.0.0",
"@tanstack/react-query": "^5.60.6",
"@types/js-cookie": "^3.0.6",
"@types/react-icons": "^3.0.0",
Expand All @@ -42,10 +43,11 @@
"react-router": "^7.0.1",
"react-router-dom": "^7.0.1",
"react-slidedown": "^2.4.7",
"react-spinners": "^0.14.1",
"react-spinners": "^0.15.0",
"rimraf": "^6.0.1",
"rollup": "^4.27.4",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"storybook": "^8.4.5",
"storybook-addon-react-router-v6": "^2.0.15",
"swiper": "^11.1.15",
Expand Down
12 changes: 6 additions & 6 deletions public/firebase-messaging-sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ importScripts('https://www.gstatic.com/firebasejs/9.21.0/firebase-messaging.js')

// Firebase 초기화
firebase.initializeApp({
apiKey: 'YOUR_API_KEY',
authDomain: 'YOUR_AUTH_DOMAIN',
projectId: 'YOUR_PROJECT_ID',
storageBucket: 'YOUR_STORAGE_BUCKET',
messagingSenderId: 'YOUR_MESSAGING_SENDER_ID',
appId: 'YOUR_APP_ID',
apiKey: 'AIzaSyCFC3QJKAZhz1R0k-h58wJA8_Rb_PbyiL4',
authDomain: 'q-feed.firebaseapp.com',
projectId: 'q-feed',
storageBucket: 'q-feed.firebasestorage.app',
messagingSenderId: '804246377517',
appId: '1:804246377517:web:71270af160949939da14a4',
});

// Messaging 초기화
Expand Down
5 changes: 1 addition & 4 deletions src/api/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import { APIResponse } from '@/types/response';
import { getCookie } from '@/utils/cookies';
import { ACCESS_TOKEN_KEY } from '@/constants/token';

export class APIClient {
private client: AxiosInstance;

Expand All @@ -18,7 +15,7 @@ export class APIClient {
// 요청 인터셉터
this.client.interceptors.request.use(
(config) => {
const token = getCookie(ACCESS_TOKEN_KEY);
const token = 'token';
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
Expand Down
5 changes: 1 addition & 4 deletions src/components/ui/LoadingSpinner/LoadingSpinner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ interface LoadingSpinnerProps {
color?: string;
}

const LoadingSpinner = ({
size = 40,
color = theme.colors.primary,
}: LoadingSpinnerProps) => {
const LoadingSpinner = ({ size = 40, color = theme.colors.primary }: LoadingSpinnerProps) => {
return (
<SpinnerContainer>
<ClockLoader size={size} color={color} />
Expand Down
18 changes: 18 additions & 0 deletions src/pages/ChatList/api/fetchChatList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { apiClient } from '@/api/fetch';
import { ChatData } from '@/pages/ChatList/type/chatListType';

export const chatAPI = {
// 채팅방 목록 가져오기
getChatList: () => apiClient.get<ChatData[]>('/chats'),
};

export const fetchChatList = async (): Promise<ChatData[]> => {
try {
const response = await chatAPI.getChatList();
console.log('응답 데이터: ', response.data); // 응답 확인
return response.data || [];
} catch (error) {
console.error('API 요청 중 오류 발생: ', error); // 에러 확인
return [];
}
};
80 changes: 36 additions & 44 deletions src/pages/ChatList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/** @jsxImportSource @emotion/react */
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { fetchChatList } from '@/pages/ChatList/api/fetchChatList';
import { ChatData } from '@/pages/ChatList/type/chatListType';
import ProfileImageCon from '../../components/ui/ProfileImageCon/ProfileImageCon';
import InputBar from '../../components/ui/InputBar/InputBar';
import Header from '@/components/common/Header';
Expand All @@ -14,34 +17,35 @@ import {
unreadCountStyle,
userNameStyle,
} from '@/pages/ChatList/styles';
interface ChatItemProps {
id: string; // 채팅방 ID
profileImg?: string; // 프로필 이미지 URL
userName: string; // 사용자 이름
lastMessage: string; // 마지막 메시지
time: string; // 메시지 시간
unreadCount?: number; // 읽지 않은 메시지 수
}

// 채팅 리스트 아이템 컴포넌트
const ChatItem = ({ id, profileImg, userName, lastMessage, time, unreadCount }: ChatItemProps) => {
const ChatItem = ({
chatRoomId,
otherUserProfile,
otherUserNickname,
lastMessageContent,
lastMessageCreatedAt,
unreadMessageCount,
}: ChatData) => {
const navigate = useNavigate();

const handleClick = () => {
navigate(`/chatroom/${id}`); // 클릭 시 채팅방으로 이동
navigate(`/chatroom/${chatRoomId}`); // 클릭 시 채팅방으로 이동
};

return (
<div css={chatItemStyle} onClick={handleClick}>
<ProfileImageCon src={profileImg || ''} size={60} />
<ProfileImageCon src={otherUserProfile || ''} size={60} />
<div css={chatContentStyle}>
<div css={userNameStyle}>
<span>{userName}</span>
<span css={timeStyle}>{time}</span>
<span>{otherUserNickname}</span>
<span css={timeStyle}>{lastMessageCreatedAt}</span>
</div>
<div css={lastMessageStyle}>
<span>{lastMessage}</span>
{unreadCount && unreadCount > 0 && <span css={unreadCountStyle}>{unreadCount}</span>}
<span>{lastMessageContent}</span>
{unreadMessageCount && unreadMessageCount > 0 ? (
<span css={unreadCountStyle}>{unreadMessageCount}</span>
) : null}
</div>
</div>
</div>
Expand All @@ -51,40 +55,28 @@ const ChatItem = ({ id, profileImg, userName, lastMessage, time, unreadCount }:
// 채팅 리스트 메인 컴포넌트
const ChatList = () => {
const [searchTerm, setSearchTerm] = useState(''); // 검색어 상태
const [chatData] = useState([
{
id: '1',
profileImg: '',
userName: 'asdf',
lastMessage: '마지막 대화 내용 어쩌구저쩌구',
time: '13:18',
unreadCount: 3,
},
{
id: '2',
profileImg: '',
userName: '홍길동',
lastMessage: '여긴 새로운 메시지가 있어요!',
time: '14:25',
unreadCount: 10,
},
{
id: '3',
profileImg: '',
userName: '이순신',
lastMessage: '반갑습니다.',
time: '15:00',
},
]);
const { data: chatData, isLoading } = useQuery({
queryKey: ['chatList'],
queryFn: fetchChatList,
refetchOnWindowFocus: false,
staleTime: 5 * 60 * 1000,
});

// 검색어를 기준으로 채팅 리스트 필터링
const filteredChatData = chatData.filter((chat) =>
chat.userName.toLowerCase().includes(searchTerm.toLowerCase())
);
const filteredChatData = Array.isArray(chatData)
? chatData.filter((chat) =>
chat.otherUserNickname.toLowerCase().includes(searchTerm.toLowerCase())
)
: [];

const handleSearchChange = (value: string) => {
setSearchTerm(value); // 검색어 상태 업데이트
};

if (isLoading) {
return <div>Loading...</div>;
}

return (
<div css={chatListContainerStyle}>
{/* 검색 인풋 */}
Expand All @@ -93,7 +85,7 @@ const ChatList = () => {
{/* 채팅 리스트 */}
<div css={chatListStyle}>
{filteredChatData.map((chat) => (
<ChatItem key={chat.id} {...chat} />
<ChatItem key={chat.chatRoomId} {...chat} />
))}
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion src/pages/ChatList/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export const userNameStyle = css`
display: flex;
justify-content: space-between;
font-size: 14px;
font-weight: bold;
`;

export const timeStyle = css`
Expand Down
8 changes: 8 additions & 0 deletions src/pages/ChatList/type/chatListType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface ChatData {
chatRoomId: string; // 채팅방 ID
otherUserProfile?: string; // 프로필 이미지 URL
otherUserNickname: string; // 사용자 이름
lastMessageContent: string; // 마지막 메시지
lastMessageCreatedAt: string; // 메시지 시간
unreadMessageCount?: number; // 읽지 않은 메시지 수
}
19 changes: 19 additions & 0 deletions src/pages/ChatRoom/api/fetchChatRoom.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { apiClient } from '@/api/fetch';
import { MessageType } from '@/pages/ChatRoom/type/messageType';

export const messageAPI = {
// 특정 채팅방 메시지 가져오기
getMessages: (chatRoomId: string) =>
apiClient.get<MessageType[]>(`/chats/${chatRoomId}/messages`),
};

export const fetchMessages = async (chatRoomId: string): Promise<MessageType[]> => {
try {
const response = await messageAPI.getMessages(chatRoomId);
console.log('응답 데이터: ', response.data); // 응답 확인
return response.data || []; // 데이터 반환
} catch (error) {
console.error('메시지 조회 중 오류 발생: ', error); // 에러 확인
return [];
}
};
37 changes: 37 additions & 0 deletions src/pages/ChatRoom/api/socket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Client } from '@stomp/stompjs';

// WebSocket URL 설정
const SOCKET_URL = 'wss://q-feed.n-e.kr/ws';

// STOMP 클라이언트 생성
export const stompClient = new Client({
brokerURL: SOCKET_URL, // WebSocket URL
reconnectDelay: 5000, // 재연결 대기 시간 (ms)
debug: (str) => {
console.log('STOMP Debug:', str);
},
connectHeaders: {
Authorization: 'Token', // 토큰 인증 (필요한 경우)
},
});

// STOMP 연결 함수
export const connectStomp = () => {
stompClient.onConnect = (frame) => {
console.log('STOMP 연결 성공:', frame);
};

stompClient.onStompError = (frame) => {
console.error('STOMP 에러:', frame.headers['message']);
};

stompClient.activate(); // STOMP 활성화
};

// STOMP 연결 해제 함수
export const disconnectStomp = () => {
if (stompClient.active) {
stompClient.deactivate();
console.log('STOMP 연결 해제');
}
};
15 changes: 13 additions & 2 deletions src/pages/ChatRoom/component/InputBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,30 @@ const ChatInputBar: React.FC<InputBarProps> = ({
onSend,
}) => {
const [value, setValue] = useState('');
const [isComposing, setIsComposing] = useState(false);

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
const handleCompositionStart = () => {
setIsComposing(true); // IME 입력 시작
};

const handleCompositionEnd = (e: React.CompositionEvent<HTMLInputElement>) => {
setIsComposing(false); // IME 입력 종료
setValue(e.currentTarget.value); // 최종 입력 값 업데이트
};

const handleSend = () => {
if (onSend && value.trim() !== '') {
if (!isComposing && onSend && value.trim() !== '') {
const messageToSend = value.trim();
setValue(''); // 입력 필드 초기화
onSend(messageToSend); // 메시지 전송
}
};

const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
if (e.key === 'Enter' && !isComposing) {
e.preventDefault(); // 기본 동작 방지
handleSend(); // 메시지 전송
}
Expand All @@ -55,6 +64,8 @@ const ChatInputBar: React.FC<InputBarProps> = ({
placeholder={placeholder}
value={value}
onChange={handleInputChange}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onKeyDown={handleKeyPress} // 엔터키 이벤트 추가
/>
</div>
Expand Down
Loading

0 comments on commit aeff5f4

Please sign in to comment.