Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] #44 소셜, 프로필 페이지 데이터 바인딩 #126

Merged
merged 23 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
47a1f28
🐛Fix: ChatInfo
shlee9999 Dec 3, 2024
058ae01
🔨Setting: API
shlee9999 Dec 3, 2024
975780f
✨Feat: 채팅방 데이터 바인딩
shlee9999 Dec 3, 2024
e0770b8
✨Feat: createChatRoom
shlee9999 Dec 4, 2024
784bf50
✨Feat: 친구목록 데이터 바인딩
shlee9999 Dec 4, 2024
4b8c201
✨Feat: familyRole
shlee9999 Dec 4, 2024
3ee5a86
🎨Design: ChatItem UnreadChatCount
shlee9999 Dec 4, 2024
5a65d1a
✨Feat: 프로필 페이지 데이터 바인딩 (임시로 마이페이지 API로 작성)
shlee9999 Dec 4, 2024
278682b
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
shlee9999 Dec 4, 2024
a7be0c6
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
shlee9999 Dec 4, 2024
16dc678
✨Feat: useCreateChatRoom
shlee9999 Dec 4, 2024
f3fa97b
✨Feat: useSocialData
shlee9999 Dec 4, 2024
a40d450
🐛Fix: chatRoomId DOM 에러
shlee9999 Dec 4, 2024
e56dc0d
✨Feat: useFetchProfile
shlee9999 Dec 4, 2024
0a61c61
✨Feat: ChatItem createRoom 적용
shlee9999 Dec 4, 2024
25e84d5
🐛Fix: ProfilePage 깜빡임
shlee9999 Dec 4, 2024
9899f73
🐛Fix: FamilyDDangPage DogProfile props
shlee9999 Dec 4, 2024
8d94b72
✨Feat: ProfilePage meta data
shlee9999 Dec 4, 2024
68aa435
✨Feat: lighthouse에 profile url 등록
shlee9999 Dec 4, 2024
bfffe48
✨Feat: ErrorBoundary, Suspense
shlee9999 Dec 5, 2024
a85bed3
✨Feat: 프로필 페이지 ErrorBoundary, Suspense 적용
shlee9999 Dec 5, 2024
d2d49d5
♻️Refactor: else문 제거
shlee9999 Dec 5, 2024
819f3e2
✨Feat: queryKey
shlee9999 Dec 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .vscode/typescript.code-snippets
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@
" default:",
" throw new Error(message || '알 수 없는 오류가 발생했습니다.')",
" }",
" } else {",
" // 요청 자체가 실패한 경우",
" throw new Error('네트워크 연결을 확인해주세요')",
" }",
" } ",
" // 요청 자체가 실패한 경우",
" throw new Error('네트워크 연결을 확인해주세요')",
" ",
" }",
"",
" console.error('예상치 못한 에러:', error);",
Expand Down
1 change: 1 addition & 0 deletions lighthouserc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,19 @@
"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",
"ios-style-picker": "^0.0.6",
"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",
Expand All @@ -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",
Expand Down
48 changes: 48 additions & 0 deletions src/apis/chatRoom/createChatRoom.ts
Original file line number Diff line number Diff line change
@@ -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<APIResponse<CreateChatRoomResponse>> => {
try {
const { data } = await axiosInstance.post<APIResponse<CreateChatRoomResponse>>(`/chat/rooms`, req)
console.log(data.message || '채팅방 생성 성공')
console.log(data.data)
return data
} catch (error) {
if (error instanceof AxiosError) {
const { response } = error as AxiosError<ErrorResponse>

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('다시 시도해주세요')
}
}
37 changes: 37 additions & 0 deletions src/apis/chatRoom/fetchChatRoomList.ts
Original file line number Diff line number Diff line change
@@ -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<CommonAPIResponse, 'chatRoomId' | 'name' | 'lastMessage' | 'unreadMessageCount' | 'members'>
>

export const fetchChatRoomList = async (): Promise<APIResponse<FetchChatRoomListResponse>> => {
try {
const { data } = await axiosInstance.get<APIResponse<FetchChatRoomListResponse>>(`/chat/rooms`)
return data
} catch (error) {
if (error instanceof AxiosError) {
const { response } = error as AxiosError<ErrorResponse>

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('다시 시도해주세요')
}
}
31 changes: 31 additions & 0 deletions src/apis/chatRoom/useCreateChatRoom.tsx
Original file line number Diff line number Diff line change
@@ -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<CreateChatRoomResponse>,
Error,
CreateChatRoomRequest
> & {
createRoom: (req: CreateChatRoomRequest) => void
} => {
const { pushModal } = useModalStore()

const mutation = useMutation<APIResponse<CreateChatRoomResponse>, Error, CreateChatRoomRequest>({
mutationFn: createChatRoom,
onSuccess: (data, { opponentMemberId }) => {
console.log(data.message || '채팅방 생성 성공')
pushModal(<ChatModal chatRoomId={data.data.chatRoomId} userId={opponentMemberId} />)
},
onError: error => {
console.error('채팅방 생성 실패:', error.message)
},
})

return {
...mutation,
createRoom: mutation.mutate,
}
}
28 changes: 28 additions & 0 deletions src/apis/chatRoom/useSocialData.tsx
Original file line number Diff line number Diff line change
@@ -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,
}
}
37 changes: 37 additions & 0 deletions src/apis/friend/fetchFriendList.ts
Original file line number Diff line number Diff line change
@@ -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<CommonAPIResponse, 'memberId' | 'gender' | 'familyRole' | 'profileImg' | 'name'>
>

