From 8646b42596bab5f8031f9fce0f68ef7a050978bd Mon Sep 17 00:00:00 2001 From: mina-gwak Date: Sat, 28 Jun 2025 23:07:15 +0900 Subject: [PATCH 1/4] =?UTF-8?q?Fix:=20PD-287=20GNB=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(business)/business/layout.tsx | 2 +- app/(guest)/layout.tsx | 2 +- app/(user)/layout.tsx | 2 +- src/shared/ui/gnb/assets/Logo.svg => public/images/logo.svg | 0 src/shared/ui/gnb/index.ts | 3 --- src/shared/ui/index.ts | 1 - src/widgets/gnb/index.ts | 3 +++ src/{shared/ui/gnb => widgets/gnb/ui}/business-button.tsx | 4 +--- src/{shared/ui/gnb => widgets/gnb/ui}/business-gnb.tsx | 4 ++-- src/{shared/ui/gnb => widgets/gnb/ui}/gnb.tsx | 4 ++-- src/{shared/ui/gnb => widgets/gnb/ui}/guest-gnb.tsx | 2 +- src/{shared/ui/gnb => widgets/gnb/ui}/navigation.tsx | 0 src/{shared/ui/gnb => widgets/gnb/ui}/notification.tsx | 4 +--- src/{shared/ui/gnb => widgets/gnb/ui}/user-button.tsx | 4 +--- src/{shared/ui/gnb => widgets/gnb/ui}/variants.ts | 0 15 files changed, 14 insertions(+), 21 deletions(-) rename src/shared/ui/gnb/assets/Logo.svg => public/images/logo.svg (100%) delete mode 100644 src/shared/ui/gnb/index.ts create mode 100644 src/widgets/gnb/index.ts rename src/{shared/ui/gnb => widgets/gnb/ui}/business-button.tsx (97%) rename src/{shared/ui/gnb => widgets/gnb/ui}/business-gnb.tsx (97%) rename src/{shared/ui/gnb => widgets/gnb/ui}/gnb.tsx (96%) rename src/{shared/ui/gnb => widgets/gnb/ui}/guest-gnb.tsx (93%) rename src/{shared/ui/gnb => widgets/gnb/ui}/navigation.tsx (100%) rename src/{shared/ui/gnb => widgets/gnb/ui}/notification.tsx (94%) rename src/{shared/ui/gnb => widgets/gnb/ui}/user-button.tsx (97%) rename src/{shared/ui/gnb => widgets/gnb/ui}/variants.ts (100%) diff --git a/app/(business)/business/layout.tsx b/app/(business)/business/layout.tsx index ef513859..ebcc33fa 100644 --- a/app/(business)/business/layout.tsx +++ b/app/(business)/business/layout.tsx @@ -7,7 +7,7 @@ import { AppProviders } from 'app/providers' import { pretendard } from 'app/styles' import { GoogleAnalytics } from 'shared/config/google-analytics' import { cn } from 'shared/lib' -import { BusinessGNB } from 'shared/ui' +import { BusinessGNB } from 'widgets/gnb' import '../../globals.css' diff --git a/app/(guest)/layout.tsx b/app/(guest)/layout.tsx index d556a66f..eaf25cdb 100644 --- a/app/(guest)/layout.tsx +++ b/app/(guest)/layout.tsx @@ -6,7 +6,7 @@ import { ReactNode } from 'react' import { pretendard } from 'app/styles' import { GoogleAnalytics } from 'shared/config/google-analytics' import { cn } from 'shared/lib' -import { GuestGNB } from 'shared/ui' +import { GuestGNB } from 'widgets/gnb' import '../globals.css' diff --git a/app/(user)/layout.tsx b/app/(user)/layout.tsx index 8ea0977c..16513646 100644 --- a/app/(user)/layout.tsx +++ b/app/(user)/layout.tsx @@ -7,7 +7,7 @@ import { AppProviders } from 'app/providers' import { pretendard } from 'app/styles' import { GoogleAnalytics } from 'shared/config/google-analytics' import { cn } from 'shared/lib' -import { GNB } from 'shared/ui' +import { GNB } from 'widgets/gnb' import '../globals.css' diff --git a/src/shared/ui/gnb/assets/Logo.svg b/public/images/logo.svg similarity index 100% rename from src/shared/ui/gnb/assets/Logo.svg rename to public/images/logo.svg diff --git a/src/shared/ui/gnb/index.ts b/src/shared/ui/gnb/index.ts deleted file mode 100644 index 9b629bbc..00000000 --- a/src/shared/ui/gnb/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { GNB } from './gnb' -export { BusinessGNB } from './business-gnb' -export { GuestGNB } from './guest-gnb' diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index 275bb7b4..ad1e3dfa 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -7,7 +7,6 @@ export { DatePicker } from './date-picker' export { Dropdown } from './dropdown' export { ErrorPage } from './error/error-page' export { Filter, type FilterValue } from './filter' -export { GNB, BusinessGNB, GuestGNB } from './gnb' export { Heading } from './heading' export { HelperText, type HelperTextProps } from './helper-text' export { Icon } from './icon' diff --git a/src/widgets/gnb/index.ts b/src/widgets/gnb/index.ts new file mode 100644 index 00000000..cf48e3c0 --- /dev/null +++ b/src/widgets/gnb/index.ts @@ -0,0 +1,3 @@ +export { GNB } from './ui/gnb' +export { BusinessGNB } from './ui/business-gnb' +export { GuestGNB } from './ui/guest-gnb' diff --git a/src/shared/ui/gnb/business-button.tsx b/src/widgets/gnb/ui/business-button.tsx similarity index 97% rename from src/shared/ui/gnb/business-button.tsx rename to src/widgets/gnb/ui/business-button.tsx index 04498352..37518a9d 100644 --- a/src/shared/ui/gnb/business-button.tsx +++ b/src/widgets/gnb/ui/business-button.tsx @@ -8,9 +8,7 @@ import { businessSignOut } from 'entities/auth' import { colors, type Locale } from 'shared/config' import { useDropdown } from 'shared/lib' import { setLocale } from 'shared/server-lib' - -import { Dropdown } from '../dropdown' -import { Icon } from '../icon' +import { Dropdown, Icon } from 'shared/ui' export const BusinessButton = () => { const router = useRouter() diff --git a/src/shared/ui/gnb/business-gnb.tsx b/src/widgets/gnb/ui/business-gnb.tsx similarity index 97% rename from src/shared/ui/gnb/business-gnb.tsx rename to src/widgets/gnb/ui/business-gnb.tsx index a795bfee..0caa62be 100644 --- a/src/shared/ui/gnb/business-gnb.tsx +++ b/src/widgets/gnb/ui/business-gnb.tsx @@ -4,10 +4,10 @@ import { useRouter } from 'next/navigation' import { useSession } from 'next-auth/react' import { useTranslations } from 'next-intl' +import Logo from 'public/images/logo.svg' import { cn } from 'shared/lib' +import { Button } from 'shared/ui' -import { Button } from '../button' -import Logo from './assets/Logo.svg' import { BusinessButton } from './business-button' import * as Navigation from './navigation' import { NotificationButton } from './notification' diff --git a/src/shared/ui/gnb/gnb.tsx b/src/widgets/gnb/ui/gnb.tsx similarity index 96% rename from src/shared/ui/gnb/gnb.tsx rename to src/widgets/gnb/ui/gnb.tsx index bb55b92e..fba1b472 100644 --- a/src/shared/ui/gnb/gnb.tsx +++ b/src/widgets/gnb/ui/gnb.tsx @@ -4,10 +4,10 @@ import { useRouter } from 'next/navigation' import { useSession } from 'next-auth/react' import { useTranslations } from 'next-intl' +import Logo from 'public/images/logo.svg' import { cn } from 'shared/lib' +import { Button } from 'shared/ui' -import { Button } from '../button' -import Logo from './assets/Logo.svg' import * as Navigation from './navigation' import { NotificationButton } from './notification' import { UserButton } from './user-button' diff --git a/src/shared/ui/gnb/guest-gnb.tsx b/src/widgets/gnb/ui/guest-gnb.tsx similarity index 93% rename from src/shared/ui/gnb/guest-gnb.tsx rename to src/widgets/gnb/ui/guest-gnb.tsx index 1b8aeb88..f18ef215 100644 --- a/src/shared/ui/gnb/guest-gnb.tsx +++ b/src/widgets/gnb/ui/guest-gnb.tsx @@ -2,9 +2,9 @@ import { useRouter } from 'next/navigation' +import Logo from 'public/images/logo.svg' import { cn } from 'shared/lib' -import Logo from './assets/Logo.svg' import * as css from './variants' export const GuestGNB = () => { diff --git a/src/shared/ui/gnb/navigation.tsx b/src/widgets/gnb/ui/navigation.tsx similarity index 100% rename from src/shared/ui/gnb/navigation.tsx rename to src/widgets/gnb/ui/navigation.tsx diff --git a/src/shared/ui/gnb/notification.tsx b/src/widgets/gnb/ui/notification.tsx similarity index 94% rename from src/shared/ui/gnb/notification.tsx rename to src/widgets/gnb/ui/notification.tsx index cb62f050..0a55f1a3 100644 --- a/src/shared/ui/gnb/notification.tsx +++ b/src/widgets/gnb/ui/notification.tsx @@ -2,9 +2,7 @@ import { useTranslations } from 'next-intl' import { colors } from 'shared/config' import { useDropdown } from 'shared/lib' - -import { Dropdown } from '../dropdown' -import { Icon } from '../icon' +import { Dropdown, Icon } from 'shared/ui' export const NotificationButton = () => { const t = useTranslations() diff --git a/src/shared/ui/gnb/user-button.tsx b/src/widgets/gnb/ui/user-button.tsx similarity index 97% rename from src/shared/ui/gnb/user-button.tsx rename to src/widgets/gnb/ui/user-button.tsx index 94bb94ad..bddea469 100644 --- a/src/shared/ui/gnb/user-button.tsx +++ b/src/widgets/gnb/ui/user-button.tsx @@ -8,9 +8,7 @@ import { teacherSignOut } from 'entities/auth' import { colors, type Locale } from 'shared/config' import { useDropdown } from 'shared/lib' import { setLocale } from 'shared/server-lib' - -import { Dropdown } from '../dropdown' -import { Icon } from '../icon' +import { Dropdown, Icon } from 'shared/ui' export const UserButton = () => { const router = useRouter() diff --git a/src/shared/ui/gnb/variants.ts b/src/widgets/gnb/ui/variants.ts similarity index 100% rename from src/shared/ui/gnb/variants.ts rename to src/widgets/gnb/ui/variants.ts From 745d3004965ba7d32f60f4d56d799aece4f25da5 Mon Sep 17 00:00:00 2001 From: mina-gwak Date: Sun, 29 Jun 2025 23:14:27 +0900 Subject: [PATCH 2/4] =?UTF-8?q?Feat:=20PD-287=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EB=A0=8C=EB=8D=94=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/api/get-notifications.ts | 42 +++++++ src/entities/notification/api/query.ts | 35 ++++++ src/entities/notification/index.ts | 1 + .../notification/model/notification.ts | 9 ++ .../gnb/api/get-business-notifications.ts | 15 +++ .../gnb/api/get-teacher-notifications.ts | 15 +++ src/widgets/gnb/lib/format-date.ts | 28 +++++ src/widgets/gnb/ui/business-gnb.tsx | 4 +- .../gnb/ui/business-notification/button.tsx | 51 ++++++++ .../gnb/ui/business-notification/list.tsx | 107 +++++++++++++++++ src/widgets/gnb/ui/gnb.tsx | 2 +- .../gnb/ui/teacher-notification/button.tsx | 51 ++++++++ .../gnb/ui/teacher-notification/list.tsx | 109 ++++++++++++++++++ 13 files changed, 465 insertions(+), 4 deletions(-) create mode 100644 src/entities/notification/api/get-notifications.ts create mode 100644 src/entities/notification/api/query.ts create mode 100644 src/entities/notification/model/notification.ts create mode 100644 src/widgets/gnb/api/get-business-notifications.ts create mode 100644 src/widgets/gnb/api/get-teacher-notifications.ts create mode 100644 src/widgets/gnb/lib/format-date.ts create mode 100644 src/widgets/gnb/ui/business-notification/button.tsx create mode 100644 src/widgets/gnb/ui/business-notification/list.tsx create mode 100644 src/widgets/gnb/ui/teacher-notification/button.tsx create mode 100644 src/widgets/gnb/ui/teacher-notification/list.tsx diff --git a/src/entities/notification/api/get-notifications.ts b/src/entities/notification/api/get-notifications.ts new file mode 100644 index 00000000..c3b6cefa --- /dev/null +++ b/src/entities/notification/api/get-notifications.ts @@ -0,0 +1,42 @@ +'use server' + +import { getBusinessSession, getTeacherSession } from 'entities/auth' +import { apiClient, Pagination, PaginationParams } from 'shared/api' + +import { Notification } from '../model/notification' + +export type GetNotificationsRequest = PaginationParams + +type GetNotificationsResponse = Pagination + +export const getTeacherNotifications = async ( + queryParams: GetNotificationsRequest, +) => { + const { accessToken } = await getTeacherSession() + + const response = await apiClient.get({ + endpoint: '/notifications', + queryParams, + option: { + authorization: `Bearer ${accessToken}`, + }, + }) + + return response +} + +export const getBusinessNotifications = async ( + queryParams: GetNotificationsRequest, +) => { + const { accessToken } = await getBusinessSession() + + const response = await apiClient.get({ + endpoint: '/notifications', + queryParams, + option: { + authorization: `Bearer ${accessToken}`, + }, + }) + + return response +} diff --git a/src/entities/notification/api/query.ts b/src/entities/notification/api/query.ts new file mode 100644 index 00000000..eb1efb59 --- /dev/null +++ b/src/entities/notification/api/query.ts @@ -0,0 +1,35 @@ +import { infiniteQueryOptions } from '@tanstack/react-query' + +import { + getBusinessNotifications, + getTeacherNotifications, +} from './get-notifications' + +export const notificationQueries = { + all: () => ['notification'], + lists: () => [...notificationQueries.all(), 'list'], + teacherList: () => + infiniteQueryOptions({ + queryKey: [...notificationQueries.lists(), 'teacher'], + queryFn: ({ pageParam = 0 }) => + getTeacherNotifications({ pageNumber: pageParam, rowCount: 10 }), + initialPageParam: 0, + getNextPageParam: lastPage => { + if (lastPage.last) return undefined + + return lastPage.pageable.pageNumber + 1 + }, + }), + businessList: () => + infiniteQueryOptions({ + queryKey: [...notificationQueries.lists(), 'business'], + queryFn: ({ pageParam = 0 }) => + getBusinessNotifications({ pageNumber: pageParam, rowCount: 10 }), + initialPageParam: 0, + getNextPageParam: lastPage => { + if (lastPage.last) return undefined + + return lastPage.pageable.pageNumber + 1 + }, + }), +} diff --git a/src/entities/notification/index.ts b/src/entities/notification/index.ts index 63acf59d..4dca1f8d 100644 --- a/src/entities/notification/index.ts +++ b/src/entities/notification/index.ts @@ -6,4 +6,5 @@ export { updateTeacherNotificationSetting, updateBusinessNotificationSetting, } from './api/update-notification-setting' +export { notificationQueries } from './api/query' export type { NotificationSetting } from './model/setting' diff --git a/src/entities/notification/model/notification.ts b/src/entities/notification/model/notification.ts new file mode 100644 index 00000000..eb44823b --- /dev/null +++ b/src/entities/notification/model/notification.ts @@ -0,0 +1,9 @@ +export type Notification = { + id: number + title: string + titleEn: string + content: string + contentEn: string + targetUrl: string + createdAt: string +} diff --git a/src/widgets/gnb/api/get-business-notifications.ts b/src/widgets/gnb/api/get-business-notifications.ts new file mode 100644 index 00000000..5320e306 --- /dev/null +++ b/src/widgets/gnb/api/get-business-notifications.ts @@ -0,0 +1,15 @@ +import { useInfiniteQuery } from '@tanstack/react-query' + +import { notificationQueries } from 'entities/notification' +import { useEmptyBoundary } from 'shared/api' + +export const useBusinessNotifications = () => { + const result = useInfiniteQuery({ + ...notificationQueries.businessList(), + select: data => data?.pages.flatMap(page => page.content), + }) + + useEmptyBoundary(result.data) + + return result +} diff --git a/src/widgets/gnb/api/get-teacher-notifications.ts b/src/widgets/gnb/api/get-teacher-notifications.ts new file mode 100644 index 00000000..3cddf9ac --- /dev/null +++ b/src/widgets/gnb/api/get-teacher-notifications.ts @@ -0,0 +1,15 @@ +import { useInfiniteQuery } from '@tanstack/react-query' + +import { notificationQueries } from 'entities/notification' +import { useEmptyBoundary } from 'shared/api' + +export const useTeacherNotifications = () => { + const result = useInfiniteQuery({ + ...notificationQueries.teacherList(), + select: data => data?.pages.flatMap(page => page.content), + }) + + useEmptyBoundary(result.data) + + return result +} diff --git a/src/widgets/gnb/lib/format-date.ts b/src/widgets/gnb/lib/format-date.ts new file mode 100644 index 00000000..143be5a0 --- /dev/null +++ b/src/widgets/gnb/lib/format-date.ts @@ -0,0 +1,28 @@ +import { format, differenceInMinutes, differenceInHours } from 'date-fns' +import { enUS, ko } from 'date-fns/locale' + +export const formatNotificationDate = ( + createdAt: string | Date, + locale: 'ko' | 'en' = 'ko', +): string => { + const now = new Date() + const created = + typeof createdAt === 'string' ? new Date(createdAt) : createdAt + + const diffMinutes = differenceInMinutes(now, created) + const diffHours = differenceInHours(now, created) + + if (diffMinutes < 1) { + return locale === 'ko' ? '1분 전' : '1minute ago' + } + if (diffMinutes < 60) { + return locale === 'ko' ? `${diffMinutes}분 전` : `${diffMinutes}minutes ago` + } + if (diffHours < 24) { + return locale === 'ko' ? `${diffHours}시간 전` : `${diffHours}hours ago` + } + + return format(created, 'yyyy.MM.dd(eee)', { + locale: locale === 'ko' ? ko : enUS, + }) +} diff --git a/src/widgets/gnb/ui/business-gnb.tsx b/src/widgets/gnb/ui/business-gnb.tsx index 0caa62be..975d0036 100644 --- a/src/widgets/gnb/ui/business-gnb.tsx +++ b/src/widgets/gnb/ui/business-gnb.tsx @@ -9,8 +9,8 @@ import { cn } from 'shared/lib' import { Button } from 'shared/ui' import { BusinessButton } from './business-button' +import { NotificationButton } from './business-notification/button' import * as Navigation from './navigation' -import { NotificationButton } from './notification' import * as css from './variants' export const BusinessGNB = () => { @@ -19,8 +19,6 @@ export const BusinessGNB = () => { const t = useTranslations() - const isDev = process.env.NODE_ENV === 'development' - const handleLogoClick = () => { router.push('/business') } diff --git a/src/widgets/gnb/ui/business-notification/button.tsx b/src/widgets/gnb/ui/business-notification/button.tsx new file mode 100644 index 00000000..a304bcc6 --- /dev/null +++ b/src/widgets/gnb/ui/business-notification/button.tsx @@ -0,0 +1,51 @@ +import { useQueryClient } from '@tanstack/react-query' +import { useEffect } from 'react' + +import { notificationQueries } from 'entities/notification' +import { EmptyBoundary } from 'shared/api' +import { colors } from 'shared/config' +import { useDropdown } from 'shared/lib' +import { Dropdown, Icon } from 'shared/ui' + +import { BusinessNotificationList, NoNotification } from './list' + +export const NotificationButton = () => { + const queryClient = useQueryClient() + + const { isOpen, toggleIsOpen, close, dropdownRef } = useDropdown() + + const handleClick = () => { + toggleIsOpen() + } + + useEffect(() => { + if (!isOpen) { + void queryClient.resetQueries({ + queryKey: notificationQueries.businessList().queryKey, + }) + } + }, [isOpen, queryClient]) + + return ( +
+ + {isOpen && ( + + }> + + + + )} +
+ ) +} diff --git a/src/widgets/gnb/ui/business-notification/list.tsx b/src/widgets/gnb/ui/business-notification/list.tsx new file mode 100644 index 00000000..e91d5079 --- /dev/null +++ b/src/widgets/gnb/ui/business-notification/list.tsx @@ -0,0 +1,107 @@ +import { useRouter } from 'next/navigation' + +import { cn } from 'shared/lib' +import { Dropdown } from 'shared/ui' + +import { useBusinessNotifications } from '../../api/get-business-notifications' +import { formatNotificationDate } from '../../lib/format-date' + +type Props = { + close: () => void +} + +export const BusinessNotificationList = ({ close }: Props) => { + const router = useRouter() + + const { data, isLoading, hasNextPage, fetchNextPage } = + useBusinessNotifications() + + if (isLoading) { + return + } + + const isLastItem = (index: number) => { + if (!data) return false + + return index === data.length - 1 + } + + return ( + <> + {data?.map((notification, index) => ( + { + if (notification.targetUrl) { + router.push(notification.targetUrl) + close() + } + }} + > +
+
+

+ {notification.title} +

+

+ {notification.content} +

+
+

+ {formatNotificationDate(notification.createdAt, 'ko')} +

+
+
+ ))} + {(() => { + if (hasNextPage) { + return ( + + + + ) + } + + return ( + +

+ 최근 90일간 받은 알림을 모두 표시했어요 +

+
+ ) + })()} + + ) +} + +export const NoNotification = () => { + return ( + +

+ 아직 새로운 알림이 없어요 +

+
+ ) +} + +export const NotificationSkeleton = () => { + return ( + +
+
+
+
+
+
+
+ + ) +} diff --git a/src/widgets/gnb/ui/gnb.tsx b/src/widgets/gnb/ui/gnb.tsx index fba1b472..edacc298 100644 --- a/src/widgets/gnb/ui/gnb.tsx +++ b/src/widgets/gnb/ui/gnb.tsx @@ -9,7 +9,7 @@ import { cn } from 'shared/lib' import { Button } from 'shared/ui' import * as Navigation from './navigation' -import { NotificationButton } from './notification' +import { NotificationButton } from './teacher-notification/button' import { UserButton } from './user-button' import * as css from './variants' diff --git a/src/widgets/gnb/ui/teacher-notification/button.tsx b/src/widgets/gnb/ui/teacher-notification/button.tsx new file mode 100644 index 00000000..4f5b31f6 --- /dev/null +++ b/src/widgets/gnb/ui/teacher-notification/button.tsx @@ -0,0 +1,51 @@ +import { useQueryClient } from '@tanstack/react-query' +import { useEffect } from 'react' + +import { notificationQueries } from 'entities/notification' +import { EmptyBoundary } from 'shared/api' +import { colors } from 'shared/config' +import { useDropdown } from 'shared/lib' +import { Dropdown, Icon } from 'shared/ui' + +import { TeacherNotificationList, NoNotification } from './list' + +export const NotificationButton = () => { + const queryClient = useQueryClient() + + const { isOpen, toggleIsOpen, close, dropdownRef } = useDropdown() + + const handleClick = () => { + toggleIsOpen() + } + + useEffect(() => { + if (!isOpen) { + void queryClient.resetQueries({ + queryKey: notificationQueries.teacherList().queryKey, + }) + } + }, [isOpen, queryClient]) + + return ( +
+ + {isOpen && ( + + }> + + + + )} +
+ ) +} diff --git a/src/widgets/gnb/ui/teacher-notification/list.tsx b/src/widgets/gnb/ui/teacher-notification/list.tsx new file mode 100644 index 00000000..9141f8a8 --- /dev/null +++ b/src/widgets/gnb/ui/teacher-notification/list.tsx @@ -0,0 +1,109 @@ +import { useRouter } from 'next/navigation' + +import { cn } from 'shared/lib' +import { Dropdown } from 'shared/ui' + +import { useTeacherNotifications } from '../../api/get-teacher-notifications' +import { formatNotificationDate } from '../../lib/format-date' + +type Props = { + close: () => void +} + +export const TeacherNotificationList = ({ close }: Props) => { + const router = useRouter() + + const { data, isLoading, hasNextPage, fetchNextPage } = + useTeacherNotifications() + + if (isLoading) { + return + } + + const isLastItem = (index: number) => { + if (!data) return false + + return index === data.length - 1 + } + + return ( + <> + {data?.map((notification, index) => ( + { + if (notification.targetUrl) { + router.push(notification.targetUrl) + close() + } + }} + > +
+
+

+ {notification.titleEn} +

+

+ {notification.contentEn} +

+
+

+ {formatNotificationDate(notification.createdAt, 'en')} +

+
+
+ ))} + {(() => { + if (hasNextPage) { + return ( + + + + ) + } + + return ( + +

+ All notifications received +
+ in the past 90 days have been displayed +

+
+ ) + })()} + + ) +} + +export const NoNotification = () => { + return ( + +

+ You have no new notifications yet +

+
+ ) +} + +export const NotificationSkeleton = () => { + return ( + +
+
+
+
+
+
+
+ + ) +} From 9ce19b302b9ae08347495e0121f3b2022979471d Mon Sep 17 00:00:00 2001 From: mina-gwak Date: Sun, 29 Jun 2025 23:40:22 +0900 Subject: [PATCH 3/4] =?UTF-8?q?Feat:=20PD-287=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EC=9A=B4=20=EC=95=8C=EB=A6=BC=20=ED=91=9C=EC=8B=9C=20=EB=B0=8F?= =?UTF-8?q?=20=EC=9D=BD=EC=9D=8C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/get-notification-unread-count.ts | 34 +++++++++++++++++++ src/entities/notification/api/query.ts | 16 ++++++++- src/entities/notification/model/setting.ts | 5 +++ .../get-business-notification-unread-count.ts | 12 +++++++ .../get-teacher-notification-unread-count.ts | 12 +++++++ .../gnb/ui/business-notification/button.tsx | 18 ++++++++-- .../gnb/ui/business-notification/list.tsx | 10 +++++- .../gnb/ui/teacher-notification/button.tsx | 17 ++++++++-- .../gnb/ui/teacher-notification/list.tsx | 10 +++++- 9 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 src/entities/notification/api/get-notification-unread-count.ts create mode 100644 src/widgets/gnb/api/get-business-notification-unread-count.ts create mode 100644 src/widgets/gnb/api/get-teacher-notification-unread-count.ts diff --git a/src/entities/notification/api/get-notification-unread-count.ts b/src/entities/notification/api/get-notification-unread-count.ts new file mode 100644 index 00000000..207f5162 --- /dev/null +++ b/src/entities/notification/api/get-notification-unread-count.ts @@ -0,0 +1,34 @@ +'use server' + +import { getTeacherSession, getBusinessSession } from 'entities/auth' +import { apiClient } from 'shared/api' + +import { NotificationUnreadCount } from '../model/setting' + +type GetNotificationUnreadCountResponse = NotificationUnreadCount + +export const getTeacherNotificationUnreadCount = async () => { + const { accessToken } = await getTeacherSession() + + const response = await apiClient.get({ + endpoint: `/notifications/unread-count`, + option: { + authorization: `Bearer ${accessToken}`, + }, + }) + + return response +} + +export const getBusinessNotificationUnreadCount = async () => { + const { accessToken } = await getBusinessSession() + + const response = await apiClient.get({ + endpoint: `/notifications/unread-count`, + option: { + authorization: `Bearer ${accessToken}`, + }, + }) + + return response +} diff --git a/src/entities/notification/api/query.ts b/src/entities/notification/api/query.ts index eb1efb59..182ccaec 100644 --- a/src/entities/notification/api/query.ts +++ b/src/entities/notification/api/query.ts @@ -1,5 +1,9 @@ -import { infiniteQueryOptions } from '@tanstack/react-query' +import { infiniteQueryOptions, queryOptions } from '@tanstack/react-query' +import { + getBusinessNotificationUnreadCount, + getTeacherNotificationUnreadCount, +} from './get-notification-unread-count' import { getBusinessNotifications, getTeacherNotifications, @@ -32,4 +36,14 @@ export const notificationQueries = { return lastPage.pageable.pageNumber + 1 }, }), + teacherUnreadCount: () => + queryOptions({ + queryKey: [...notificationQueries.lists(), 'teacher', 'unread-count'], + queryFn: getTeacherNotificationUnreadCount, + }), + businessUnreadCount: () => + queryOptions({ + queryKey: [...notificationQueries.lists(), 'business', 'unread-count'], + queryFn: getBusinessNotificationUnreadCount, + }), } diff --git a/src/entities/notification/model/setting.ts b/src/entities/notification/model/setting.ts index 4f9027a5..399ee02e 100644 --- a/src/entities/notification/model/setting.ts +++ b/src/entities/notification/model/setting.ts @@ -1,3 +1,8 @@ export type NotificationSetting = { allowEmail: boolean } + +export type NotificationUnreadCount = { + hasUnreadNotification: boolean + count: number +} diff --git a/src/widgets/gnb/api/get-business-notification-unread-count.ts b/src/widgets/gnb/api/get-business-notification-unread-count.ts new file mode 100644 index 00000000..8eb601da --- /dev/null +++ b/src/widgets/gnb/api/get-business-notification-unread-count.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query' + +import { notificationQueries } from 'entities/notification' + +export const useBusinessNotificationUnreadCount = () => { + const { data, refetch } = useQuery(notificationQueries.businessUnreadCount()) + + return { + hasUnreadNotification: data?.hasUnreadNotification, + refetch, + } +} diff --git a/src/widgets/gnb/api/get-teacher-notification-unread-count.ts b/src/widgets/gnb/api/get-teacher-notification-unread-count.ts new file mode 100644 index 00000000..8d7b82a0 --- /dev/null +++ b/src/widgets/gnb/api/get-teacher-notification-unread-count.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query' + +import { notificationQueries } from 'entities/notification' + +export const useTeacherNotificationUnreadCount = () => { + const { data, refetch } = useQuery(notificationQueries.teacherUnreadCount()) + + return { + hasUnreadNotification: data?.hasUnreadNotification, + refetch, + } +} diff --git a/src/widgets/gnb/ui/business-notification/button.tsx b/src/widgets/gnb/ui/business-notification/button.tsx index a304bcc6..74d0f3d8 100644 --- a/src/widgets/gnb/ui/business-notification/button.tsx +++ b/src/widgets/gnb/ui/business-notification/button.tsx @@ -1,4 +1,5 @@ import { useQueryClient } from '@tanstack/react-query' +import { usePathname } from 'next/navigation' import { useEffect } from 'react' import { notificationQueries } from 'entities/notification' @@ -8,12 +9,18 @@ import { useDropdown } from 'shared/lib' import { Dropdown, Icon } from 'shared/ui' import { BusinessNotificationList, NoNotification } from './list' +import { useBusinessNotificationUnreadCount } from '../../api/get-business-notification-unread-count' export const NotificationButton = () => { + const pathname = usePathname() + const queryClient = useQueryClient() const { isOpen, toggleIsOpen, close, dropdownRef } = useDropdown() + const { hasUnreadNotification, refetch } = + useBusinessNotificationUnreadCount() + const handleClick = () => { toggleIsOpen() } @@ -26,13 +33,20 @@ export const NotificationButton = () => { } }, [isOpen, queryClient]) + useEffect(() => { + void refetch() + }, [pathname, refetch]) + return (
- {isOpen && ( { displayLimit={15} > }> - + )} diff --git a/src/widgets/gnb/ui/business-notification/list.tsx b/src/widgets/gnb/ui/business-notification/list.tsx index e91d5079..fcc090b9 100644 --- a/src/widgets/gnb/ui/business-notification/list.tsx +++ b/src/widgets/gnb/ui/business-notification/list.tsx @@ -1,4 +1,5 @@ import { useRouter } from 'next/navigation' +import { useEffect } from 'react' import { cn } from 'shared/lib' import { Dropdown } from 'shared/ui' @@ -8,14 +9,21 @@ import { formatNotificationDate } from '../../lib/format-date' type Props = { close: () => void + updateCount: () => void } -export const BusinessNotificationList = ({ close }: Props) => { +export const BusinessNotificationList = ({ close, updateCount }: Props) => { const router = useRouter() const { data, isLoading, hasNextPage, fetchNextPage } = useBusinessNotifications() + useEffect(() => { + if (!isLoading) { + void updateCount() + } + }, [isLoading, updateCount]) + if (isLoading) { return } diff --git a/src/widgets/gnb/ui/teacher-notification/button.tsx b/src/widgets/gnb/ui/teacher-notification/button.tsx index 4f5b31f6..c028af86 100644 --- a/src/widgets/gnb/ui/teacher-notification/button.tsx +++ b/src/widgets/gnb/ui/teacher-notification/button.tsx @@ -1,4 +1,5 @@ import { useQueryClient } from '@tanstack/react-query' +import { usePathname } from 'next/navigation' import { useEffect } from 'react' import { notificationQueries } from 'entities/notification' @@ -8,12 +9,17 @@ import { useDropdown } from 'shared/lib' import { Dropdown, Icon } from 'shared/ui' import { TeacherNotificationList, NoNotification } from './list' +import { useTeacherNotificationUnreadCount } from '../../api/get-teacher-notification-unread-count' export const NotificationButton = () => { + const pathname = usePathname() + const queryClient = useQueryClient() const { isOpen, toggleIsOpen, close, dropdownRef } = useDropdown() + const { hasUnreadNotification, refetch } = useTeacherNotificationUnreadCount() + const handleClick = () => { toggleIsOpen() } @@ -26,13 +32,20 @@ export const NotificationButton = () => { } }, [isOpen, queryClient]) + useEffect(() => { + void refetch() + }, [pathname, refetch]) + return (
- {isOpen && ( { displayLimit={15} > }> - + )} diff --git a/src/widgets/gnb/ui/teacher-notification/list.tsx b/src/widgets/gnb/ui/teacher-notification/list.tsx index 9141f8a8..8b6b7109 100644 --- a/src/widgets/gnb/ui/teacher-notification/list.tsx +++ b/src/widgets/gnb/ui/teacher-notification/list.tsx @@ -1,4 +1,5 @@ import { useRouter } from 'next/navigation' +import { useEffect } from 'react' import { cn } from 'shared/lib' import { Dropdown } from 'shared/ui' @@ -8,14 +9,21 @@ import { formatNotificationDate } from '../../lib/format-date' type Props = { close: () => void + updateCount: () => void } -export const TeacherNotificationList = ({ close }: Props) => { +export const TeacherNotificationList = ({ close, updateCount }: Props) => { const router = useRouter() const { data, isLoading, hasNextPage, fetchNextPage } = useTeacherNotifications() + useEffect(() => { + if (!isLoading) { + void updateCount() + } + }, [isLoading, updateCount]) + if (isLoading) { return } From 1ac18b5a437fb7578ade7c5bd118f81896b3cd71 Mon Sep 17 00:00:00 2001 From: mina-gwak Date: Mon, 30 Jun 2025 00:38:02 +0900 Subject: [PATCH 4/4] =?UTF-8?q?Fix:=20PD-308=20GNB=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=9D=B4=EC=8A=88=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/layout/layout.tsx | 8 +++++--- src/shared/ui/layout/variants.ts | 2 +- src/shared/ui/modal/modal.tsx | 4 ++-- src/widgets/gnb/ui/variants.ts | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/shared/ui/layout/layout.tsx b/src/shared/ui/layout/layout.tsx index 3f0c560a..8d9873d7 100644 --- a/src/shared/ui/layout/layout.tsx +++ b/src/shared/ui/layout/layout.tsx @@ -15,9 +15,11 @@ export const Layout = ({ children, }: PropsWithChildren) => { return ( -
-
- {children} +
+
+
+ {children} +
) diff --git a/src/shared/ui/layout/variants.ts b/src/shared/ui/layout/variants.ts index a2e31045..c877c20e 100644 --- a/src/shared/ui/layout/variants.ts +++ b/src/shared/ui/layout/variants.ts @@ -1,6 +1,6 @@ import { cva, VariantProps } from 'class-variance-authority' -export const outerWrapper = cva('h-fit w-fit flex-grow', { +export const outerWrapper = cva('h-[calc(100dvh-65px)] w-fit flex-grow', { variants: { wide: { true: 'mx-auto my-0 flex', diff --git a/src/shared/ui/modal/modal.tsx b/src/shared/ui/modal/modal.tsx index a1cd23f3..714900fe 100644 --- a/src/shared/ui/modal/modal.tsx +++ b/src/shared/ui/modal/modal.tsx @@ -51,7 +51,7 @@ const ModalOverlay = forwardRef<