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

feat: 알림 기능 구현 #85

Merged
merged 10 commits into from
Sep 11, 2024
Merged
77 changes: 77 additions & 0 deletions src/components/Notification/NotificationList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { If } from '@/system/utils/If';
import { useGetNotificationList } from './apis/useGetNotificationList';
import { Icon } from '@/system/components';
import { Spacing } from '@/system/utils/Spacing';
import { dday, formatToYYMMDD } from '@/utils/date';
import { cn } from '@/utils/tailwind-util';
import { useRouter } from 'next/navigation';
import { NotificationType } from '@/types/notification';
import { useNotificationContext } from './context';

export function NotificationList() {
const router = useRouter();
const { close } = useNotificationContext();

const { data: notificationList } = useGetNotificationList();

const getDateText = (date: string) => {
if (dday(date) >= 0) {
return '오늘';
}

return formatToYYMMDD(date, { separator: '.' });
};

const handleNotificationClick = (notification: NotificationType) => {
router.push(`/my-recruit/${notification.referenceId}`);
close();
};

return (
<>
<If condition={notificationList?.length === 0}>
<div className="flex h-full items-center justify-center">
<div className="flex flex-col gap-24">
<Icon name="IllustAlarm" />
<div className="flex flex-col items-center gap-4">
<h2 className="text-neutral-10 text-body1 font-normal">지금은 알림이 없어요</h2>
<p className="text-caption2 font-regular text-neutral-35">알림은 30일 뒤에 자동으로 사라져요</p>
</div>
<Spacing size={100} />
</div>
</div>
</If>
<If condition={notificationList && notificationList.length > 0}>
<Spacing size={40} />
{notificationList?.map((notification) => (
<div key={notification.id}>
<div className={cn('flex flex-col gap-8 py-8', notification.isRead && 'opacity-30')}>
<div className="flex gap-8">
<If condition={!notification.isRead}>
<div className="flex-shrink-0 w-6 h-6 mt-6 rounded-full bg-red-40" />
</If>
<div className="text-white text-label1">
<span
className="font-semibold underline cursor-pointer"
onClick={() => handleNotificationClick(notification)}>
{notification.title}
<span className="relative">
<span className="absolute top-1 left-2">
<Icon name="pageOpen" />
</span>
</span>
</span>
<span className="ml-20">{notification.message}</span>
</div>
</div>
<div className="flex justify-end text-caption2 font-regular text-neutral-35">
{getDateText(notification.createdAt)}
</div>
</div>
<div className="w-full h-[1px] my-16 bg-neutral-75" />
</div>
))}
</If>
</>
);
}
16 changes: 6 additions & 10 deletions src/components/Notification/NotificationWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { TouchButton } from '../TouchButton';
import { color } from '@/system/token/color';
import { useNotificationContext } from './context';
import { AnimatePresence, motion } from 'framer-motion';
import { Spacing } from '@/system/utils/Spacing';
import { NotificationList } from './NotificationList';
import { AsyncBoundaryWithQuery } from '@/lib';

export function NotificationWindow() {
const { isOpen, close } = useNotificationContext();
Expand All @@ -24,16 +25,11 @@ export function NotificationWindow() {
<Icon name="x" color={color.neutral40} />
</TouchButton>
</div>
<div className="flex-1 flex items-center justify-center">
<div className="flex flex-col gap-24">
<Icon name="IllustAlarm" />
<div className="flex flex-col items-center gap-4">
<h2 className="text-neutral-10 text-body1 font-normal">지금은 알림이 없어요</h2>
<p className="text-caption2 font-regular text-neutral-35">알림은 30일 뒤에 자동으로 사라져요</p>
</div>
</div>
<div className="flex-1 overflow-auto">
<AsyncBoundaryWithQuery>
<NotificationList />
</AsyncBoundaryWithQuery>
</div>
<Spacing size={100} />
</motion.div>
)}
</AnimatePresence>
Expand Down
23 changes: 23 additions & 0 deletions src/components/Notification/apis/useGetNotificationCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useQuery } from '@tanstack/react-query';
import { http } from '@/apis/http';

export const GET_NOTIFICATION_COUNT = 'notification-count';

interface GetNotificationCountResponse {
number: number;
}

const getNotificationCount = () => {
return http.get<GetNotificationCountResponse>({ url: `/notifications/num` });
};

export const useGetNotificationCount = () => {
return useQuery({
queryKey: [GET_NOTIFICATION_COUNT],
queryFn: async () => {
const res = await getNotificationCount();

return res.data;
},
});
};
22 changes: 22 additions & 0 deletions src/components/Notification/apis/useGetNotificationList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { NotificationType } from '@/types/notification';
import { http } from '@/apis/http';

export const GET_NOTIFICATION_LIST = 'notification-list';

type GetNotificationListResponse = NotificationType[];

const getNotificationList = () => {
return http.get<GetNotificationListResponse>({ url: `/notifications` });
};

export const useGetNotificationList = () => {
return useSuspenseQuery({
queryKey: [GET_NOTIFICATION_LIST],
queryFn: async () => {
const res = await getNotificationList();

return res.data;
},
});
};
20 changes: 20 additions & 0 deletions src/components/Notification/apis/usePutNotificationRead.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { http } from '@/apis/http';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { GET_NOTIFICATION_COUNT } from './useGetNotificationCount';
import { GET_NOTIFICATION_LIST } from './useGetNotificationList';