export const fetchFriendList = async (): Promise<APIResponse<FetchFriendListResponse>> => {
try {
const { data } = await axiosInstance.get<APIResponse<FetchFriendListResponse>>(`/friend`)
return data
} catch (error) {
if (error instanceof AxiosError) {
const { response } = error as AxiosError<ErrorResponse>

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('다시 시도해주세요')
}
}
49 changes: 49 additions & 0 deletions src/apis/member/fetchProfile.ts
Original file line number Diff line number Diff line change
@@ -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<CommonAPIRequest, 'memberId'>

export type FetchProfileResponse = Pick<
CommonAPIResponse,
| 'memberId'
| 'name'
| 'address'
| 'gender'
| 'familyRole'
| 'profileImg'
| 'totalDistance'
| 'walkCount'
| 'countWalksWithMember'
| 'dog'
>

export const fetchProfile = async ({ memberId }: FetchProfileRequest): Promise<APIResponse<FetchProfileResponse>> => {
try {
const { data } = await axiosInstance.get<APIResponse<FetchProfileResponse>>(`/member/${memberId}`)
return data
} catch (error) {
if (error instanceof AxiosError) {
const { response } = error as AxiosError<ErrorResponse>

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('다시 시도해주세요')
}
}
11 changes: 11 additions & 0 deletions src/apis/member/useFetchProfile.tsx
Original file line number Diff line number Diff line change
@@ -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<FetchProfileResponse, Error> => {
return useSuspenseQuery<FetchProfileResponse, Error>({
queryKey: queryKey.profile(memberId),
queryFn: () => fetchProfile({ memberId }).then(data => data.data),
staleTime: 1000 * 60 * 5, // 5분
})
}
32 changes: 12 additions & 20 deletions src/components/DogProfile/index.tsx
Original file line number Diff line number Diff line change
@@ -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<Dog, 'birthDate' | 'breed' | 'comment' | 'gender' | 'name' | 'profileImg'>

export default function DogProfile({ birthDate, breed, comment, gender, name, profileImg }: DogProfileProps) {
return (
<S.DogInfoArea>
<S.DogInfoWrapper>
<Profile $size={80} $src={dogInfo.profileImg} />
<Profile $size={80} $src={profileImg} />
<S.DogDetailWrapper>
<S.TypoWrapper>
<Typo20 $weight='700'>{dogInfo.name}</Typo20>
<Typo15 $weight='400'>{dogInfo.breed}</Typo15>
<Typo20 $weight='700'>{name}</Typo20>
<Typo15 $weight='400'>{breed}</Typo15>
<Separator $height={8} />
<Typo15 $weight='400'>{dogInfo.age}살</Typo15>
<Typo15 $weight='400'>{calculateAge(birthDate)}살</Typo15>
<Separator $height={8} />
<Typo15 $weight='400'>{dogInfo.gender === 'male' ? '남' : '여'}</Typo15>
<Typo15 $weight='400'>{gender === 'MALE' ? '남' : '여'}</Typo15>
</S.TypoWrapper>
<S.TypoWrapper>
<Typo13>중성화 X</Typo13>
Expand All @@ -30,21 +34,9 @@ export default function DogProfile() {
우리 댕댕이를 소개해요!
</Typo15>
<Typo13 $weight='500' style={{ lineHeight: 1.2 }}>
{dogInfo.intro}
{comment}
</Typo13>
</S.OneLineIntro>
</S.DogInfoArea>
)
}

const dogInfo = {
name: '밤톨이',
breed: '포메라니안',
age: 4,
gender: 'male',
neutered: false, // 중성화 여부
weight: 3.4,
profileImg: '',
intro: `우리아이 안 물어요 착해요.
강아지껌을 너무 좋아해요 같이 놀아요. `,
}
Loading
Loading