Skip to content

Commit

Permalink
Merge pull request #132 from prgrms-web-devcourse-final-project/38-fe…
Browse files Browse the repository at this point in the history
…ature/notification-modal-data-binding

[Feature] #38 알림창 데이터 바인딩
  • Loading branch information
shlee9999 authored Dec 6, 2024
2 parents 94fb2f0 + 35a94c7 commit 043a6e3
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 133 deletions.
45 changes: 45 additions & 0 deletions src/apis/notification/fetchNotificationList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { AxiosError } from 'axios'
import { APIResponse, CommonAPIResponse, ErrorResponse, PaginationResponse } from '~types/api'
import { axiosInstance } from '~apis/axiosInstance'

export type FetchNotificationListRequest = {
page: number
}

export type FetchNotificationListResponse = PaginationResponse & {
content: Array<Pick<CommonAPIResponse, 'notificationId' | 'type' | 'content' | 'isRead' | 'memberId' | 'createdAt'>>
}

export const fetchNotificationList = async (
req: FetchNotificationListRequest
): Promise<APIResponse<FetchNotificationListResponse>> => {
try {
const { data } = await axiosInstance.get<APIResponse<FetchNotificationListResponse>>(`/notification/list`, {
params: req,
})
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('다시 시도해주세요')
}
}
18 changes: 18 additions & 0 deletions src/apis/notification/useInfiniteNotificationList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useSuspenseInfiniteQuery } from '@tanstack/react-query'
import { FetchNotificationListResponse, fetchNotificationList } from '~apis/notification/fetchNotificationList'
import { queryKey } from '~constants/queryKey'
import { APIResponse } from '~types/api'

export default function useInfiniteNotificationList() {
return useSuspenseInfiniteQuery<APIResponse<FetchNotificationListResponse>>({
queryKey: queryKey.notification(),
queryFn: async ({ pageParam = 0 }) => {
if (pageParam !== 0) await new Promise(resolve => setTimeout(resolve, 500))
return await fetchNotificationList({ page: pageParam as number })
},
getNextPageParam: lastPage => {
return lastPage.data.last ? undefined : lastPage.data.number + 1
},
initialPageParam: 0,
})
}
8 changes: 8 additions & 0 deletions src/components/InfiniteScrollTrigger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { styled } from 'styled-components'

type InfiniteScrollTriggerProps = {
$height?: number
}
export const InfiniteScrollTrigger = styled.div<InfiniteScrollTriggerProps>`
height: ${({ $height = 20 }) => $height + 'px'};
`
145 changes: 20 additions & 125 deletions src/components/NotificationList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,134 +1,29 @@
import useInfiniteNotificationList from '~apis/notification/useInfiniteNotificationList'
import Loader from '~components/Loader'
import NotificationItem from '~components/NotificationItem'
import useObserver from '~hooks/useObserver'
import * as S from './styles'
import { InfiniteScrollTrigger } from '~components/InfiniteScrollTrigger'

const notificationList = [
{
content: '엄마 산책 시켜주세요!',
date: new Date('2024-11-21 09:38:04'),
},
{
content: '오늘 빼먹지 말고 산책!',
date: new Date('2024-11-21 07:13:04'),
},
{
content: '다 함께 즐거운 순간!',
date: new Date('2024-11-21 01:32:04'),
},
{
content: '오늘 빼먹지 말고 산책!',
date: new Date('2024-11-21 11:51:04'),
},
{
content: '다 함께 즐거운 순간!',
date: new Date('2024-11-21 06:26:04'),
},
{
content: '오늘 빼먹지 말고 산책!',
date: new Date('2024-11-21 18:00:04'),
},
{
content: '오늘 빼먹지 말고 산책!',
date: new Date('2024-11-21 04:24:04'),
},
{
content: '오늘의 미션을 완료했어요!',
date: new Date('2024-11-21 16:10:04'),
},
{
content: '내일 날씨가 좋으면 산책해요!',
date: new Date('2024-11-21 13:44:04'),
},
{
content: '엄마 산책 시켜주세요!',
date: new Date('2024-11-21 18:35:04'),
},
{
content: '다 함께 즐거운 순간!',
date: new Date('2024-11-21 20:32:04'),
},
{
content: '내일 날씨가 좋으면 산책해요!',
date: new Date('2024-11-21 20:29:04'),
},
{
content: '이번 주 미션을 완료할 거예요!',
date: new Date('2024-11-21 16:24:04'),
},
{
content: '오늘 빼먹지 말고 산책!',
date: new Date('2024-11-21 04:00:04'),
},
{
content: '다 함께 즐거운 순간!',
date: new Date('2024-11-21 20:15:04'),
},
{
content: '오늘 기분이 어때요?',
date: new Date('2024-11-21 13:07:04'),
},
{
content: '엄마 나와 함께 놀아요!',
date: new Date('2024-11-21 12:04:04'),
},
{
content: '산책이 너무 좋아요!',
date: new Date('2024-11-21 07:04:04'),
},
{
content: '엄마 나와 함께 놀아요!',
date: new Date('2024-11-21 07:22:04'),
},
{
content: '오늘 기분이 어때요?',
date: new Date('2024-11-21 09:32:04'),
},
{
content: '오늘 빼먹지 말고 산책!',
date: new Date('2024-11-21 14:29:04'),
},
{
content: '엄마 나와 함께 놀아요!',
date: new Date('2024-11-21 11:32:04'),
},
{
content: '밥은 언제 먹어?',
date: new Date('2024-11-21 23:44:04'),
},
{
content: '내일 날씨가 좋으면 산책해요!',
date: new Date('2024-11-21 01:33:04'),
},
{
content: '다 함께 즐거운 순간!',
date: new Date('2024-11-21 19:34:04'),
},
{
content: '엄마 나와 함께 놀아요!',
date: new Date('2024-11-21 13:40:04'),
},
{
content: '이번 주 미션을 완료할 거예요!',
date: new Date('2024-11-21 14:05:04'),
},
{
content: '밥은 언제 먹어?',
date: new Date('2024-11-21 01:02:04'),
},
{
content: '이번 주 미션을 완료할 거예요!',
date: new Date('2024-11-21 04:02:04'),
},
{
content: '엄마 산책 시켜주세요!',
date: new Date('2024-11-21 22:11:04'),
},
]
export default function NotificationList() {
const { observerRef } = useObserver<HTMLDivElement>({
callback: () => hasNextPage && !isFetchingNextPage && fetchNextPage(),
})

const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteNotificationList()

return (
<S.NotificationList>
{notificationList.map((notification, index) => (
<NotificationItem content={notification.content} date={notification.date} key={index} />
))}
{data?.pages.map(page =>
page.data?.content.map(notification => (
<NotificationItem
key={notification.notificationId}
content={notification.content}
date={new Date(notification.createdAt)}
/>
))
)}
<InfiniteScrollTrigger ref={observerRef}>{isFetchingNextPage && <Loader />}</InfiniteScrollTrigger>
</S.NotificationList>
)
}
1 change: 1 addition & 0 deletions src/constants/queryKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export const queryKey = {
},
profile: (memberId: number) => ['profile', memberId],
home: () => ['homePageData'],
notification: () => ['notification'],
}
32 changes: 32 additions & 0 deletions src/hooks/useObserver.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useRef, useCallback, useEffect } from 'react'