const putNotificationRead = () => {
return http.put({ url: `/notifications` });
};

export const usePutNotificationRead = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: putNotificationRead,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [GET_NOTIFICATION_COUNT] });
queryClient.invalidateQueries({ queryKey: [GET_NOTIFICATION_LIST] });
},
});
};
10 changes: 8 additions & 2 deletions src/components/Notification/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { generateContext } from '@/lib';
import { useState } from 'react';
import { usePutNotificationRead } from './apis/usePutNotificationRead';

interface NotificationContext {
isOpen: boolean;
Expand All @@ -17,11 +18,16 @@ const [NotificationWrapper, useNotificationContext] = generateContext<Notificati
function NotificatinProvider({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);

const { mutate: readNotification } = usePutNotificationRead();

const open = () => setIsOpen(true);

const close = () => setIsOpen(false);
const close = () => {
setIsOpen(false);
readNotification();
};

const toggle = () => setIsOpen((prev) => !prev);
const toggle = () => (isOpen ? close() : open());

return (
<NotificationWrapper isOpen={isOpen} open={open} close={close} toggle={toggle}>
Expand Down
9 changes: 8 additions & 1 deletion src/container/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { PropsWithChildren, useState } from 'react';
import { Collapsible } from './Collapsible/Collapsible';
import { useNotificationContext } from '@/components/Notification/context';
import { LogoOnlyLeaf } from '@/components/LogoOnlyLeaf';
import { useGetNotificationCount } from '@/components/Notification/apis/useGetNotificationCount';

export function Sidebar() {
const router = useRouter();
Expand All @@ -28,6 +29,7 @@ export function Sidebar() {

const { isOpen: isNotificationOpen, toggle: toggleNotification } = useNotificationContext();

const { data: notificationCount } = useGetNotificationCount();
const { data: typeCounts } = useGetCardTypeCount();
const { data: recruiteTitles } = useGetRecruitTitles();

Expand Down Expand Up @@ -86,10 +88,15 @@ export function Sidebar() {
</Dialog.Content>
</Dialog>
<SidebarButton
iconName="bell"
iconName={!!notificationCount?.number ? 'bellWithRedDot' : 'bell'}
selected={isNotificationOpen}
expanded={expanded}
expandedText="알림"
right={
<div className="px-4 rounded-3 bg-neutral-80 text-neutral-35 text-caption1 font-medium">
{notificationCount?.number || '0'}
</div>
}
onClick={toggleNotification}
/>
{/* <SidebarButton iconName="memo" selected={false} expanded={expanded} expandedText="메모 모아보기" /> */}
Expand Down
4 changes: 4 additions & 0 deletions src/system/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import Warning from './SVG/Warning';
import { WorkFill } from './SVG/WorkFill';
import { X } from './SVG/X';
import { IllustAlarm } from './SVG/IllustAlarm';
import { PageOpen } from './SVG/PageOpen';
import { BellWithRedDot } from './SVG/BellWithRedDot';
import { Backspace } from './SVG/Backspace';
import { SavingSuccess } from './SVG/SavingSuccess';

Expand Down Expand Up @@ -105,6 +107,8 @@ const iconMap = {
announcementFolder: AnnouncementFolder,
IllustAlarm: IllustAlarm,
warning: Warning,
pageOpen: PageOpen,
bellWithRedDot: BellWithRedDot,
backspace: Backspace,
savingSuccess: SavingSuccess,
} as const;
Expand Down
20 changes: 20 additions & 0 deletions src/system/components/Icon/SVG/BellWithRedDot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { IconBaseType } from './type';

export function BellWithRedDot({ size, color }: IconBaseType) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox={`0 0 ${size} ${size}`} fill="none">
<path
d="M5.81186 8.93778C5.81186 5.52036 8.58222 2.75 11.9996 2.75C15.417 2.75 18.1874 5.52036 18.1874 8.93777V13.5184L19.8 17.8572H4.19922L5.81186 13.5184V8.93778Z"
stroke={color}
strokeWidth="1.5"
/>
<path
d="M15.1673 17.8574V18.0834C15.1673 19.8323 13.7496 21.2501 12.0007 21.2501C10.2517 21.2501 8.83398 19.8323 8.83398 18.0834V17.8574"
stroke={color}
strokeWidth="1.5"
/>
<circle cx="22" cy="2" r="2" fill="#FF6C62" />
<circle cx="22" cy="2" r="2" fill="#FF6C62" />
</svg>
);
}
8 changes: 8 additions & 0 deletions src/system/components/Icon/SVG/PageOpen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function PageOpen() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.6654 9.66675V4.33341H6.33203" stroke="white" strokeWidth="1.25" strokeLinecap="square" />
<path d="M11 5L3 13" stroke="white" strokeWidth="1.25" strokeLinecap="square" />
</svg>
);
}
9 changes: 9 additions & 0 deletions src/types/notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface NotificationType {
id: number;
title: string;
message: string;
isRead: boolean;
type: string;
referenceId: number;
createdAt: string;
}
Loading