Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
52ae362
✨ 알림 기능 구현
solprime Dec 28, 2024
0d76847
Merge branch 'develop' of https://github.com/codeitFE11-part3-team7/w…
solprime Dec 28, 2024
d036e13
✨ 알림 클릭으로 이동 후 알림창 닫기
solprime Dec 28, 2024
5259d42
🔥 이전 코드 제거
solprime Dec 28, 2024
552cc20
🐛 이벤트 전파 방지
solprime Dec 28, 2024
39e8c43
Merge branch 'develop' into feature/#265_추가기능-알림-기능
solprime Dec 28, 2024
6c0dd1b
Merge branch 'develop' into feature/#265_추가기능-알림-기능
junghwaYang Dec 28, 2024
f25c5e2
⚡️ 메뉴에 내 위키 추가
solprime Dec 28, 2024
eab8783
💄 로그아웃 텍스트 색상 변경
solprime Dec 28, 2024
4acca52
💄 메뉴에서 border 삭제
solprime Dec 28, 2024
8eb0d5b
🐛 알림 아이콘 한 번 더 누르면 닫히도록 수정
solprime Dec 28, 2024
5561803
Merge branch 'develop' of https://github.com/codeitFE11-part3-team7/w…
solprime Dec 28, 2024
0e05930
Merge branch 'feature/#265_추가기능-알림-기능' of https://github.com/codeitFE…
solprime Dec 28, 2024
1e2e361
Merge branch 'develop' of https://github.com/codeitFE11-part3-team7/w…
solprime Dec 28, 2024
f1119f2
Merge branch 'feature/#265_추가기능-알림-기능' of https://github.com/codeitFE…
solprime Dec 28, 2024
d1c714e
💄 스타일 수정
solprime Dec 28, 2024
51ce027
🐛 버튼 안에 버튼 제거
solprime Dec 28, 2024
f08f912
💄 6시간 전 알림까지 파란불 띄우기
solprime Dec 28, 2024
f18101e
🐛 pc 드롭다운 오류 해결
solprime Dec 28, 2024
3812221
💄 헤더 다크모드 그림자
solprime Dec 28, 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
39 changes: 25 additions & 14 deletions components/Headers/Alarm.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
import Image from 'next/image';
import { useState } from 'react';
import NotificationWrapper from '@/components/Notification/NotificationWrapper';

interface AlarmProps {
isLoggedIn: boolean;
}

/**
* 알림 컴포넌트
* @param isLoggedIn 로그인 여부 판별
*/

export default function Alarm({ isLoggedIn }: AlarmProps) {
const [isOpen, setIsOpen] = useState(false);
const handleAlarmOpen = () => {
if (isOpen) {
setIsOpen(false);
return;
}
setIsOpen(true);
};
const handleAlarmClose = () => setIsOpen(false);

if (!isLoggedIn) {
return null;
}

return (
<button>
<Image
src="/icon/icon-alarm.svg"
className="mo:hidden"
alt="알림 아이콘"
width={32}
height={32}
/>
</button>
<div className="relative">
<button onClick={handleAlarmOpen}>
<Image
src="/icon/icon-alarm.svg"
className="mo:hidden"
alt="알림 아이콘"
width={32}
height={32}
/>
</button>

<NotificationWrapper isOpen={isOpen} onClose={handleAlarmClose} />
</div>
);
}
2 changes: 1 addition & 1 deletion components/Headers/Headers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default function Headers() {
<GNB />
</div>
{/* 로그인 여부에 따라 조건부로 노출 */}
<div className="flex items-center gap-5 mo:gap-2">
<div className="flex items-center gap-5 mo:gap-0">
<DarkModeToggle />
<Alarm isLoggedIn={isAuthenticated} />
<Login
Expand Down
142 changes: 80 additions & 62 deletions components/Headers/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Profile } from 'types/profile';

import Menu from '../Menu';
import { useSnackbar } from 'context/SnackBarContext';
import NotificationWrapper from '../Notification/NotificationWrapper';
import { ProfileContext } from 'context/ProfileContext';