type UseObserverProps = {
callback: () => void
}

export default function useObserver<T extends HTMLElement>({ callback }: UseObserverProps) {
const observerRef = useRef<T>(null)
const handleObserver = useCallback(
(entries: IntersectionObserverEntry[]) => {
const [target] = entries
if (target.isIntersecting) {
callback()
}
},
[callback]
)

useEffect(() => {
const element = observerRef.current
const option = { threshold: 0.5 }

const observer = new IntersectionObserver(handleObserver, option)
if (element) observer.observe(element)

return () => {
if (element) observer.unobserve(element)
}
}, [handleObserver])

return { observerRef }
}
15 changes: 14 additions & 1 deletion src/modals/NotificationModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,26 @@ import Header from '~components/Header'
import NotificationList from '~components/NotificationList'
import { useModalStore } from '~stores/modalStore'
import * as S from './styles'
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { Suspense } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import ErrorFallback from '~components/ErrorFallback'
import Loader from '~components/Loader'

export default function NotificationModal() {
const { popModal } = useModalStore()
return (
<S.NotificationModal>
<Header prevBtn onClickPrev={popModal} type='sm' title='알림' />
<NotificationList />
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={reset}>
<Suspense fallback={<Loader />}>
<NotificationList />
</Suspense>
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
</S.NotificationModal>
)
}
51 changes: 44 additions & 7 deletions src/types/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChatType, Gender, FamilyRole, DayOfWeek, Provider, BooleanString, Time } from '~types/common'
import { ChatType, Gender, FamilyRole, DayOfWeek, Provider, BooleanString, Time, NotificationType } from '~types/common'

export type APIResponse<T> = {
code: number
Expand All @@ -18,6 +18,13 @@ export type BasicInfo = {
gender: Gender
}

export type TimeStamp = {
/** 생성 시간 @example "2024-12-03T08:17:04.717Z" */
createdAt: string
/** 업데이트 시간 @example "2024-12-03T08:17:04.717Z" */
updatedAt: string
}

export type Dog = BasicInfo & {
/** 강아지 ID @example 1 */
dogId: number
Expand Down Expand Up @@ -106,7 +113,7 @@ export type Walk = {
totalDistanceInKilometers: number
}

export type Chat = {
export type Chat = TimeStamp & {
/** 채팅 ID @example 123 */
chatId: number
/** 채팅방 ID @example 3 */
Expand All @@ -123,10 +130,6 @@ export type Chat = {
readMessageIds: null
/** 읽지 않은 메시지 수 @example 3 */
unreadMessageCount: number
/** 생성 시간 @example "2024-12-03T08:17:04.717Z" */
createdAt: string
/** 업데이트 시간 @example "2024-12-03T08:17:04.717Z" */
updatedAt: string
}

export type Family = {
Expand All @@ -147,14 +150,21 @@ export type CommonAPIRequest = Member &
totalWalkTimeSecond: number
}

export type Notification = TimeStamp & {
notificationId: number
type: NotificationType
content: string
}

export type CommonAPIResponse = BasicInfo &
Member &
Chat &
Family &
Walk &
Position &
Dog &
OtherDog & {
OtherDog &
Notification & {
dog: Dog
dogs: Dog[]
dogWalkCount: number
Expand All @@ -172,3 +182,30 @@ export type CommonAPIResponse = BasicInfo &
positionList: Position[]
memberEmail: string
}

//* Pagination 관련
export type SortInfo = {
empty: boolean
sorted: boolean
unsorted: boolean
}

export type PageableInfo = {
offset: number
sort: SortInfo
pageSize: number
paged: boolean
pageNumber: number
unpaged: boolean
}

export type PaginationResponse = {
size: number
number: number
numberOfElements: number
first: boolean
last: boolean
empty: boolean
sort: SortInfo
pageable: PageableInfo
}

0 comments on commit 043a6e3

Please sign in to comment.