diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 6ecc939a..0ca4cf3b 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -7,6 +7,6 @@ module.exports = {
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
- '@typescript-eslint/no-explicit-any': 'warn',
+ '@typescript-eslint/no-explicit-any': 'off',
},
}
diff --git a/package.json b/package.json
index d1e3b758..1b4a6784 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"scripts": {
"dev": "vite --host",
"build": "tsc && vite build",
- "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 10",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 30",
"preview": "vite preview",
"pwa": "pwa-assets-generator --preset minimal public/test.svg",
"format": "prettier --check --ignore-path .gitignore \"**/*.{ts,tsx}\"",
diff --git a/src/App.tsx b/src/App.tsx
index afcd894f..6657a9fa 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -7,7 +7,6 @@ import { router } from '~/router'
import GlobalStyle from '~/styles/globalStyle'
import { darkTheme, lightTheme } from '~/styles/theme'
import Loader from '~components/Loader'
-import { WebSocketProvider } from '~/WebSocketContext'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
@@ -18,29 +17,27 @@ function App() {
const toggleTheme = () => setTheme(prev => (prev === lightTheme ? darkTheme : lightTheme))
return (
<>
-
+
-
-
- DDang
-
-
-
-
-
- }>
-
-
-
-
-
-
+
+ DDang
+
+
+
+
+
+ }>
+
+
+
+
+
-
+
>
)
}
diff --git a/src/WebSocketContext.tsx b/src/WebSocketContext.tsx
index c637ab81..bb615fd0 100644
--- a/src/WebSocketContext.tsx
+++ b/src/WebSocketContext.tsx
@@ -1,5 +1,5 @@
-import React, { createContext, useContext, useEffect, useState } from 'react'
import { Client } from '@stomp/stompjs'
+import React, { createContext, useContext, useEffect, useState } from 'react'
import SockJS from 'sockjs-client'
interface WebSocketContextType {
@@ -11,12 +11,13 @@ interface WebSocketContextType {
const WebSocketContext = createContext(null)
+const token = localStorage.getItem('token')
+
export const WebSocketProvider = ({ children }: { children: React.ReactNode }) => {
const [client, setClient] = useState(null)
const [isConnected, setIsConnected] = useState(false)
useEffect(() => {
- const token = localStorage.getItem('token')
const SERVER_URL = 'https://ddang.shop/ws'
const socket = new SockJS(SERVER_URL)
@@ -62,6 +63,9 @@ export const WebSocketProvider = ({ children }: { children: React.ReactNode }) =
client.publish({
destination,
body: JSON.stringify(body),
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
})
}
}
diff --git a/src/apis/chat/fetchChatMessageList.tsx b/src/apis/chat/fetchChatMessageList.tsx
new file mode 100644
index 00000000..8fb1119e
--- /dev/null
+++ b/src/apis/chat/fetchChatMessageList.tsx
@@ -0,0 +1,46 @@
+import { AxiosError } from 'axios'
+import { APIResponse, CommonAPIRequest, CommonAPIResponse, ErrorResponse, PaginationResponse } from '~types/api'
+import { axiosInstance } from '~apis/axiosInstance'
+
+export type FetchChatMessageListRequest = Pick
+
+export type FetchChatMessageListResponse = PaginationResponse & {
+ content: Array<
+ Pick
+ >
+}
+
+export const fetchChatMessageList = async ({
+ chatRoomId,
+ lastMessageCreatedAt,
+}: FetchChatMessageListRequest): Promise> => {
+ try {
+ const { data } = await axiosInstance.get>(`/chat/message/${chatRoomId}`, {
+ params: { lastMessageCreatedAt },
+ })
+ return data
+ } catch (error) {
+ if (error instanceof AxiosError) {
+ const { response } = error as AxiosError
+
+ if (response) {
+ const { code, message } = response.data
+ switch (code) {
+ case 400:
+ throw new Error(message || '잘못된 요청입니다.')
+ case 401:
+ throw new Error(message || '인증에 실패했습니다.')
+ case 500:
+ throw new Error(message || '서버 오류가 발생했습니다.')
+ default:
+ throw new Error(message || '알 수 없는 오류가 발생했습니다.')
+ }
+ }
+ // 요청 자체가 실패한 경우
+ throw new Error('네트워크 연결을 확인해주세요')
+ }
+
+ console.error('예상치 못한 에러:', error)
+ throw new Error('다시 시도해주세요')
+ }
+}
diff --git a/src/apis/chat/useChatMessageList.tsx b/src/apis/chat/useChatMessageList.tsx
new file mode 100644
index 00000000..3c00adf1
--- /dev/null
+++ b/src/apis/chat/useChatMessageList.tsx
@@ -0,0 +1,20 @@
+import { useSuspenseInfiniteQuery } from '@tanstack/react-query'
+import { fetchChatMessageList } from '~apis/chat/fetchChatMessageList'
+import { queryKey } from '~constants/queryKey'
+
+type useChatMessageListProps = {
+ chatRoomId: number
+}
+
+export default function useChatMessageList({ chatRoomId }: useChatMessageListProps) {
+ return useSuspenseInfiniteQuery({
+ queryKey: queryKey.social.chatMessageList(chatRoomId),
+ queryFn: async ({ pageParam }) => {
+ return await fetchChatMessageList({ lastMessageCreatedAt: pageParam as string, chatRoomId })
+ },
+ getNextPageParam: lastPage => {
+ return lastPage.data.last ? undefined : lastPage.data.content[0].createdAt
+ },
+ initialPageParam: '',
+ })
+}
diff --git a/src/apis/chatRoom/useChatList.ts b/src/apis/chatRoom/useChatList.ts
new file mode 100644
index 00000000..402ca6db
--- /dev/null
+++ b/src/apis/chatRoom/useChatList.ts
@@ -0,0 +1,10 @@
+import { useQuery } from '@tanstack/react-query'
+import { fetchChatRoomList } from '~apis/chatRoom/fetchChatRoomList'
+import { queryKey } from '~constants/queryKey'
+
+export default function useChatList() {
+ return useQuery({
+ queryKey: queryKey.social.chatRoomList(),
+ queryFn: () => fetchChatRoomList().then(res => res.data),
+ })
+}
diff --git a/src/apis/chatRoom/useCreateChatRoom.tsx b/src/apis/chatRoom/useCreateChatRoom.tsx
index 1a20e751..db10b6b0 100644
--- a/src/apis/chatRoom/useCreateChatRoom.tsx
+++ b/src/apis/chatRoom/useCreateChatRoom.tsx
@@ -3,6 +3,7 @@ import ChatModal from '~modals/ChatModal'
import { useModalStore } from '~stores/modalStore'
import { APIResponse } from '~types/api'
import { createChatRoom, CreateChatRoomRequest, CreateChatRoomResponse } from './createChatRoom'
+import { useWebSocket } from '~/WebSocketContext'
export const useCreateChatRoom = (): UseMutationResult<
APIResponse,
@@ -12,12 +13,16 @@ export const useCreateChatRoom = (): UseMutationResult<
createRoom: (req: CreateChatRoomRequest) => void
} => {
const { pushModal } = useModalStore()
+ const { isConnected } = useWebSocket()
const mutation = useMutation, Error, CreateChatRoomRequest>({
mutationFn: createChatRoom,
onSuccess: (data, { opponentMemberId }) => {
+ if (!isConnected) {
+ throw new Error('WebSocket 연결이 되어 있지 않습니다.')
+ }
console.log(data.message || '채팅방 생성 성공')
- pushModal()
+ pushModal()
},
onError: error => {
console.error('채팅방 생성 실패:', error.message)
diff --git a/src/apis/chatRoom/useFriendList.ts b/src/apis/chatRoom/useFriendList.ts
new file mode 100644
index 00000000..cf441129
--- /dev/null
+++ b/src/apis/chatRoom/useFriendList.ts
@@ -0,0 +1,10 @@
+import { useQuery } from '@tanstack/react-query'
+import { fetchFriendList } from '~apis/friend/fetchFriendList'
+import { queryKey } from '~constants/queryKey'
+
+export default function useFriendList() {
+ return useQuery({
+ queryKey: queryKey.social.friendList(),
+ queryFn: () => fetchFriendList().then(res => res.data),
+ })
+}
diff --git a/src/apis/chatRoom/useSocialData.tsx b/src/apis/chatRoom/useSocialData.tsx
deleted file mode 100644
index 3d45bab3..00000000
--- a/src/apis/chatRoom/useSocialData.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { useSuspenseQueries } from '@tanstack/react-query'
-import { fetchChatRoomList } from '~apis/chatRoom/fetchChatRoomList'
-import { fetchFriendList } from '~apis/friend/fetchFriendList'
-import { queryKey } from '~constants/queryKey'
-
-export function useSocialData() {
- const results = useSuspenseQueries({
- queries: [
- {
- queryKey: queryKey.social.chatRoomList(),
- queryFn: () => fetchChatRoomList().then(res => res.data),
- },
- {
- queryKey: queryKey.social.friendList(),
- queryFn: () => fetchFriendList().then(res => res.data),
- },
- ],
- })
-
- const [chatListQuery, friendListQuery] = results
-
- return {
- chatList: chatListQuery.data ?? [],
- friendList: friendListQuery.data ?? [],
- isLoading: chatListQuery.isLoading || friendListQuery.isLoading,
- isError: chatListQuery.isError || friendListQuery.isError,
- }
-}
diff --git a/src/apis/friend/requestFriend.ts b/src/apis/friend/requestFriend.ts
new file mode 100644
index 00000000..59ea960f
--- /dev/null
+++ b/src/apis/friend/requestFriend.ts
@@ -0,0 +1,17 @@
+import { axiosInstance } from '~apis/axiosInstance'
+import { APIResponse, CommonAPIResponse } from '~types/api'
+
+export type RequestFriendRequest = {
+ memberId: number
+ decision: 'ACCEPT' | 'DENY'
+}
+
+export type RequestFriendResponse = Pick<
+ CommonAPIResponse,
+ 'memberId' | 'name' | 'email' | 'provider' | 'gender' | 'address' | 'familyRole' | 'profileImg'
+>
+
+export const requestFriend = async (req: RequestFriendRequest): Promise> => {
+ const { data } = await axiosInstance.post>(`/friend`, req)
+ return data
+}
diff --git a/src/apis/main/fetchHomePageData.ts b/src/apis/main/fetchHomePageData.ts
index 417e87e7..c7eaa491 100644
--- a/src/apis/main/fetchHomePageData.ts
+++ b/src/apis/main/fetchHomePageData.ts
@@ -4,7 +4,18 @@ import { axiosInstance } from '~apis/axiosInstance'
export type FetchHomePageDataResponse = Pick<
CommonAPIResponse,
- 'memberId' | 'familyRole' | 'dogName' | 'timeDuration' | 'totalDistanceMeter' | 'totalCalorie' | 'memberProfileImgUrl'
+ | 'memberId'
+ | 'familyRole'
+ | 'address'
+ | 'email'
+ | 'memberGender'
+ | 'memberName'
+ | 'memberProfileImgUrl'
+ | 'provider'
+ | 'dogName'
+ | 'timeDuration'
+ | 'totalDistanceMeter'
+ | 'totalCalorie'
>
export const fetchHomePageData = async (): Promise> => {
diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx
index 1f158ceb..6cdd4b1a 100644
--- a/src/components/Footer/index.tsx
+++ b/src/components/Footer/index.tsx
@@ -3,10 +3,11 @@ import { FaRegCalendarCheck } from 'react-icons/fa6'
import { IoMdPeople } from 'react-icons/io'
import { IoHomeSharp } from 'react-icons/io5'
import { MdOutlineFamilyRestroom } from 'react-icons/md'
+import { useNavigate } from 'react-router-dom'
import { useTheme } from 'styled-components'
+import useChatList from '~apis/chatRoom/useChatList'
import { Typo11 } from '~components/Typo'
import * as S from './styles'
-import { useNavigate } from 'react-router-dom'
const FOOTER_NAV_LIST = [
{ Icon: IoHomeSharp, endpoint: '/', typo: '홈' },
@@ -34,6 +35,11 @@ export default function Footer() {
}
navigate(endpoint)
}
+ const { data } = useChatList()
+ let sum = 0
+ data?.forEach(({ unreadMessageCount }) => {
+ sum += unreadMessageCount
+ })
return (
@@ -47,6 +53,7 @@ export default function Footer() {
handleNavigation(endpoint)
}}
>
+ {typo === '소셜' && {sum}}
{typo}
diff --git a/src/components/Footer/styles.ts b/src/components/Footer/styles.ts
index c44af6fe..b922fa1a 100644
--- a/src/components/Footer/styles.ts
+++ b/src/components/Footer/styles.ts
@@ -1,5 +1,6 @@
import { Link } from 'react-router-dom'
import { styled } from 'styled-components'
+import { UnreadChatCount } from '~components/UnreadChatCount'
import { FOOTER_HEIGHT } from '~constants/layout'
export const Footer = styled.footer`
@@ -18,6 +19,7 @@ export const FooterNavList = styled.ul`
`
export const FooterNavItem = styled(Link)`
+ position: relative;
flex: 1;
display: flex;
flex-direction: column;
@@ -25,3 +27,14 @@ export const FooterNavItem = styled(Link)`
justify-content: center;
text-decoration: none;
`
+
+export const ChatCount = styled(UnreadChatCount)`
+ left: calc(50% + 3px);
+ top: 2px;
+ translate: 8px;
+ font-size: ${({ theme }) => theme.typography._9};
+ min-width: 0;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+`
diff --git a/src/components/SendMessageForm/index.tsx b/src/components/SendMessageForm/index.tsx
index 94fc5a6d..08478c6a 100644
--- a/src/components/SendMessageForm/index.tsx
+++ b/src/components/SendMessageForm/index.tsx
@@ -1,31 +1,34 @@
import { Typo14 } from '~components/Typo'
-import * as S from './styles'
-import { createChatRoom } from '~apis/chatRoom/createChatRoom'
import { CommonAPIResponse } from '~types/api'
+import * as S from './styles'
+import { useWebSocket } from '~/WebSocketContext'
+
+type SendMessageFormProps = React.FormHTMLAttributes & Partial>
-type SendMessageFormProps = React.FormHTMLAttributes & {
- chatCount: number
-} & Partial>
+export default function SendMessageForm({ chatRoomId, ...rest }: SendMessageFormProps) {
+ const { publish } = useWebSocket()
+ const sendMessage = (message: string) => {
+ console.log(`채팅방 번호 ${chatRoomId}로 채팅을 보냅니다.`)
+ publish(`/pub/api/v1/chat/message`, { chatRoomId, message })
+ }
-export default function SendMessageForm({ chatRoomId, chatCount, ...rest }: SendMessageFormProps) {
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (rest.onSubmit) {
rest.onSubmit(e)
return
}
-
- if (chatCount === 0) {
- //* 채팅방 생성
- await createChatRoom({ opponentMemberId: 123 })
- }
- //* 채팅 전송 웹소켓
- console.log('chatRoomId:', chatRoomId)
+ const $form = e.target as HTMLFormElement
+ const formData = new FormData($form)
+ const message = formData.get('message') as string
+ if (!message.trim()) return
+ sendMessage(message)
+ $form.reset()
}
return (
-
+
전송
diff --git a/src/components/UnreadChatCount.ts b/src/components/UnreadChatCount.ts
new file mode 100644
index 00000000..a4ceec15
--- /dev/null
+++ b/src/components/UnreadChatCount.ts
@@ -0,0 +1,17 @@
+import styled from 'styled-components'
+
+export const UnreadChatCount = styled.span`
+ position: absolute;
+ right: 20px;
+ display: block;
+ border-radius: 22px;
+ background-color: ${({ theme }) => theme.colors.brand.sub};
+ min-width: 20px;
+
+ padding: 1.5px 6px;
+ color: ${({ theme }) => theme.colors.grayscale.gc_4};
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`
diff --git a/src/constants/familyRole.ts b/src/constants/familyRole.ts
index b03254f6..ace24a22 100644
--- a/src/constants/familyRole.ts
+++ b/src/constants/familyRole.ts
@@ -7,4 +7,5 @@ export const FAMILY_ROLE = {
OLDER_SISTER: '누나',
GRANDFATHER: '할아버지',
GRANDMOTHER: '할머니',
+ '': '',
}
diff --git a/src/constants/queryKey.ts b/src/constants/queryKey.ts
index deba03bc..00511531 100644
--- a/src/constants/queryKey.ts
+++ b/src/constants/queryKey.ts
@@ -2,6 +2,7 @@ export const queryKey = {
social: {
chatRoomList: () => ['chatRoomList'],
friendList: () => ['friendList'],
+ chatMessageList: (chatRoomId: number) => ['chatMessageList', chatRoomId],
},
profile: (memberId: number) => ['profile', memberId],
home: () => ['homePageData'],
diff --git a/src/hooks/useScrollPreservation.ts b/src/hooks/useScrollPreservation.ts
new file mode 100644
index 00000000..05397422
--- /dev/null
+++ b/src/hooks/useScrollPreservation.ts
@@ -0,0 +1,26 @@
+import { useEffect, useRef, useState } from 'react'
+
+type useScrollPreservationProps = {
+ dependency: unknown[]
+}
+
+export function useScrollPreservation({ dependency }: useScrollPreservationProps) {
+ const elementRef = useRef(null)
+ const [prevScrollHeight, setPrevScrollHeight] = useState(0)
+
+ const preserveScroll = () => {
+ if (elementRef.current) {
+ setPrevScrollHeight(elementRef.current.scrollHeight)
+ }
+ }
+
+ useEffect(() => {
+ if (elementRef.current) {
+ const { scrollHeight, scrollTop } = elementRef.current
+ const nextScrollTop = scrollHeight - prevScrollHeight + scrollTop
+ elementRef.current.scrollTop = nextScrollTop
+ }
+ }, [prevScrollHeight, ...dependency])
+
+ return { elementRef, preserveScroll }
+}
diff --git a/src/modals/ChatArea/index.tsx b/src/modals/ChatArea/index.tsx
new file mode 100644
index 00000000..7fdde944
--- /dev/null
+++ b/src/modals/ChatArea/index.tsx
@@ -0,0 +1,60 @@
+import useChatMessageList from '~apis/chat/useChatMessageList'
+import { useHomePageData } from '~apis/main/useHomePageData'
+import { IncomingMessage, OutgoingMessage } from '~components/Message'
+import SendMessageForm from '~components/SendMessageForm'
+import useObserver from '~hooks/useObserver'
+import { useScrollPreservation } from '~hooks/useScrollPreservation'
+import * as S from './styles'
+
+type ChatAreaListProps = {
+ chatRoomId: number
+}
+
+export default function ChatArea({ chatRoomId }: ChatAreaListProps) {
+ const {
+ data: { memberId },
+ } = useHomePageData()
+ const { data, hasNextPage, isFetchingNextPage, fetchNextPage } = useChatMessageList({ chatRoomId })
+ const { elementRef: chatAreaRef, preserveScroll } = useScrollPreservation({ dependency: [data] })
+ const { observerRef } = useObserver({
+ callback: () => {
+ if (hasNextPage && !isFetchingNextPage) {
+ if (chatAreaRef.current) {
+ preserveScroll()
+ }
+ fetchNextPage()
+ }
+ },
+ })
+
+ return (
+
+
+ {[...data.pages].reverse().map(page =>
+ page.data.content.map(chat =>
+ chat.memberInfo?.memberId === memberId ? (
+
+ {chat.text}
+
+ ) : (
+
+ {chat.text}
+
+ )
+ )
+ )}
+
+
+
+ )
+}
diff --git a/src/modals/ChatArea/styles.ts b/src/modals/ChatArea/styles.ts
new file mode 100644
index 00000000..c974fd14
--- /dev/null
+++ b/src/modals/ChatArea/styles.ts
@@ -0,0 +1,7 @@
+import { styled } from 'styled-components'
+
+export const ChatArea = styled.div`
+ height: calc(100% - 64px);
+ overflow: auto;
+`
+export const ChatMessageList = styled.div``
diff --git a/src/modals/ChatModal/index.tsx b/src/modals/ChatModal/index.tsx
index fee98bed..32f7745a 100644
--- a/src/modals/ChatModal/index.tsx
+++ b/src/modals/ChatModal/index.tsx
@@ -1,296 +1,69 @@
import { HiEllipsisVertical } from 'react-icons/hi2'
import Header from '~components/Header'
-import { IncomingMessage, OutgoingMessage } from '~components/Message'
import Profile from '~components/Profile'
-import SendMessageForm from '~components/SendMessageForm'
import { Separator } from '~components/Separator'
import { Typo11, Typo15 } from '~components/Typo'
-import { useScrollToBottom } from '~hooks/useScrollToBottom'
+import ChatArea from '~modals/ChatArea'
import { useModalStore } from '~stores/modalStore'
import * as S from './styles'
+import { useFetchProfile } from '~apis/member/useFetchProfile'
+import { QueryErrorResetBoundary } from '@tanstack/react-query'
+import { ErrorBoundary } from 'react-error-boundary'
+import { Suspense } from 'react'
+import Loader from '~components/Loader'
+import ErrorFallback from '~components/ErrorFallback'
+import { FAMILY_ROLE } from '~constants/familyRole'
type ChatModalProps = {
chatRoomId: number
- userId: number
+ opponentMemberId: number
}
-export default function ChatModal({ chatRoomId, userId }: ChatModalProps) {
- const { popModal } = useModalStore()
- console.log('chatRoomId', chatRoomId) //todo fetch by chatRoomId
- console.log('userId', userId) //todo fetch by userId
- const ref = useScrollToBottom()
-
+export default function ChatModal({ chatRoomId, opponentMemberId }: ChatModalProps) {
return (
-
-
-
-
-
- 감자탕수육
-
- 남자
-
- 할아버지
-
-
-
-
-
-
-
-
- {chatDummyData.map(chat =>
- chat.sender === '나' ? (
- {chat.message}
- ) : (
- {chat.message}
- )
- )}
-
-
+
+
+
+ }>
+
+
+
+
+ }>
+
+
+
+
)
}
-const chatDummyData = [
- {
- id: 0,
- sender: '나',
- receiver: '이서연',
- message: '안녕하세요! 지난번 회의 자료 보내주실 수 있나요?',
- timestamp: '2024-11-15T09:30:01.936376',
- isRead: true,
- },
- {
- id: 1,
- sender: '이서연',
- receiver: '나',
- message: '네! 잠시만 기다려주세요. 바로 찾아보겠습니다.',
- timestamp: '2024-11-15T09:31:01.936400',
- isRead: true,
- },
- {
- id: 2,
- sender: '이서연',
- receiver: '나',
- message: '[회의자료.pdf] 첨부파일 보내드립니다!',
- timestamp: '2024-11-15T09:33:01.936409',
- isRead: true,
- },
- {
- id: 3,
- sender: '나',
- receiver: '이서연',
- message: '감사합니다 😊',
- timestamp: '2024-11-15T09:34:01.936416',
- isRead: true,
- },
- {
- id: 4,
- sender: '나',
- receiver: '이서연',
- message: '그런데 이번 프로젝트 일정이 어떻게 되나요?',
- timestamp: '2024-11-15T09:35:01.936423',
- isRead: true,
- },
- {
- id: 5,
- sender: '이서연',
- receiver: '나',
- message: '다음 주 월요일부터 시작이에요!',
- timestamp: '2024-11-15T09:36:01.936430',
- isRead: true,
- },
- {
- id: 6,
- sender: '나',
- receiver: '이서연',
- message: '알겠습니다. 혹시 킥오프 미팅은 몇 시인가요?',
- timestamp: '2024-11-15T09:37:01.936438',
- isRead: true,
- },
- {
- id: 7,
- sender: '이서연',
- receiver: '나',
- message: '오전 10시에 회의실 3번에서 진행됩니다.',
- timestamp: '2024-11-15T09:38:01.936445',
- isRead: true,
- },
- {
- id: 8,
- sender: '나',
- receiver: '이서연',
- message: '네, 참석하겠습니다!',
- timestamp: '2024-11-15T09:39:01.936452',
- isRead: true,
- },
- {
- id: 9,
- sender: '이서연',
- receiver: '나',
- message: '그리고 팀원 명단도 보내드릴게요.',
- timestamp: '2024-11-15T09:40:01.936458',
- isRead: true,
- },
- {
- id: 10,
- sender: '이서연',
- receiver: '나',
- message: '[팀원명단.xlsx] 여기 있습니다!',
- timestamp: '2024-11-15T09:41:01.936465',
- isRead: true,
- },
- {
- id: 11,
- sender: '나',
- receiver: '이서연',
- message: '혹시 사전 준비해야 할 사항이 있을까요?',
- timestamp: '2024-11-15T09:42:01.936471',
- isRead: true,
- },
- {
- id: 12,
- sender: '이서연',
- receiver: '나',
- message: '간단한 자기소개만 준비해 오시면 됩니다!',
- timestamp: '2024-11-15T09:43:01.936478',
- isRead: true,
- },
- {
- id: 13,
- sender: '나',
- receiver: '이서연',
- message: '알겠습니다. 점심 식사는 어떻게 하나요?',
- timestamp: '2024-11-15T09:44:01.936485',
- isRead: true,
- },
- {
- id: 14,
- sender: '이서연',
- receiver: '나',
- message: '회사에서 도시락을 준비한다고 합니다.',
- timestamp: '2024-11-15T09:45:01.936491',
- isRead: true,
- },
- {
- id: 15,
- sender: '나',
- receiver: '이서연',
- message: '아, 그렇군요. 좋네요 👍',
- timestamp: '2024-11-15T09:46:01.936497',
- isRead: false,
- },
- {
- id: 16,
- sender: '이서연',
- receiver: '나',
- message: '혹시 음료 선호도 있으신가요?',
- timestamp: '2024-11-15T09:47:01.936504',
- isRead: false,
- },
- {
- id: 17,
- sender: '나',
- receiver: '이서연',
- message: '아메리카노 좋아합니다!',
- timestamp: '2024-11-15T09:48:01.936511',
- isRead: false,
- },
- {
- id: 18,
- sender: '이서연',
- receiver: '나',
- message: '네, 참고하겠습니다 😊',
- timestamp: '2024-11-15T09:49:01.936517',
- isRead: false,
- },
- {
- id: 19,
- sender: '나',
- receiver: '이서연',
- message: '회의실에 프로젝터 있나요?',
- timestamp: '2024-11-15T09:50:01.936523',
- isRead: false,
- },
- {
- id: 20,
- sender: '이서연',
- receiver: '나',
- message: '네, 프로젝터랑 화이트보드 모두 구비되어 있습니다.',
- timestamp: '2024-11-15T09:51:01.936530',
- isRead: false,
- },
- {
- id: 21,
- sender: '나',
- receiver: '이서연',
- message: '노트북은 개인이 준비하면 되나요?',
- timestamp: '2024-11-15T09:52:01.936536',
- isRead: false,
- },
- {
- id: 22,
- sender: '이서연',
- receiver: '나',
- message: '네, 개인 노트북 지참해 주세요!',
- timestamp: '2024-11-15T09:53:01.936542',
- isRead: false,
- },
- {
- id: 23,
- sender: '나',
- receiver: '이서연',
- message: '와이파이 비밀번호는 어떻게 되나요?',
- timestamp: '2024-11-15T09:54:01.936549',
- isRead: false,
- },
- {
- id: 24,
- sender: '이서연',
- receiver: '나',
- message: '당일 회의실에 비치해두겠습니다.',
- timestamp: '2024-11-15T09:55:01.936555',
- isRead: false,
- },
- {
- id: 25,
- sender: '나',
- receiver: '이서연',
- message: '알겠습니다. 다른 준비사항 있으면 알려주세요!',
- timestamp: '2024-11-15T09:56:01.936562',
- isRead: false,
- },
- {
- id: 26,
- sender: '이서연',
- receiver: '나',
- message: '네, 추가 사항 있으면 다시 연락드리겠습니다.',
- timestamp: '2024-11-15T09:57:01.936569',
- isRead: false,
- },
- {
- id: 27,
- sender: '나',
- receiver: '이서연',
- message: '감사합니다. 좋은 하루 보내세요!',
- timestamp: '2024-11-15T09:58:01.936575',
- isRead: false,
- },
- {
- id: 28,
- sender: '이서연',
- receiver: '나',
- message: '네, 과장님도 좋은 하루 되세요 😊',
- timestamp: '2024-11-15T09:59:01.936582',
- isRead: false,
- },
- {
- id: 29,
- sender: '나',
- receiver: '이서연',
- message: '월요일에 뵙겠습니다!',
- timestamp: '2024-11-15T10:00:01.936589',
- isRead: false,
- },
-]
+type ChatModalHeaderProps = {
+ opponentMemberId: number
+}
+
+function ChatModalHeader({ opponentMemberId }: ChatModalHeaderProps) {
+ const {
+ data: { name, gender, familyRole },
+ } = useFetchProfile(opponentMemberId)
+ const { popModal } = useModalStore()
+
+ return (
+
+
+
+
+ {name}
+
+ {gender === 'MALE' ? '남자' : '여자'}
+
+ {FAMILY_ROLE[familyRole]}
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/modals/ChatModal/styles.ts b/src/modals/ChatModal/styles.ts
index a49d1e45..4582f538 100644
--- a/src/modals/ChatModal/styles.ts
+++ b/src/modals/ChatModal/styles.ts
@@ -8,7 +8,6 @@ export const ChatModal = styled.div`
height: 100%;
padding: ${HEADER_HEIGHT_LG}px 20px 0;
background-color: ${({ theme }) => theme.colors.brand.lighten_3};
- overflow: auto;
&::after {
background: url(${DogHowling}) center/cover;
content: '';
diff --git a/src/pages/HomePage/index.tsx b/src/pages/HomePage/index.tsx
index 6079e28e..df6c11dc 100644
--- a/src/pages/HomePage/index.tsx
+++ b/src/pages/HomePage/index.tsx
@@ -1,12 +1,13 @@
-import { QueryErrorResetBoundary } from '@tanstack/react-query'
-import { Suspense } from 'react'
+import { InfiniteData, QueryErrorResetBoundary, useQueryClient } from '@tanstack/react-query'
+import { Suspense, useEffect } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import { Helmet } from 'react-helmet-async'
-import BellIcon from '~assets/icons/bell_icon.svg?react'
-import GPSIcon from '~assets/icons/gps_icon.svg?react'
-import ClockIcon from '~assets/icons/clock_icon.svg?react'
+import { useNavigate, useSearchParams } from 'react-router-dom'
import { useHomePageData } from '~apis/main/useHomePageData'
import DogHand from '~assets/dog_hand.svg?react'
+import BellIcon from '~assets/icons/bell_icon.svg?react'
+import ClockIcon from '~assets/icons/clock_icon.svg?react'
+import GPSIcon from '~assets/icons/gps_icon.svg?react'
import { ActionButton } from '~components/Button/ActionButton'
import ErrorFallback from '~components/ErrorFallback'
import Loader from '~components/Loader'
@@ -17,13 +18,109 @@ import { FAMILY_ROLE } from '~constants/familyRole'
import NotificationModal from '~modals/NotificationModal'
import { useModalStore } from '~stores/modalStore'
import * as S from './styles'
-import { useSearchParams, useNavigate } from 'react-router-dom'
-import { useEffect } from 'react'
+import { useWebSocket } from '~/WebSocketContext'
+import { FetchChatMessageListResponse } from '~apis/chat/fetchChatMessageList'
+import { queryKey } from '~constants/queryKey'
+import { APIResponse, CommonAPIResponse } from '~types/api'
function HomeContent() {
+ const { isConnected, subscribe } = useWebSocket()
const { data } = useHomePageData()
- console.log(data)
const { pushModal } = useModalStore()
+ const queryClient = useQueryClient()
+
+ useEffect(() => {
+ if (isConnected) {
+ console.log('구독!')
+ subscribe(`/user/queue/errors`, message => {
+ const response = JSON.parse(message.body)
+ console.log('에러 구독', response)
+ })
+
+ subscribe(`/sub/message/${data.email}`, message => {
+ const response = JSON.parse(message.body) as {
+ data: {
+ chatRoomId: number
+ unreadCount: number
+ }[]
+ }
+ console.log('이메일 구독', response)
+ response.data.forEach(chatRoom => {
+ subscribe(`/sub/chat/${chatRoom.chatRoomId}`, message => {
+ const res = JSON.parse(message.body) as APIResponse<
+ Pick<
+ CommonAPIResponse,
+ 'chatId' | 'createdAt' | 'updatedAt' | 'chatRoomId' | 'memberInfo' | 'isRead' | 'text'
+ >
+ >
+ console.log('채팅방 구독', res)
+ queryClient.invalidateQueries({
+ queryKey: queryKey.social.chatRoomList(),
+ })
+ if (res.data.chatId)
+ queryClient.setQueryData>>(
+ queryKey.social.chatMessageList(res.data.chatRoomId),
+ oldData => {
+ if (!oldData) {
+ const initialPage: APIResponse = {
+ code: 200,
+ status: 'OK',
+ message: 'Success',
+ data: {
+ content: [res.data],
+ size: 1,
+ number: 0,
+ numberOfElements: 1,
+ first: true,
+ last: true,
+ empty: false,
+ sort: {
+ empty: true,
+ sorted: false,
+ unsorted: true,
+ },
+ pageable: {
+ offset: 0,
+ sort: {
+ empty: true,
+ sorted: false,
+ unsorted: true,
+ },
+ pageSize: 1,
+ paged: true,
+ pageNumber: 0,
+ unpaged: false,
+ },
+ },
+ }
+ return {
+ pages: [initialPage],
+ pageParams: [null],
+ }
+ }
+ return {
+ ...oldData,
+ pages: oldData.pages.map((page, index) => {
+ if (index === 0) {
+ return {
+ ...page,
+ data: {
+ ...page.data,
+ content: [...page.data.content, res.data],
+ numberOfElements: page.data.numberOfElements + 1,
+ },
+ }
+ }
+ return page
+ }),
+ }
+ }
+ )
+ })
+ })
+ })
+ }
+ }, [isConnected])
return (
<>
@@ -81,6 +178,7 @@ function HomeContent() {
export default function HomePage() {
const [searchParams] = useSearchParams()
const navigate = useNavigate()
+
useEffect(() => {
const accessToken = searchParams.get('accessToken')
if (accessToken) {
@@ -90,12 +188,15 @@ export default function HomePage() {
window.history.replaceState({}, '', '/')
return
}
+
const storedToken = localStorage.getItem('token')
if (!storedToken) {
console.log('토큰 없음 비로그인 상태. login페이지 이동.')
navigate('/login')
+ return
}
}, [searchParams, navigate])
+
return (
diff --git a/src/pages/RegisterPage/Register/index.tsx b/src/pages/RegisterPage/Register/index.tsx
index c339c775..82548c6a 100644
--- a/src/pages/RegisterPage/Register/index.tsx
+++ b/src/pages/RegisterPage/Register/index.tsx
@@ -53,6 +53,8 @@ export default function Register() {
provider: registerData.provider as 'KAKAO' | 'GOOGLE',
})
if (response.code === 201) {
+ //? 채팅 구현을 위해 임의로 추가한 부분입니다.
+ setOwnerProfile({ memberId: response.data.memberId })
pushModal()
}
} catch (error) {
diff --git a/src/pages/SocialPage/components/ChatItem/index.tsx b/src/pages/SocialPage/components/ChatItem/index.tsx
index 3636e995..4e2f0965 100644
--- a/src/pages/SocialPage/components/ChatItem/index.tsx
+++ b/src/pages/SocialPage/components/ChatItem/index.tsx
@@ -5,6 +5,7 @@ import { Separator } from '~components/Separator'
import { Typo11, Typo13, Typo17 } from '~components/Typo'
import * as S from './styles'
import { FAMILY_ROLE } from '~constants/familyRole'
+import { UnreadChatCount } from '~components/UnreadChatCount'
type ChatItemProps = FetchChatRoomListResponse[number]
@@ -31,9 +32,9 @@ export default function ChatItem({ lastMessage, members, name, unreadMessageCoun
{lastMessage}
-
+
{unreadMessageCount}
-
+
)
}
diff --git a/src/pages/SocialPage/components/ChatItem/styles.ts b/src/pages/SocialPage/components/ChatItem/styles.ts
index d732531a..664b444f 100644
--- a/src/pages/SocialPage/components/ChatItem/styles.ts
+++ b/src/pages/SocialPage/components/ChatItem/styles.ts
@@ -20,19 +20,3 @@ export const DetailWrapper = styled.div`
align-items: center;
gap: 4px;
`
-
-export const UnreadChatCount = styled.span`
- position: absolute;
- right: 20px;
- display: block;
- border-radius: 22px;
- background-color: ${({ theme }) => theme.colors.brand.sub};
- min-width: 20px;
-
- padding: 1.5px 6px;
- color: ${({ theme }) => theme.colors.grayscale.gc_4};
-
- display: flex;
- justify-content: center;
- align-items: center;
-`
diff --git a/src/pages/SocialPage/components/FriendChatList/index.tsx b/src/pages/SocialPage/components/FriendChatList/index.tsx
index 0ac6d9cc..45eca904 100644
--- a/src/pages/SocialPage/components/FriendChatList/index.tsx
+++ b/src/pages/SocialPage/components/FriendChatList/index.tsx
@@ -1,4 +1,5 @@
-import { useSocialData } from '~apis/chatRoom/useSocialData'
+import useChatList from '~apis/chatRoom/useChatList'
+import useFriendList from '~apis/chatRoom/useFriendList'
import ChatItem from '~pages/SocialPage/components/ChatItem'
import FriendItem from '~pages/SocialPage/components/FriendItem'
import { SocialTabs } from '~types/social'
@@ -8,14 +9,26 @@ type FriendChatListProps = {
selectedTab: SocialTabs
}
-export default function FriendChatList({ selectedTab }: FriendChatListProps) {
- const { chatList, friendList } = useSocialData()
+function FriendList() {
+ const { data: friendList } = useFriendList()
return (
- {selectedTab === 'friendList'
- ? friendList.map(friendInfo => )
- : chatList.map(chatInfo => )}
+ {friendList?.map(friendInfo => )}
)
}
+
+function ChatList() {
+ const { data: chatList } = useChatList()
+
+ return (
+
+ {chatList?.map(chatInfo => )}
+
+ )
+}
+
+export default function FriendChatList({ selectedTab }: FriendChatListProps) {
+ return selectedTab === 'friendList' ? :
+}
diff --git a/src/router.tsx b/src/router.tsx
index 55aaaf98..bda24bab 100644
--- a/src/router.tsx
+++ b/src/router.tsx
@@ -2,16 +2,17 @@ import Footer from '~components/Footer'
import { createBrowserRouter, Outlet } from 'react-router-dom'
import * as Pages from './components/LazyComponents'
import ModalContainer from '~modals/ModalContainer'
+import { WebSocketProvider } from '~/WebSocketContext'
export const router = createBrowserRouter([
{
path: '/',
element: (
- <>
+
- >
+
),
children: [
{
diff --git a/src/types/api.ts b/src/types/api.ts
index 5ca4e137..026230bd 100644
--- a/src/types/api.ts
+++ b/src/types/api.ts
@@ -130,6 +130,8 @@ export type Chat = TimeStamp & {
readMessageIds: null
/** 읽지 않은 메시지 수 @example 3 */
unreadMessageCount: number
+ /** 현재 가장 오래된 메시지 생성 시간 @example "2024-12-03T08:17:04.717Z" */
+ lastMessageCreatedAt: string
}
export type Family = {