interface LoginProps {
Expand All @@ -25,6 +26,7 @@ interface LoginProps {
export default function Login({ isMobile }: LoginProps) {
const [isOpen, setIsOpen] = useState(false);
const [profileMenu, setProfileMenu] = useState<string[]>([]);
const [showNotification, setShowNotification] = useState(false);
const { showSnackbar } = useSnackbar();

// ProfileContext에서 상태 가져오기
Expand Down Expand Up @@ -67,74 +69,90 @@ export default function Login({ isMobile }: LoginProps) {
localStorage.removeItem('refreshToken'); // refreshToken 제거
await router.push('/');
showSnackbar('로그아웃 되었습니다.', 'fail');
} else if (option === '알림') {
setShowNotification(true);
}
};

const closeNotification = () => {
setShowNotification(false);
};

useOutsideClick(loginMenuRef, () => setIsOpen(false));

return isAuthenticated ? (
<div ref={loginMenuRef} className="flex">
<div
role="button"
tabIndex={0}
className="relative cursor-pointer"
onClick={() => setIsOpen(!isOpen)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
setIsOpen(!isOpen);
}
}}
>
<div className="flex size-[32px] overflow-hidden rounded-full mo:hidden">
<Image
src={profileImage}
className="object-cover mo:hidden"
alt="프로필 아이콘"
width={32}
height={32}
/>
</div>
<Image
src="/icon/icon-menu.svg"
className="hidden mo:block"
alt="메뉴 아이콘"
width={32}
height={32}
/>
return (
<>
{isAuthenticated ? (
<div ref={loginMenuRef} className="flex">
<div
role="button"
tabIndex={0}
className="relative cursor-pointer"
onClick={() => setIsOpen(!isOpen)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
setIsOpen(!isOpen);
}
}}
>
<div className="flex size-[32px] overflow-hidden rounded-full mo:hidden">
<Image
src={profileImage}
className="object-cover mo:hidden"
alt="프로필 아이콘"
width={32}
height={32}
/>
</div>
<Image
src="/icon/icon-menu.svg"
className="hidden mo:block"
alt="메뉴 아이콘"
width={32}
height={32}
/>

{isOpen && (
<Menu
options={profileMenu}
onSelect={handleLoginMenu}
menuSize="w-28"
/>
)}
</div>
</div>
) : isMobile ? (
<div ref={loginMenuRef} className="flex">
<button className="relative" onClick={() => setIsOpen(!isOpen)}>
<Image
src="/icon/icon-menu.svg"
alt="메뉴 아이콘"
width={24}
height={24}
{isOpen && (
<Menu
options={profileMenu}
onSelect={handleLoginMenu}
menuSize="w-28"
/>
)}
</div>
</div>
) : isMobile ? (
<div ref={loginMenuRef} className="flex">
<button className="relative" onClick={() => setIsOpen(!isOpen)}>
<Image
src="/icon/icon-menu.svg"
alt="메뉴 아이콘"
width={24}
height={24}
/>
{isOpen && (
<Menu
options={profileMenu}
onSelect={handleLoginMenu}
menuSize="w-28"
/>
)}
</button>
</div>
) : (
<button
onClick={() => router.push('/login')}
className="text-14 text-gray-400"
>
로그인
</button>
)}
{showNotification && (
<NotificationWrapper
isOpen={showNotification}
onClose={closeNotification}
/>
{isOpen && (
<Menu
options={profileMenu}
onSelect={handleLoginMenu}
menuSize="w-28"
/>
)}
</button>
</div>
) : (
<button
onClick={() => router.push('/login')}
className="text-14 text-gray-400"
>
로그인
</button>
)}
</>
);
}
77 changes: 77 additions & 0 deletions components/Notification/NorificationBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import Image from 'next/image';
import NotificationContents from './NotificationContents';
import { NotificationProps } from '../Notification/NotificationWrapper';
import instance from '@/lib/axios-client';

