Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 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
46 changes: 32 additions & 14 deletions components/Headers/Alarm.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,46 @@
import Image from 'next/image';
import { useRef, useState } from 'react';
import NotificationWrapper from '@/components/Notification/NotificationWrapper';
import useOutsideClick from '@/hooks/useOutsideClick';

interface AlarmProps {
isLoggedIn: boolean;
}

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

export default function Alarm({ isLoggedIn }: AlarmProps) {
const [isOpen, setIsOpen] = useState(false);
const handleAlarmOpen = () => setIsOpen(!isOpen);
const handleAlarmClose = () => setIsOpen(false);
const alarmRef = useRef<HTMLDivElement>(null);

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

if (!isLoggedIn) {
return null;
}

return (
<button>
<Image
src="/icon/icon-alarm.svg"
className="mo:hidden"
alt="알림 아이콘"
width={32}
height={32}
/>
</button>
<div ref={alarmRef}>
<div
role="button"
tabIndex={0}
onClick={handleAlarmOpen}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') handleAlarmOpen();
}}
>
<Image
src="/icon/icon-alarm.svg"
className="mo:hidden"
alt="알림 아이콘"
width={32}
height={32}
/>
</div>

{isOpen && (
<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
157 changes: 93 additions & 64 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 All @@ -39,9 +41,16 @@ export default function Login({ isMobile }: LoginProps) {
if (!isAuthenticated) {
if (isMobile) return ['로그인', '위키목록', '자유게시판'];
} else if (isMobile) {
return ['위키목록', '자유게시판', '알림', '마이페이지', '로그아웃'];
return [
'위키목록',
'자유게시판',
'내 위키',
'알림',
'마이페이지',
'로그아웃',
];
} else {
return ['마이페이지', '로그아웃'];
return ['마이페이지', '내 위키', '로그아웃'];
}
return [];
}, [isAuthenticated, isMobile]); // 의존성 배열에 필요한 값만 포함
Expand All @@ -67,74 +76,94 @@ export default function Login({ isMobile }: LoginProps) {
localStorage.removeItem('refreshToken'); // refreshToken 제거
await router.push('/');
showSnackbar('로그아웃 되었습니다.', 'fail');
} else if (option === '알림') {
setShowNotification(true);
} else if (option === '내 위키') {
await router.push(`/wiki/${profile?.code}`);
}
};

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"
isBorder={false}
/>
)}
</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"
isBorder={false}
/>
)}
</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>
)}
</>
);
}
40 changes: 24 additions & 16 deletions components/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ interface MenuProps {
options: string[];
menuSize?: string;
onSelect: (option: string) => void;
isBorder?: boolean;
}

/**
Expand All @@ -11,25 +12,32 @@ interface MenuProps {
* @param onSelect 선택한 옵션을 반환
*/

export default function Menu({ options, onSelect, menuSize }: MenuProps) {
const fadeIn = 'pc:animate-pcFadeIn tamo:animate-tamoFadeIn';
export default function Menu({
options,
onSelect,
menuSize,
isBorder = true,
}: MenuProps) {
return (
<ul
className={`${menuSize} ${fadeIn} absolute z-10 mt-2 rounded-xl border border-gray-300 bg-background p-[4px] text-14 shadow-custom pc:right-1/2 pc:translate-x-1/2 tamo:right-0`}
className={`${menuSize} downFadein absolute z-10 mt-2 rounded-xl ${isBorder ? 'border border-gray-300' : ''} bg-background p-[4px] text-14 shadow-custom dark:shadow-custom-dark pc:right-1/2 pc:translate-x-1/2 tamo:right-0`}
>
{options.map((option, index) => (
<button
key={index}
tabIndex={0}
onClick={() => onSelect(option)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') onSelect(option);
}}
className={`flex h-[35px] w-full cursor-pointer flex-col rounded-md px-[16px] py-[5px] hover:bg-green-100`}
>
{option}
</button>
))}
{options.map((option, index) => {
const isLogout = option === '로그아웃';
return (
<button
key={index}
tabIndex={0}
onClick={() => onSelect(option)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') onSelect(option);
}}
className={`flex h-[35px] w-full cursor-pointer flex-col rounded-md px-[16px] py-[5px] hover:bg-green-100 ${isLogout ? 'text-gray-400' : ''}`}
>
{option}
</button>
);
})}
</ul>
);
}
76 changes: 76 additions & 0 deletions components/Notification/NorificationBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Image from 'next/image';
import NotificationContents from './NotificationContents';
import { NotificationProps } from '../Notification/NotificationWrapper';
import instance from '@/lib/axios-client';
import { useSnackbar } from 'context/SnackBarContext';

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

export default function NotificationBox({
onClose,
content,
setNotification,
}: NotificationBoxProps) {
const { showSnackbar } = useSnackbar();
const handleNotiContent = async (id: number) => {
try {
await instance.delete(`/notifications/${id}`);

if (content?.list) {
const updatedList = content.list.filter(
(notification) => notification.id !== id
);

setNotification({
totalCount: updatedList.length,
list: updatedList,
});
}
} catch (error) {
showSnackbar('오류가 발생했습니다', 'fail');
}
};

return (
<div className="alarmDownFadein absolute right-0 z-20 mt-[8px] flex max-h-[293px] w-[368px] flex-col gap-[16px] rounded-custom bg-gray-200 px-[20px] py-[24px] shadow-custom dark:shadow-custom-dark 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>
);
}
Loading
Loading