-
Notifications
You must be signed in to change notification settings - Fork 1
[Feature/#323] 헤더 알림 BellIcon, 알림 패널 UI #467
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
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
7403da1
feat: 알림 API에 맞게 type 정리중
jjjsun 5166ebe
Merge branch 'develop' into feature/#323
jjjsun 45bca45
feat: 알림 기록조회 API 타입정의 및 mock데이터 추가
jjjsun 0219009
feat: 알림 훅 추가 (API미제작상태라 mock데이터로)
jjjsun e6c5e71
feat: 알림 UI 컴포넌트및조립완료
jjjsun 678c180
style: 알림창 디자인 수정. 급감/급증 구분 추가
jjjsun 7ffafa4
style: 안읽음 알림 (리포트/클릭수증가/클릭수감소) 구분되도록 배경색 변경. 점 컬러 변경
jjjsun 3ef2eef
style: 알림창 제목에 BellIcon추가. 세부적인 컬러 디자인 수정
jjjsun 049cfa0
fix: 알림 없을떄 문구 수정
jjjsun 2c1d65d
fix: report 알림은 증감판단제외하도록
jjjsun 7e9dcec
fix: 오타수정
jjjsun ec28a6d
fix: import @별칭으로 수정
jjjsun 9dd0a17
chore: 워크스페이스 바꾸면 목록 달라지도록 수정(api연동시 추후 삭제예정이지만, mock데이터로 UI확인을 위해서 수…
jjjsun ab0970c
style: bell아이콘 크기 확대, 안읽음메세지수 위치변경
jjjsun 5dc1132
style: bell아이콘 크기 수정및 컬러 수정
jjjsun 1ba19d7
style: 안읽음 메세지수 너비 3단계로 나누기
jjjsun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { useState } from "react"; | ||
| import { twMerge } from "tailwind-merge"; | ||
|
|
||
| import { useNotificationHistory } from "@/hooks/notification/useNotificationHistory"; | ||
|
|
||
| import NotificationList from "@/components/notification/NotificationList"; | ||
| import NotificationPanel from "@/components/notification/NotificationPanel"; | ||
|
|
||
| import BellIcon from "@/assets/icon/sidebar/notification.svg?react"; | ||
|
|
||
| export default function NotificationBell() { | ||
| const [isOpen, setIsOpen] = useState(false); | ||
| const { orgId, notifications, unreadCount, isLoading } = | ||
| useNotificationHistory(); | ||
|
|
||
| // const openPanel = () => setIsOpen(true); | ||
| const closePanel = () => setIsOpen(false); | ||
| const togglePanel = () => setIsOpen((prev) => !prev); | ||
|
|
||
| const badgeLabel = unreadCount > 99 ? "99+" : String(unreadCount); | ||
| const badgeSizeClass = | ||
| unreadCount > 99 ? "h-4.5 w-6.5" : unreadCount < 10 ? "h-4 w-4" : "h-4 w-5"; | ||
|
|
||
| return ( | ||
| <> | ||
| <button | ||
| type="button" | ||
| aria-label="알림 열기" | ||
| aria-expanded={isOpen} | ||
| aria-haspopup="dialog" | ||
| onClick={togglePanel} | ||
| // onMouseEnter={openPanel} | ||
| className="relative flex h-10 w-10 cursor-pointer items-center justify-center rounded-xl text-text-title transition-colors hover:bg-surface-200 hover:text-text-body" | ||
| > | ||
| <BellIcon className="h-6.5 w-6.5" /> | ||
| {unreadCount > 0 ? ( | ||
| <span | ||
| className={twMerge( | ||
| "absolute top-0 right-0.5 flex items-center justify-center rounded-full bg-info-red font-caption text-surface-100", | ||
| badgeSizeClass, | ||
| )} | ||
| > | ||
| {badgeLabel} | ||
| </span> | ||
| ) : null} | ||
| </button> | ||
| <NotificationPanel isOpen={isOpen} onClose={closePanel}> | ||
| <NotificationList | ||
| orgId={orgId} | ||
| isLoading={isLoading} | ||
| notifications={notifications} | ||
| /> | ||
| </NotificationPanel> | ||
| </> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import { twMerge } from "tailwind-merge"; | ||
|
|
||
| import type { | ||
| INotificationHistoryItem, | ||
| TNotificationType, | ||
| } from "@/types/notification/notification"; | ||
|
|
||
| import TrendDownIcon from "@/assets/icon/chevron/trend-down.svg?react"; | ||
| import TrendUpIcon from "@/assets/icon/chevron/trend-up.svg?react"; | ||
|
|
||
| interface INotificationItemProps { | ||
| item: INotificationHistoryItem; | ||
| } | ||
|
|
||
| function formatNotificationTime(iso: string) { | ||
| return new Intl.DateTimeFormat("ko-KR", { | ||
| month: "numeric", | ||
| day: "numeric", | ||
| hour: "2-digit", | ||
| minute: "2-digit", | ||
| }).format(new Date(iso)); | ||
| } | ||
|
|
||
| function getClickTrend(item: INotificationHistoryItem): "up" | "down" | null { | ||
| const text = `${item.title} ${item.message}`; | ||
|
|
||
| if (text.includes("급증") || text.includes("증가")) return "up"; | ||
| if (text.includes("급감") || text.includes("감소")) return "down"; | ||
| if (item.type !== "CLICKS") return null; | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| function getNotificationRowClass( | ||
| type: TNotificationType, | ||
| trend: "up" | "down" | null, | ||
| ): string { | ||
| if (type === "REPORT") return "bg-surface-200"; | ||
| if (trend === "up") return "bg-info-red/[0.08]"; | ||
| if (trend === "down") return "bg-info-blue/[0.08]"; | ||
| return ""; | ||
| } | ||
|
|
||
| export default function NotificationItem({ item }: INotificationItemProps) { | ||
| const trend = getClickTrend(item); | ||
| const rowClass = getNotificationRowClass(item.type, trend); | ||
| return ( | ||
| <li | ||
| className={twMerge( | ||
| "flex gap-3 rounded-2xl px-4 py-3", | ||
| !item.isRead && rowClass, | ||
| )} | ||
| > | ||
| <span | ||
| className={twMerge( | ||
| "mt-2 h-2 w-2 shrink-0 rounded-full", | ||
| item.isRead ? "bg-transparent" : "bg-text-body", | ||
| )} | ||
| aria-hidden | ||
| /> | ||
| <div className="min-w-0 flex-1"> | ||
| <div className="flex items-center gap-1"> | ||
| <p className="min-w-0 truncate font-body1 text-text-title"> | ||
| {item.title} | ||
| </p> | ||
| {trend === "up" ? ( | ||
| <TrendUpIcon | ||
| className="h-4 w-4 shrink-0 text-info-red" | ||
| aria-label="증가" | ||
| /> | ||
| ) : null} | ||
| {trend === "down" ? ( | ||
| <TrendDownIcon | ||
| className="h-4 w-4 shrink-0 text-info-blue" | ||
| aria-label="감소" | ||
| /> | ||
| ) : null} | ||
| </div> | ||
|
|
||
| <p className="mt-1 font-body2 text-text-muted">{item.message}</p> | ||
| <p className="mt-2 font-caption text-text-placeholder"> | ||
| {formatNotificationTime(item.createdAt)} | ||
| </p> | ||
| </div> | ||
| </li> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import type { INotificationHistoryItem } from "@/types/notification/notification"; | ||
|
|
||
| import NotificationItem from "@/components/notification/NotificationItem"; | ||
| import NotificationListSkeleton from "@/components/notification/NotificationListSkeleton"; | ||
|
|
||
| interface INotificationListProps { | ||
| orgId: number | null; | ||
| isLoading: boolean; | ||
| notifications: INotificationHistoryItem[]; | ||
| } | ||
|
|
||
| export default function NotificationList({ | ||
| orgId, | ||
| isLoading, | ||
| notifications, | ||
| }: INotificationListProps) { | ||
| if (orgId === null) { | ||
| return ( | ||
| <div className="flex flex-col items-center justify-center gap-2 px-6 py-16 text-center"> | ||
| <p className="font-heading4 text-text-title"> | ||
| 워크스페이스를 선택해주세요 | ||
| </p> | ||
| <p className="font-body2 text-text-muted"> | ||
| 현재 워크스페이스 기준으로 알림을 보여줍니다 | ||
| </p> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (isLoading) { | ||
| return <NotificationListSkeleton />; | ||
| } | ||
|
|
||
| if (notifications.length === 0) { | ||
| return ( | ||
| <div className="flex flex-col items-center justify-center gap-2 px-6 py-16 text-center"> | ||
| <p className="font-heading4 text-text-title">아직 알림이 없어요</p> | ||
| <p className="font-body 2 text-text-muted"> | ||
| 클릭수 변화나 주간 리포트가 오면 | ||
| <br /> 여기에 표시됩니다 | ||
| </p> | ||
| </div> | ||
| ); | ||
| } | ||
| return ( | ||
| <ul className="flex flex-col gap-1 px-4 py-3"> | ||
| {notifications.map((item) => ( | ||
| <NotificationItem key={item.userNotificationId} item={item} /> | ||
| ))} | ||
| </ul> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { Skeleton } from "../common/skeleton/Skeleton"; | ||
|
|
||
| export default function NotificationListSkeleton() { | ||
| return ( | ||
| <ul className="flex flex-col gap-3 px-4 py-3" aria-hidden> | ||
| {Array.from({ length: 5 }, (_, index) => ( | ||
| <li key={index} className="flex flex-col gap-2 rounded-2xl px-3 py-3"> | ||
| <Skeleton className="h-4 w-32" /> | ||
| <Skeleton className="h-4 w-full" /> | ||
| <Skeleton className="h-3 w-20" /> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import type { ReactNode } from "react"; | ||
|
|
||
| import Drawer from "@/components/common/drawer/Drawer"; | ||
|
|
||
| import BellIcon from "@/assets/icon/sidebar/notification.svg?react"; | ||
|
|
||
| interface INotificationPanelProps { | ||
| isOpen: boolean; | ||
| onClose: () => void; | ||
| children: ReactNode; | ||
| } | ||
|
|
||
| export default function NotificationPanel({ | ||
| isOpen, | ||
| onClose, | ||
| children, | ||
| }: INotificationPanelProps) { | ||
| return ( | ||
| <Drawer | ||
| isOpen={isOpen} | ||
| onClose={onClose} | ||
| title={ | ||
| <h2 className="flex items-center gap-2 pl-2 pt-2 font-heading4 text-text-title"> | ||
| <BellIcon className="h-6 w-6 text-text-title" /> 알림 | ||
| </h2> | ||
| } | ||
| className="max-w-90 h-auto min-h-[min(72vh,560px)] my-4 rounded-l-3xl" | ||
| > | ||
| {children} | ||
| </Drawer> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import type { INotificationHistoryData } from "@/types/notification/notification"; | ||
| import { MOCK_NOTIFICATION_HISTORY } from "@/types/notification/notification.mock"; | ||
|
|
||
| import { useCoreQuery } from "@/hooks/customQuery"; | ||
|
|
||
| import useWorkspaceStore from "@/store/useWorkspaceStore"; | ||
|
|
||
| const MOCK_LOADING_MS = 400; | ||
|
|
||
| //API 연동전 mock데이터 활용을 위함. API함수추가시 삭제 예정 | ||
| async function getMockNotificationHistory(): Promise<INotificationHistoryData> { | ||
| await new Promise((resolve) => setTimeout(resolve, MOCK_LOADING_MS)); | ||
| return MOCK_NOTIFICATION_HISTORY; | ||
| } | ||
|
|
||
| export function useNotificationHistory() { | ||
| const orgId = useWorkspaceStore((s) => s.selectedOrgId); | ||
|
|
||
| const query = useCoreQuery( | ||
| ["notification-history", orgId], | ||
| () => getMockNotificationHistory(), | ||
| { enabled: orgId != null }, | ||
| ); | ||
|
|
||
| const notifications = query.data?.notifications ?? []; | ||
| const unreadCount = notifications.filter((item) => !item.isRead).length; | ||
|
|
||
| return { | ||
| orgId, | ||
| notifications, | ||
| unreadCount, | ||
| isLoading: query.isLoading, | ||
| isError: query.isError, | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import type { INotificationHistoryData } from "@/types/notification/notification"; | ||
|
|
||
| export const MOCK_NOTIFICATION_HISTORY: INotificationHistoryData = { | ||
| hasNext: false, | ||
| nextCursor: null, | ||
| notifications: [ | ||
| { | ||
| userNotificationId: 4, | ||
| title: "주간 리포트", | ||
| message: "이번 주 성과 리포트가 이메일로 발송되었습니다.", | ||
| createdAt: "2026-08-13T23:00:00.000Z", | ||
| type: "REPORT", | ||
| isRead: false, | ||
| }, | ||
| { | ||
| userNotificationId: 3, | ||
| title: "클릭수 급감 알림", | ||
| message: "오늘 클릭수가 전일 대비 13% 감소했습니다", | ||
| createdAt: "2026-08-14T04:52:25.364Z", | ||
| type: "CLICKS", | ||
| isRead: false, | ||
| }, | ||
|
jjjsun marked this conversation as resolved.
|
||
| { | ||
| userNotificationId: 2, | ||
| title: "클릭수 급증 알림", | ||
| message: "오늘 클릭수가 전일 대비 49% 증가했습니다", | ||
| createdAt: "2026-08-13T11:20:00.000Z", | ||
| type: "CLICKS", | ||
| isRead: false, | ||
| }, | ||
| { | ||
| userNotificationId: 1, | ||
| title: "클릭수 급감 알림", | ||
| message: "오늘 클릭수가 전일 대비 68% 감소했습니다", | ||
| createdAt: "2026-08-12T08:10:00.000Z", | ||
| type: "CLICKS", | ||
| isRead: true, | ||
| }, | ||
| ], | ||
| }; | ||
|
|
||
| export const MOCK_NOTIFICATION_HISTORY_EMPTY: INotificationHistoryData = { | ||
| hasNext: false, | ||
| nextCursor: null, | ||
| notifications: [], | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| export interface IMyNotificationSettings { | ||
| isMasterEnabled: boolean; | ||
| isBrowserPushEnabled: boolean; | ||
| isEmailEnabled: boolean; | ||
| isSlackEnabled: boolean; | ||
| isSlackConnted: boolean; | ||
| isDiscordEnabled: boolean; | ||
| isDiscordConnected: boolean; | ||
| alertClicks: boolean; | ||
| alertReport: boolean; | ||
| orgAlertclicks: boolean; | ||
| orgAlertReport: boolean; | ||
| } | ||
|
|
||
| export interface INotificationMemberSetting { | ||
| membershipId: number; | ||
| name: string; | ||
| email: string; | ||
| role: string; | ||
| isReceive: boolean; | ||
| } | ||
|
|
||
| export type TNotificationType = "CLICKS" | "REPORT"; | ||
|
|
||
| export interface INotificationHistoryItem { | ||
| userNotificationId: number; | ||
| title: string; | ||
| message: string; | ||
| createdAt: string; | ||
| type: TNotificationType; | ||
| isRead: boolean; | ||
| } | ||
|
|
||
| export interface INotificationHistoryData { | ||
| hasNext: boolean; | ||
| nextCursor: string | null; | ||
| notifications: INotificationHistoryItem[]; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.