interface NotificationBoxProps {
onClose: () => void;
content: NotificationProps | undefined;
setNotification: (notification: NotificationProps) => void;
}

export default function NotificationBox({
onClose,
content,
setNotification,
}: NotificationBoxProps) {
const handleNotiContent = async (id: number) => {
try {
// 삭제 요청 보내기 (id 기반)
await instance.delete(`/notifications/${id}`);

// content.list에서 삭제된 알림 항목을 제외한 새 배열로 업데이트
if (content?.list) {
const updatedList = content.list.filter(
(notification) => notification.id !== id
);

// 상태 업데이트 (list에서 해당 알림을 제외)
setNotification({
totalCount: updatedList.length,
list: updatedList,
});
}
} catch (error) {
console.error('알림 삭제 중 오류 발생', error);
}
};

return (
<div className="absolute right-0 z-20 flex max-h-[293px] w-[368px] flex-col gap-[16px] rounded-custom bg-gray-200 px-[24px] py-[20px] mo:top-[30px] mo:w-[250px]">
<div className="flex items-center justify-between">
<div className="text-20b">알림 {content?.totalCount}</div>
<button onClick={onClose}>
<Image
src="/icon/icon-close.svg"
alt="닫기 버튼"
width={24}
height={24}
className="brightness-0"
/>
</button>
</div>
<div
className={`flex flex-col gap-[8px] overflow-y-auto [&::-webkit-scrollbar]:hidden`}
style={{
scrollbarWidth: 'none',
msOverflowStyle: 'none',
}}
>
{content?.list && content.list.length > 0 ? (
content.list.map((notification) => (
<NotificationContents
key={notification.id}
createdAt={notification.createdAt}
handleNotiContent={() => handleNotiContent(notification.id)}
onClose={onClose}
/>
))
) : (
<div className="flex h-[100px] items-center justify-center text-14 text-gray-400">
최근 알림이 없습니다
</div>
)}
</div>
</div>
);
}
74 changes: 74 additions & 0 deletions components/Notification/NotificationContents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useProfileContext } from '@/hooks/useProfileContext';
import Image from 'next/image';
import { useRouter } from 'next/router';

interface NotificationContentsProps {
createdAt: Date;
handleNotiContent: () => void;
onClose: () => void;
}

export default function NotificationContents({
createdAt,
handleNotiContent,
onClose,
}: NotificationContentsProps) {
const { profile } = useProfileContext();
const router = useRouter();

const now = new Date();

const timeDifference = now.getTime() - new Date(createdAt).getTime();

const minutes = Math.floor(timeDifference / 1000 / 60);
const hours = Math.floor(timeDifference / 1000 / 60 / 60);
const days = Math.floor(timeDifference / 1000 / 60 / 60 / 24);

let timeAgo = '';
if (days > 0) {
timeAgo = `${days}일 전`;
} else if (hours > 0) {
timeAgo = `${hours}시간 전`;
} else if (minutes > 0) {
timeAgo = `${minutes}분 전`;
} else {
timeAgo = '방금';
}

const handleLinkClick = async () => {
const url = `/wiki/${profile?.code}`;
await router.replace(url);
handleNotiContent();
onClose();
};

const handleCloseClick = (e: React.MouseEvent) => {
e.stopPropagation();
handleNotiContent();
};

return (
<button
onClick={handleLinkClick}
className="flex w-full flex-col gap-[4px] rounded-custom bg-background px-[16px] py-[12px] dark:bg-gray-100"
>
<div>
<div className="flex w-full items-center justify-between">
<div className="size-[5px] rounded-full bg-red-100"></div>
<button onClick={handleCloseClick}>
<Image
src="/icon/icon-close.svg"
alt="닫기 버튼"
width={24}
height={24}
/>
</button>
</div>
<div className="w-[288px] text-left text-14 mo:w-[172px]">
내 위키가 수정되었습니다.
</div>
</div>
<div className="text-12 text-gray-400">{timeAgo}</div>
</button>
);
}
Loading
Loading