Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default tseslint.config(sheriff(sheriffOptions), {
patterns: ['.*'],
},
],
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-floating-promises': 'off',
'react-refresh/only-export-components': 'off',
'react/jsx-no-useless-fragment': 'off',
Expand All @@ -46,5 +47,10 @@ export default tseslint.config(sheriff(sheriffOptions), {
'react/no-multi-comp': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/require-await': 'off',
'tsdoc/syntax': 'off',
'fsecond/valid-event-listener': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'fsecond/prefer-destructured-optionals': 'off',
'@typescript-eslint/no-misused-spread': 'off',
},
});
4 changes: 2 additions & 2 deletions src/components/ui/chip/chip-profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const ChipVariants = cva(
},
}
);
interface ChipProps
export interface ChipProfileProps
extends HTMLAttributes<HTMLDivElement>,
VariantProps<typeof ChipVariants> {
children?: ReactNode;
Expand All @@ -47,7 +47,7 @@ export default function ChipProfile({
size = 'md',
color = 'green',
...props
}: ChipProps): ReactNode {
}: ChipProfileProps): ReactNode {
return (
<>
<div className={cn(ChipVariants({ color, size }))} {...props}>
Expand Down
64 changes: 64 additions & 0 deletions src/components/ui/dashboard-header/header-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useRouter } from 'next/router';
import { type ReactNode, useCallback, useEffect } from 'react';
import ChipProfile, {
type ChipProfileProps,
} from '@/components/ui/chip/chip-profile';
import Dropdown from '@/components/ui/dropdown';

interface HeaderDropdownProps {
nickname: string;
profileLabel: string;
profileColor: ChipProfileProps['color'];
}
export default function HeaderDropdown({
nickname,
profileLabel,
profileColor,
}: HeaderDropdownProps): ReactNode {
const router = useRouter();

const handleMyPageButton = useCallback(() => {
router.push('/mypage');
}, [router]);

const handleMyDashboardButton = useCallback(() => {
router.push('/mydashboard');
}, [router]);

const handleLogoutButton = useCallback(async () => {
try {
await fetch('/api/logout', { method: 'POST' });
} catch {
/**
* @todo
*/
}
router.push('/');
}, [router]);

useEffect(() => {
router.prefetch('/mypage');
router.prefetch('/mydashboard');
}, [router]);

return (
<Dropdown>
<Dropdown.Toggle>
<div className='border-l-gray-3 hover:bg-gray-4 active:bg-gray-3 mobile:pl-3 tablet:pr-8 mobile:pr-2 flex h-full cursor-pointer items-center gap-3 border-l-1 pr-20 pl-6'>
<ChipProfile label={profileLabel} size='lg' color={profileColor} />
<span className='mobile:hidden font-medium'>{nickname}</span>
</div>
</Dropdown.Toggle>
<Dropdown.List
positionClassName='top-3 right-3 w-fit'
ariaLabel='사용자 메뉴'
>
<Dropdown.Item onClick={handleLogoutButton}>로그아웃</Dropdown.Item>
<Dropdown.Item onClick={handleMyPageButton}>내 정보</Dropdown.Item>
<Dropdown.Item onClick={handleMyDashboardButton}>
내 대시보드
</Dropdown.Item>
</Dropdown.List>
</Dropdown>
);
}
78 changes: 10 additions & 68 deletions src/components/ui/dashboard-header/index.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import {
type ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import InviteMemberModal from '@/components/mydashboard/invite-member-modal';
import ChipProfile from '@/components/ui/chip/chip-profile';

import { type ReactNode, useState } from 'react';
import HeaderDropdown from '@/components/ui/dashboard-header/header-dropdown';
import InviteMemberModal from '@/components/ui/dashboard-header/invite-member-modal';
import ProfileList from '@/components/ui/dashboard-header/profile-list';

const buttonClass =
'flex-center border-gray-3 text-md mobile:px-3 mobile:py-1.5 h-9 cursor-pointer gap-2 rounded-lg border-1 px-4 py-2.5 hover:bg-gray-4 active:bg-gray-3';

export default function DashboardHeader(): ReactNode {
const [isModalOpen, setIsModalOpen] = useState(false);
const router = useRouter();
const [open, setOpen] = useState(false);
const menuRef = useRef<HTMLDivElement | null>(null);
const dashboardId = 1;

const handleOpenModal = () => {
setIsModalOpen(true);
};
Expand All @@ -33,53 +25,6 @@ export default function DashboardHeader(): ReactNode {
handleCloseModal();
};

const toggle = useCallback(() => {
setOpen((v) => !v);
}, []);
const close = useCallback(() => {
setOpen(false);
}, []);

useEffect(() => {
const handleDocClick = (e: MouseEvent) => {
if (!menuRef.current) {
return;
}
if (!menuRef.current.contains(e.target as Node)) {
setOpen(false);
}
};

document.addEventListener('mousedown', handleDocClick);

return () => {
document.removeEventListener('mousedown', handleDocClick);
};
}, []);

// 이재준 작성 - 내 정보 페이지로 이동하는 기능 추가
const goMyPage = useCallback(() => {
close();
router.push('/mypage');
}, [close, router]);

// 이재준 작성 - 내 대시보드 페이지로 이동하는 기능 추가
const goMyDashboard = useCallback(() => {
close();
router.push('/mydashboard');
}, [close, router]);

// 이재준 작성 - 로그아웃 기능 추가
const doLogout = useCallback(async () => {
try {
await fetch('/api/logout', { method: 'POST' });
} catch {
// 로그아웃 실패 시 무시
}
close();
router.push('/');
}, [close, router]);

return (
<header className='border-gray-3 fixed top-0 right-0 left-0 z-50 flex h-[4.375rem] w-full items-center justify-between border-b-1 bg-white pl-96'>
<div className='flex gap-2 text-xl font-bold text-black'>
Expand Down Expand Up @@ -113,14 +58,11 @@ export default function DashboardHeader(): ReactNode {
</div>
<div className='flex h-full gap-6'>
<ProfileList />
<Link
href={'/mypage'}
aria-label='마이 페이지로 이동'
className='border-l-gray-3 hover:bg-gray-4 active:bg-gray-3 flex cursor-pointer items-center gap-3 border-l-1 pr-20 pl-6'
>
<ChipProfile label={'K'} size='lg' color='green' />
<span className='font-medium'>권수형</span>
</Link>
<HeaderDropdown
nickname={'권수형'}
profileColor={'red'}
profileLabel={'K'}
/>
</div>
<InviteMemberModal
isOpen={isModalOpen}
Expand Down
36 changes: 36 additions & 0 deletions src/components/ui/dropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { createContext, type ReactNode, useState } from 'react';
import Item from '@/components/ui/dropdown/item';
import List from '@/components/ui/dropdown/list';
import Toggle from '@/components/ui/dropdown/toggle';
import useExitWhenClickOutSide from '@/hooks/useExitWhenClickOutSide';

interface ContextType {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
}

export const DropdownContext = createContext<ContextType | null>(null);

export default function Dropdown({
children,
}: {
children: ReactNode;
}): ReactNode {
const [isOpen, setIsOpen] = useState(false);
const handleClose = () => {
setIsOpen(false);
};
const dropdownRef = useExitWhenClickOutSide(handleClose);

return (
<DropdownContext.Provider value={{ isOpen, setIsOpen }}>
<div className='flex-center relative w-full' ref={dropdownRef}>
{children}
</div>
</DropdownContext.Provider>
);
}

Dropdown.Toggle = Toggle;
Dropdown.List = List;
Dropdown.Item = Item;
21 changes: 21 additions & 0 deletions src/components/ui/dropdown/item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { ReactNode } from 'react';

interface DropdownItemProps {
children: ReactNode;
onClick: () => void;
}
export default function Item({
children,
onClick,
}: DropdownItemProps): ReactNode {
return (
<li
role='menuitem'
className='hover:bg-violet-light hover:text-violet w-full rounded-md'
>
<button className='w-full px-4 py-1' onClick={onClick}>
{children}
</button>
</li>
);
}
36 changes: 36 additions & 0 deletions src/components/ui/dropdown/list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { type ReactNode, useContext } from 'react';
import { DropdownContext } from '@/components/ui/dropdown';
import { cn } from '@/utils/cn';

interface DropdownListProps {
children: ReactNode;
positionClassName?: string;
ariaLabel?: string;
}
export default function List({
children,
positionClassName,
ariaLabel = '메뉴',
}: DropdownListProps): ReactNode {
const context = useContext(DropdownContext);

if (!context) {
throw new Error('DropdownContext 내부에서 호출하세요.');
}
const { isOpen } = context;

return (
isOpen && (
<ul
role='menu'
aria-label={ariaLabel}
className={cn(
`text-md absolute top-0 right-0 mt-2 w-full translate-y-1/2 overflow-hidden rounded-md border border-gray-200 bg-white p-1.5 font-medium`,
positionClassName
)}
>
{children}
</ul>
)
);
}
32 changes: 32 additions & 0 deletions src/components/ui/dropdown/toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { type ReactNode, useContext } from 'react';
import { DropdownContext } from '@/components/ui/dropdown';

export default function Toggle({
children,
}: {
children: ReactNode;
}): ReactNode {
const context = useContext(DropdownContext);

if (!context) {
throw new Error('Dropdown Context 내부에서 호출해야 합니다');
}
const { isOpen, setIsOpen } = context;
const handleToggle = () => {
setIsOpen((prev) => !prev);
};

return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<div
role='button'
tabIndex={0}
aria-haspopup='menu'
aria-expanded={isOpen}
className='flex-center relative h-full w-full'
onClick={handleToggle}
>
{children}
</div>
);
}
26 changes: 26 additions & 0 deletions src/hooks/useExitWhenClickOutSide.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { type Ref, useEffect, useRef } from 'react';

export default function useExitWhenClickOutSide(
callback: () => void
): Ref<HTMLDivElement | null> {
const ref = useRef<HTMLDivElement | null>(null);

useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (!ref.current) {
return;
}
if (!ref.current.contains(e.target as Node)) {
callback();
}
};

document.addEventListener('click', handleClickOutside);

return () => {
document.removeEventListener('click', handleClickOutside);
};
}, [callback]);

return ref;
}
25 changes: 0 additions & 25 deletions src/pages/dashboard/[dashboardId].tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/pages/dashboard/[dashboardId]/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Link from 'next/link';
import { useRouter } from 'next/router';
import { type ReactNode, useState } from 'react';
import DashboardLayout from '@/components/layout/dashboard-layout';
import InviteMemberModal from '@/components/mydashboard/invite-member-modal';
import InviteMemberModal from '@/components/ui/dashboard-header/invite-member-modal';

export default function MydashboardEdit(): ReactNode {
const router = useRouter();
Expand Down
Loading