diff --git a/.vscode/typescript.code-snippets b/.vscode/typescript.code-snippets index e7e11ee..4bd6c80 100644 --- a/.vscode/typescript.code-snippets +++ b/.vscode/typescript.code-snippets @@ -59,10 +59,10 @@ " default:", " throw new Error(message || '알 수 없는 오류가 발생했습니다.')", " }", - " } else {", - " // 요청 자체가 실패한 경우", - " throw new Error('네트워크 연결을 확인해주세요')", - " }", + " } ", + " // 요청 자체가 실패한 경우", + " throw new Error('네트워크 연결을 확인해주세요')", + " ", " }", "", " console.error('예상치 못한 에러:', error);", diff --git a/lighthouserc.cjs b/lighthouserc.cjs index fb16b48..06365e2 100644 --- a/lighthouserc.cjs +++ b/lighthouserc.cjs @@ -8,6 +8,7 @@ module.exports = { // 'http://localhost:4173/walk', 'http://localhost:4173/login', 'http://localhost:4173/mypage', + 'http://localhost:4173/profile/:id', ], numberOfRuns: 2, startServerReadyPattern: 'Local', diff --git a/package-lock.json b/package-lock.json index d2ae54e..325e69c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "ol": "^10.2.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-error-boundary": "^4.1.2", "react-helmet-async": "^2.0.5", "react-icons": "^5.3.0", "react-router-dom": "^6.28.0", @@ -9715,6 +9716,17 @@ "react": "^18.3.1" } }, + "node_modules/react-error-boundary": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz", + "integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", diff --git a/package.json b/package.json index da8a71e..c177f53 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,11 @@ "build-and-test": "npm run build && lhci autorun" }, "dependencies": { - "@stomp/stompjs": "^7.0.0", "@emotion/react": "^11.13.5", "@emotion/styled": "^11.13.5", "@mui/material": "^6.1.9", "@mui/styled-engine-sc": "^6.1.9", + "@stomp/stompjs": "^7.0.0", "@tanstack/react-query": "^5.62.2", "@tanstack/react-query-devtools": "^5.62.2", "d3": "^7.9.0", @@ -26,6 +26,7 @@ "ol": "^10.2.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-error-boundary": "^4.1.2", "react-helmet-async": "^2.0.5", "react-icons": "^5.3.0", "react-router-dom": "^6.28.0", @@ -42,8 +43,8 @@ "@types/ol": "^7.0.0", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", - "@types/sockjs-client": "^1.5.4", "@types/react-textarea-autosize": "^8.0.0", + "@types/sockjs-client": "^1.5.4", "@types/styled-components": "^5.1.34", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", diff --git a/src/apis/chatRoom/createChatRoom.ts b/src/apis/chatRoom/createChatRoom.ts new file mode 100644 index 0000000..bde5dad --- /dev/null +++ b/src/apis/chatRoom/createChatRoom.ts @@ -0,0 +1,48 @@ +import { AxiosError } from 'axios' +import { APIResponse, CommonAPIResponse, ErrorResponse } from '~types/api' +import { axiosInstance } from '~apis/axiosInstance' + +export type CreateChatRoomRequest = { + opponentMemberId: number +} + +export type CreateChatRoomResponse = Pick< + CommonAPIResponse, + 'chatRoomId' | 'name' | 'lastMessage' | 'unreadMessageCount' | 'members' +> + +/** + * 새로운 채팅방을 생성하고, 채팅방 정보를 반환합니다. + *? 이미 동일한 맴버와의 채팅방이 있다면, 해당 채팅방을 반환합니다. + */ +export const createChatRoom = async (req: CreateChatRoomRequest): Promise> => { + try { + const { data } = await axiosInstance.post>(`/chat/rooms`, req) + console.log(data.message || '채팅방 생성 성공') + console.log(data.data) + 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/chatRoom/fetchChatRoomList.ts b/src/apis/chatRoom/fetchChatRoomList.ts new file mode 100644 index 0000000..37bf062 --- /dev/null +++ b/src/apis/chatRoom/fetchChatRoomList.ts @@ -0,0 +1,37 @@ +import { AxiosError } from 'axios' +import { APIResponse, CommonAPIResponse, ErrorResponse } from '~types/api' +import { axiosInstance } from '~apis/axiosInstance' + +export type FetchChatRoomListResponse = Array< + Pick +> + +export const fetchChatRoomList = async (): Promise> => { + try { + const { data } = await axiosInstance.get>(`/chat/rooms`) + 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/chatRoom/useCreateChatRoom.tsx b/src/apis/chatRoom/useCreateChatRoom.tsx new file mode 100644 index 0000000..1a20e75 --- /dev/null +++ b/src/apis/chatRoom/useCreateChatRoom.tsx @@ -0,0 +1,31 @@ +import { useMutation, UseMutationResult } from '@tanstack/react-query' +import ChatModal from '~modals/ChatModal' +import { useModalStore } from '~stores/modalStore' +import { APIResponse } from '~types/api' +import { createChatRoom, CreateChatRoomRequest, CreateChatRoomResponse } from './createChatRoom' + +export const useCreateChatRoom = (): UseMutationResult< + APIResponse, + Error, + CreateChatRoomRequest +> & { + createRoom: (req: CreateChatRoomRequest) => void +} => { + const { pushModal } = useModalStore() + + const mutation = useMutation, Error, CreateChatRoomRequest>({ + mutationFn: createChatRoom, + onSuccess: (data, { opponentMemberId }) => { + console.log(data.message || '채팅방 생성 성공') + pushModal() + }, + onError: error => { + console.error('채팅방 생성 실패:', error.message) + }, + }) + + return { + ...mutation, + createRoom: mutation.mutate, + } +} diff --git a/src/apis/chatRoom/useSocialData.tsx b/src/apis/chatRoom/useSocialData.tsx new file mode 100644 index 0000000..3d45bab --- /dev/null +++ b/src/apis/chatRoom/useSocialData.tsx @@ -0,0 +1,28 @@ +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/fetchFriendList.ts b/src/apis/friend/fetchFriendList.ts new file mode 100644 index 0000000..0afa07a --- /dev/null +++ b/src/apis/friend/fetchFriendList.ts @@ -0,0 +1,37 @@ +import { AxiosError } from 'axios' +import { APIResponse, CommonAPIResponse, ErrorResponse } from '~types/api' +import { axiosInstance } from '~apis/axiosInstance' + +export type FetchFriendListResponse = Array< + Pick +> + +export const fetchFriendList = async (): Promise> => { + try { + const { data } = await axiosInstance.get>(`/friend`) + 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/member/fetchProfile.ts b/src/apis/member/fetchProfile.ts new file mode 100644 index 0000000..e2880a4 --- /dev/null +++ b/src/apis/member/fetchProfile.ts @@ -0,0 +1,49 @@ +import { AxiosError } from 'axios' +import { APIResponse, CommonAPIRequest, CommonAPIResponse, ErrorResponse } from '~types/api' +import { axiosInstance } from '~apis/axiosInstance' + +export type FetchProfileRequest = Pick + +export type FetchProfileResponse = Pick< + CommonAPIResponse, + | 'memberId' + | 'name' + | 'address' + | 'gender' + | 'familyRole' + | 'profileImg' + | 'totalDistance' + | 'walkCount' + | 'countWalksWithMember' + | 'dog' +> + +export const fetchProfile = async ({ memberId }: FetchProfileRequest): Promise> => { + try { + const { data } = await axiosInstance.get>(`/member/${memberId}`) + 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/member/useFetchProfile.tsx b/src/apis/member/useFetchProfile.tsx new file mode 100644 index 0000000..9d7a80e --- /dev/null +++ b/src/apis/member/useFetchProfile.tsx @@ -0,0 +1,11 @@ +import { useSuspenseQuery, UseSuspenseQueryResult } from '@tanstack/react-query' +import { fetchProfile, FetchProfileResponse } from '~apis/member/fetchProfile' +import { queryKey } from '~constants/queryKey' + +export const useFetchProfile = (memberId: number): UseSuspenseQueryResult => { + return useSuspenseQuery({ + queryKey: queryKey.profile(memberId), + queryFn: () => fetchProfile({ memberId }).then(data => data.data), + staleTime: 1000 * 60 * 5, // 5분 + }) +} diff --git a/src/components/DogProfile/index.tsx b/src/components/DogProfile/index.tsx index 69da547..5dfdefd 100644 --- a/src/components/DogProfile/index.tsx +++ b/src/components/DogProfile/index.tsx @@ -1,21 +1,25 @@ import * as S from './styles' import { Typo13, Typo15, Typo20 } from '~components/Typo' import { Separator } from '~components/Separator' - import Profile from '~components/Profile' -export default function DogProfile() { +import { Dog } from '~types/api' +import { calculateAge } from '~utils/calculateAge' + +type DogProfileProps = Pick + +export default function DogProfile({ birthDate, breed, comment, gender, name, profileImg }: DogProfileProps) { return ( - + - {dogInfo.name} - {dogInfo.breed} + {name} + {breed} - {dogInfo.age}살 + {calculateAge(birthDate)}살 - {dogInfo.gender === 'male' ? '남' : '여'} + {gender === 'MALE' ? '남' : '여'} 중성화 X @@ -30,21 +34,9 @@ export default function DogProfile() { 우리 댕댕이를 소개해요! - {dogInfo.intro} + {comment} ) } - -const dogInfo = { - name: '밤톨이', - breed: '포메라니안', - age: 4, - gender: 'male', - neutered: false, // 중성화 여부 - weight: 3.4, - profileImg: '', - intro: `우리아이 안 물어요 착해요. - 강아지껌을 너무 좋아해요 같이 놀아요. `, -} diff --git a/src/components/ErrorFallback/index.tsx b/src/components/ErrorFallback/index.tsx new file mode 100644 index 0000000..ce24d21 --- /dev/null +++ b/src/components/ErrorFallback/index.tsx @@ -0,0 +1,23 @@ +import { FallbackProps } from 'react-error-boundary' +import { ActionButton } from '~components/Button/ActionButton' +import { Typo15, Typo17 } from '~components/Typo' +import * as S from './styles' + +type ErrorFallbackProps = FallbackProps + +export default function ErrorFallback({ error, resetErrorBoundary }: ErrorFallbackProps) { + return ( + + {error.message}... + + 오류가 발생했네요! +
+ 아래 버튼을 통해 다시 요청해보세요! +
+ + resetErrorBoundary()}> + 다시 불러오기! + +
+ ) +} diff --git a/src/components/ErrorFallback/styles.ts b/src/components/ErrorFallback/styles.ts new file mode 100644 index 0000000..64fcf62 --- /dev/null +++ b/src/components/ErrorFallback/styles.ts @@ -0,0 +1,3 @@ +import { styled } from 'styled-components' + +export const ErrorFallback = styled.div`` diff --git a/src/components/Loader/index.tsx b/src/components/Loader/index.tsx index 6aad46e..f6c1f0d 100644 --- a/src/components/Loader/index.tsx +++ b/src/components/Loader/index.tsx @@ -1,5 +1,5 @@ import * as S from './styles' export default function Loader() { - return Loading... + return Loader... } diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index d748697..140fef0 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -5,7 +5,7 @@ import { useModalStore } from '~stores/modalStore' type ProfileProps = { $size: number $src: string - userId?: string + userId?: number } export default function Profile({ $size, $src, userId }: ProfileProps) { diff --git a/src/components/Profile/styles.ts b/src/components/Profile/styles.ts index 37ce943..1da3c9a 100644 --- a/src/components/Profile/styles.ts +++ b/src/components/Profile/styles.ts @@ -3,7 +3,7 @@ import styled from 'styled-components' type ProfileProps = { $size: number $src: string - $userId?: string + $userId?: number } export const Profile = styled.div` diff --git a/src/components/SendMessageForm/index.tsx b/src/components/SendMessageForm/index.tsx index 4cffa82..94fc5a6 100644 --- a/src/components/SendMessageForm/index.tsx +++ b/src/components/SendMessageForm/index.tsx @@ -1,11 +1,30 @@ import { Typo14 } from '~components/Typo' import * as S from './styles' +import { createChatRoom } from '~apis/chatRoom/createChatRoom' +import { CommonAPIResponse } from '~types/api' -type SendMessageFormProps = React.FormHTMLAttributes +type SendMessageFormProps = React.FormHTMLAttributes & { + chatCount: number +} & Partial> + +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) + } -export default function SendMessageForm({ ...rest }: SendMessageFormProps) { return ( - + 전송 diff --git a/src/constants/familyRole.ts b/src/constants/familyRole.ts new file mode 100644 index 0000000..b03254f --- /dev/null +++ b/src/constants/familyRole.ts @@ -0,0 +1,10 @@ +export const FAMILY_ROLE = { + FATHER: '아빠', + MOTHER: '엄마', + ELDER_BROTHER: '오빠', + OLDER_BROTHER: '형', + ELDER_SISTER: '언니', + OLDER_SISTER: '누나', + GRANDFATHER: '할아버지', + GRANDMOTHER: '할머니', +} diff --git a/src/constants/queryKey.ts b/src/constants/queryKey.ts new file mode 100644 index 0000000..3a216be --- /dev/null +++ b/src/constants/queryKey.ts @@ -0,0 +1,7 @@ +export const queryKey = { + social: { + chatRoomList: () => ['chatRoomList'], + friendList: () => ['friendList'], + }, + profile: (memberId: number) => ['profile', memberId], +} diff --git a/src/modals/ChatModal/index.tsx b/src/modals/ChatModal/index.tsx index 2239991..fee98be 100644 --- a/src/modals/ChatModal/index.tsx +++ b/src/modals/ChatModal/index.tsx @@ -5,17 +5,19 @@ 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 { useModalStore } from '~stores/modalStore' import * as S from './styles' -import { useScrollToBottom } from '~hooks/useScrollToBottom' type ChatModalProps = { - userId: string + chatRoomId: number + userId: number } -export default function ChatModal({ userId }: ChatModalProps) { +export default function ChatModal({ chatRoomId, userId }: ChatModalProps) { const { popModal } = useModalStore() - console.log('ChatModal', userId) //todo fetch by userId + console.log('chatRoomId', chatRoomId) //todo fetch by chatRoomId + console.log('userId', userId) //todo fetch by userId const ref = useScrollToBottom() return ( @@ -45,7 +47,7 @@ export default function ChatModal({ userId }: ChatModalProps) { ) )} - + ) } diff --git a/src/modals/RegisterDogModal/CheckDogProfileSection/index.tsx b/src/modals/RegisterDogModal/CheckDogProfileSection/index.tsx index 8feb719..62df58c 100644 --- a/src/modals/RegisterDogModal/CheckDogProfileSection/index.tsx +++ b/src/modals/RegisterDogModal/CheckDogProfileSection/index.tsx @@ -32,7 +32,7 @@ export default function CheckDogProfileSection() { $src={ 'https://www.shutterstock.com/image-photo/beautiful-golden-retriever-cute-puppy-260nw-2526542701.jpg' } - userId='' + userId={0} /> diff --git a/src/pages/FamilyDDangPage/index.tsx b/src/pages/FamilyDDangPage/index.tsx index bc11ef9..4b51e13 100644 --- a/src/pages/FamilyDDangPage/index.tsx +++ b/src/pages/FamilyDDangPage/index.tsx @@ -15,12 +15,12 @@ export default function FamilyDDang() { - - + {/* 데이터 바인딩 시 props 넣어주세요 */} + - + {family1Info.nickName} @@ -49,7 +49,7 @@ export default function FamilyDDang() { - + {family2Info.nickName} diff --git a/src/pages/ProfilePage/index.tsx b/src/pages/ProfilePage/index.tsx index e17619b..c23d87a 100644 --- a/src/pages/ProfilePage/index.tsx +++ b/src/pages/ProfilePage/index.tsx @@ -1,58 +1,75 @@ +import { QueryErrorResetBoundary } from '@tanstack/react-query' +import { Suspense } from 'react' +import { ErrorBoundary } from 'react-error-boundary' +import { Helmet } from 'react-helmet-async' import { useNavigate, useParams } from 'react-router-dom' +import { useFetchProfile } from '~apis/member/useFetchProfile' +import DogProfile from '~components/DogProfile' +import ErrorFallback from '~components/ErrorFallback' +import Loader from '~components/Loader' import Profile from '~components/Profile' import { Separator } from '~components/Separator' import { Typo13, Typo15, Typo20, Typo24 } from '~components/Typo' +import { FAMILY_ROLE } from '~constants/familyRole' import * as S from './styles' -import DogProfile from '~components/DogProfile' -export default function ProfilePage() { - const { id } = useParams() +function ProfileContent({ id }: { id: number }) { + const { data } = useFetchProfile(+id) const navigate = useNavigate() - console.log(id) //todo id 이용해 fetch + return ( - navigate(-1)} title='닉네임' /> + + {`DDang | ${data?.name}`} + + + navigate(-1)} title={data?.name} /> - - {userInfo.name} + + {data?.name} - {userInfo.location} 거주 + {data?.address} 거주 - {userInfo.gender === 'male' ? '남자' : '여자'} + {data?.gender === 'MALE' ? '남자' : '여자'} - {userInfo.role} + {data ? FAMILY_ROLE[data.familyRole] : ''} - {userInfo.walkCount}회 + {data?.walkCount}회 누적 산책 횟수 - {userInfo.walkDistance}km + {data?.totalDistance}km 총 산책 거리 - {userInfo.gangbunttaCount}회 + {data?.countWalksWithMember}회 강번따 횟수 - + {data && } ) } -const userInfo = { - name: '이성훈', - gender: 'male', - location: '용산구 남영동', - role: '할아버지', - walkCount: 23, - walkDistance: 32, - gangbunttaCount: 16, - profileImg: '', +export default function ProfilePage() { + const { id = '0' } = useParams() + + return ( + + {({ reset }) => ( + + }> + + + + )} + + ) } diff --git a/src/pages/SocialPage/components/ChatItem/index.tsx b/src/pages/SocialPage/components/ChatItem/index.tsx index f7b762b..f78b452 100644 --- a/src/pages/SocialPage/components/ChatItem/index.tsx +++ b/src/pages/SocialPage/components/ChatItem/index.tsx @@ -1,36 +1,38 @@ -import { ChatInfo } from '~types/social' -import * as S from './styles' +import { FetchChatRoomListResponse } from '~apis/chatRoom/fetchChatRoomList' +import { useCreateChatRoom } from '~apis/chatRoom/useCreateChatRoom' +import Profile from '~components/Profile' import { Separator } from '~components/Separator' import { Typo11, Typo13, Typo17 } from '~components/Typo' -import Profile from '~components/Profile' -import { useModalStore } from '~stores/modalStore' -import ChatModal from '~modals/ChatModal' +import * as S from './styles' +import { FAMILY_ROLE } from '~constants/familyRole' + +type ChatItemProps = FetchChatRoomListResponse[number] -type ChatItemProps = ChatInfo +export default function ChatItem({ lastMessage, members, name, unreadMessageCount }: ChatItemProps) { + const { createRoom } = useCreateChatRoom() -export default function ChatItem({ gender, lastChat, name, profileImg, role, unreadChatCount, userId }: ChatItemProps) { - const { pushModal } = useModalStore() + const onClickChatItem = () => createRoom({ opponentMemberId: members[0].memberId }) return ( - pushModal()}> - + + {name} - {role} + {FAMILY_ROLE[members[0].familyRole]} - {gender === 'male' ? '남' : '여'} + {members[0].gender === 'MALE' ? '남' : '여'} - {lastChat} + {lastMessage} - {unreadChatCount} + {unreadMessageCount} ) diff --git a/src/pages/SocialPage/components/ChatItem/styles.ts b/src/pages/SocialPage/components/ChatItem/styles.ts index 1a4b887..d732531 100644 --- a/src/pages/SocialPage/components/ChatItem/styles.ts +++ b/src/pages/SocialPage/components/ChatItem/styles.ts @@ -27,6 +27,12 @@ export const UnreadChatCount = styled.span` 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 b725386..0ac6d9c 100644 --- a/src/pages/SocialPage/components/FriendChatList/index.tsx +++ b/src/pages/SocialPage/components/FriendChatList/index.tsx @@ -1,22 +1,21 @@ +import { useSocialData } from '~apis/chatRoom/useSocialData' import ChatItem from '~pages/SocialPage/components/ChatItem' import FriendItem from '~pages/SocialPage/components/FriendItem' -import { ChatInfo, FriendInfo, SocialTabs } from '~types/social' +import { SocialTabs } from '~types/social' import * as S from './styles' type FriendChatListProps = { selectedTab: SocialTabs - friendList: FriendInfo[] - chatList: ChatInfo[] } -export default function FriendChatList({ selectedTab, friendList, chatList }: FriendChatListProps) { - //todo fetch by userId +export default function FriendChatList({ selectedTab }: FriendChatListProps) { + const { chatList, friendList } = useSocialData() return ( {selectedTab === 'friendList' - ? friendList.map(friendInfo => ) - : chatList.map(chatInfo => )} + ? friendList.map(friendInfo => ) + : chatList.map(chatInfo => )} ) } diff --git a/src/pages/SocialPage/components/FriendItem/index.tsx b/src/pages/SocialPage/components/FriendItem/index.tsx index ac81a4c..5f38a80 100644 --- a/src/pages/SocialPage/components/FriendItem/index.tsx +++ b/src/pages/SocialPage/components/FriendItem/index.tsx @@ -1,31 +1,34 @@ +import { useCreateChatRoom } from '~apis/chatRoom/useCreateChatRoom' +import { FetchFriendListResponse } from '~apis/friend/fetchFriendList' import Profile from '~components/Profile' import { Separator } from '~components/Separator' import { Typo14, Typo17 } from '~components/Typo' -import ChatModal from '~modals/ChatModal' -import { useModalStore } from '~stores/modalStore' -import { FriendInfo } from '~types/social' +import { FAMILY_ROLE } from '~constants/familyRole' import * as S from './styles' -type FriendItemProps = FriendInfo +type FriendItemProps = FetchFriendListResponse[number] + +export default function FriendItem({ gender, name, profileImg, memberId, familyRole }: FriendItemProps) { + const { createRoom } = useCreateChatRoom() + + const onClickMessageBtn = () => createRoom({ opponentMemberId: memberId }) -export default function FriendItem({ gender, name, profileImg, role, userId }: FriendItemProps) { - const { pushModal } = useModalStore() return ( - + {name} - {gender === 'male' ? '남자' : '여자'} + {gender === 'MALE' ? '남자' : '여자'} - {role} + {FAMILY_ROLE[familyRole]} - pushModal()}> + 메시지 diff --git a/src/pages/SocialPage/index.tsx b/src/pages/SocialPage/index.tsx index f5e8824..494240d 100644 --- a/src/pages/SocialPage/index.tsx +++ b/src/pages/SocialPage/index.tsx @@ -1,8 +1,11 @@ -import { useState } from 'react' +import { Suspense, useState } from 'react' import { Typo15 } from '~components/Typo' import FriendChatList from '~pages/SocialPage/components/FriendChatList' -import { ChatInfo, FriendInfo } from '~types/social' import * as S from './styles' +import { ErrorBoundary } from 'react-error-boundary' +import ErrorFallback from '~components/ErrorFallback' +import { QueryErrorResetBoundary } from '@tanstack/react-query' +import Loader from '~components/Loader' export default function SocialPage() { const [selectedTab, setSelectedTab] = useState<'friendList' | 'dangTalk'>('friendList') @@ -27,192 +30,15 @@ export default function SocialPage() { - + + {({ reset }) => ( + + }> + + + + )} + ) } - -const friendList: FriendInfo[] = [ - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '1', - role: '이모', - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '2', - role: '이모', - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '3', - role: '이모', - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '4', - role: '이모', - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '5', - role: '이모', - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '6', - role: '이모', - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '7', - role: '이모', - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '8', - role: '이모', - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '9', - role: '이모', - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '10', - role: '이모', - }, -] -const chatList: ChatInfo[] = [ - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '1', - role: '이모', - lastChat: '마지막 채팅 텍스트입니다', - unreadChatCount: 15, - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '2', - role: '이모', - lastChat: '마지막 채팅 텍스트입니다', - unreadChatCount: 15, - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '3', - role: '이모', - lastChat: '마지막 채팅 텍스트입니다', - unreadChatCount: 15, - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '4', - role: '이모', - lastChat: '마지막 채팅 텍스트입니다', - unreadChatCount: 15, - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '5', - role: '이모', - lastChat: '마지막 채팅 텍스트입니다', - unreadChatCount: 15, - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '6', - role: '이모', - lastChat: '마지막 채팅 텍스트입니다', - unreadChatCount: 15, - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '7', - role: '이모', - lastChat: '마지막 채팅 텍스트입니다', - unreadChatCount: 15, - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '8', - role: '이모', - lastChat: '마지막 채팅 텍스트입니다', - unreadChatCount: 15, - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '9', - role: '이모', - lastChat: '마지막 채팅 텍스트입니다', - unreadChatCount: 15, - }, - { - profileImg: '', - name: '감자탕수육', - gender: 'female', - userId: 'dummyUserId', - id: '10', - role: '이모', - lastChat: '마지막 채팅 텍스트입니다', - unreadChatCount: 15, - }, -] diff --git a/src/stores/modalStore.ts b/src/stores/modalStore.ts index 7c86205..c256267 100644 --- a/src/stores/modalStore.ts +++ b/src/stores/modalStore.ts @@ -31,6 +31,7 @@ export const useModalStore = create((set, get) => ({ clearModal: () => { set({ modalList: [] }) // 모든 모달 제거 시 히스토리 초기화 - window.history.go(-get().modalList.length) + const modalCount = get().modalList.length + if (modalCount) window.history.go(-modalCount) }, })) diff --git a/src/types/api.ts b/src/types/api.ts index af2051b..1ed6134 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -163,7 +163,7 @@ export type CommonAPIResponse = BasicInfo & dogName: string count: number memberInfo: Pick - members: Pick + members: Pick[] isMatched: BooleanString memberGender: Gender memberProfileImg: string diff --git a/src/types/common.ts b/src/types/common.ts index 85325aa..a7e6602 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1,5 +1,7 @@ +import { FAMILY_ROLE } from '~constants/familyRole' + export type Gender = 'MALE' | 'FEMALE' -export type FamilyRole = 'FATHER' | 'MOTHER' | 'SISTER' | 'BROTHER' +export type FamilyRole = keyof typeof FAMILY_ROLE export type DayOfWeek = 'MONDAY' | 'TUESDAY' | 'WEDNESDAY' | 'THURSDAY' | 'FRIDAY' | 'SATURDAY' | 'SUNDAY' export type NotificationType = 'WALK' | 'CHAT' | 'FRIEND' export type Provider = 'GOOGLE' | 'NAVER' | 'KAKAO' diff --git a/src/types/social.ts b/src/types/social.ts index c4290be..bd28305 100644 --- a/src/types/social.ts +++ b/src/types/social.ts @@ -1,21 +1 @@ export type SocialTabs = 'friendList' | 'dangTalk' - -export type FriendInfo = { - profileImg: string - name: string - gender: 'male' | 'female' - role: string - id: string - userId: string -} - -export type ChatInfo = { - profileImg: string - name: string - gender: 'male' | 'female' - role: string - lastChat: string - unreadChatCount: number - id: string - userId: string -} diff --git a/src/utils/calculateAge.ts b/src/utils/calculateAge.ts new file mode 100644 index 0000000..53b5e67 --- /dev/null +++ b/src/utils/calculateAge.ts @@ -0,0 +1,12 @@ +export function calculateAge(birthDate: string) { + const today = new Date() + const birthDateObj = new Date(birthDate) + let age = today.getFullYear() - birthDateObj.getFullYear() + const monthDiff = today.getMonth() - birthDateObj.getMonth() + + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDateObj.getDate())) { + age-- + } + + return age +}