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: 3 additions & 3 deletions src/apis/members/quries.ts → src/apis/members/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { DeleteMemberRequest, GetMembersRequest } from './types';

export const useMembersQuery = (params: GetMembersRequest) => {
return useQuery({
queryKey: ['members', params.dashboardId],
queryKey: ['members', params],
queryFn: () => getMembers(params),
});
};
Expand All @@ -16,8 +16,8 @@ export const useRemoveMember = () => {
mutationFn: (params: DeleteMemberRequest) => {
return deleteMember(params);
},
onSuccess: (_, { dashboardId }) => {
queryClient.invalidateQueries({ queryKey: ['members', dashboardId] });
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['members'] });
},
});
};
9 changes: 9 additions & 0 deletions src/apis/users/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useQuery } from '@tanstack/react-query';
import { getUser } from '.';

export const useGetUser = () => {
return useQuery({
queryKey: ['user'],
queryFn: () => getUser(),
});
};
2 changes: 1 addition & 1 deletion src/app/(after-login)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function layout({ children }: PropsWithChildren) {
<main className='flex-1 overflow-y-auto bg-gray-10 pl-16 md:pl-0'>
{/* header */}
<div className='sticky left-16 top-0 z-30 bg-white md:left-0'>
<Header showCrown showSetting showMembers showProfile />
<Header />
</div>

{/* content page */}
Expand Down
58 changes: 58 additions & 0 deletions src/components/dashboard-header/DashboardInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use client';

import { useRef } from 'react';
import { useParams } from 'next/navigation';
import Image from 'next/image';
import useMediaQuery from '@/hooks/useMediaQuery';
import { useDashboardQuery } from '@/apis/dashboards/queries';
import { useMembersQuery } from '@/apis/members/queries';
import StackAvatars from '@/components/ui/Avatar/StackAvatars';
import { ModalHandle } from '@/components/ui/Modal/Modal';
import InviteDashboard from '@/components/dashboard/InviteDashboard';
import HeaderButton from '@/components/dashboard-header/HeaderButton';
import Setting from '@/assets/icons/setting.svg';
import AddBox from '@/assets/icons/add_box.svg';
import crown from '@/assets/icons/crown.svg';

export default function DashboardInfo() {
const { id } = useParams();
const { data: dashboardDetail } = useDashboardQuery(Number(id));
const { data: membersData } = useMembersQuery({ dashboardId: Number(id), page: 1, size: 4 });
const inviteModalRef = useRef<ModalHandle | null>(null);

const isDesktop = useMediaQuery('(min-width: 1024px)');
const isTablet = useMediaQuery('(min-width: 768px)');
const visibleCount = isDesktop ? 4 : isTablet ? 3 : 2;

return (
<div className='mr-4 flex items-center gap-4 border-r border-gray-30 pr-4 md:mr-6 md:pr-6 lg:mr-9 lg:pr-9'>
{/* 대시보드 방 제목 */}
<h2 className='hidden items-center gap-2 text-xl font-bold leading-none xl:flex'>
<span className='overflow-hidden text-ellipsis whitespace-nowrap'>{dashboardDetail?.title}</span>
{dashboardDetail?.createdByMe && <Image src={crown} alt='crown' />}
</h2>

<div className='ml-auto flex items-center gap-4 leading-none md:gap-8 lg:gap-10'>
{/* 대시보드 관리자만 보임 : 대시보드 관리 버튼) */}
{dashboardDetail?.createdByMe && (
<div className='flex gap-[6px] md:gap-3 lg:gap-4'>
<HeaderButton href={`/dashboard/${id}/edit`}>
<Image src={Setting} alt='관리' className='hidden sm:block' />
관리
</HeaderButton>
<HeaderButton onClick={() => inviteModalRef.current?.open()}>
<Image src={AddBox} alt='초대하기' className='hidden sm:block' />
초대하기
</HeaderButton>

{/* 초대모달 */}
<InviteDashboard ref={inviteModalRef} />
</div>
)}

{/* 모든 맴버가 보임 : 대시보드 맴버목록 */}
<StackAvatars members={membersData?.members || []} visibleCount={visibleCount} totalCount={membersData?.totalCount || 0} />
</div>
</div>
);
}
76 changes: 15 additions & 61 deletions src/components/dashboard-header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,25 @@
'use client';

import React from 'react';
import { usePathname } from 'next/navigation';
import Image from 'next/image';
import Crown from '@/assets/icons/crown.svg';
import Title from './Title';
import SettingBtn from './SettingBtn';
import Members from './Members';
import Profile from './Profile';
import { useParams, useSelectedLayoutSegment } from 'next/navigation';
import Profile from '@/components/dashboard-header/Profile';
import DashboardInfo from '@/components/dashboard-header/DashboardInfo';
import { PAGE_TITLE } from '@/constants/dashboard';

interface HeaderProps {
title?: string;
showCrown?: boolean;
showSetting?: boolean;
showMembers?: boolean;
showProfile?: boolean;
}

const titleMap: { [key: string]: string } = {
'/mydashboard': '내 대시보드',
'/mypage': '계정관리',
};

// TODO : API 연동 이후 추가 작업 예정, 우선 mock 데이터로 집어넣어놨습니다.
interface UserType {
nickname: string;
profileImageUrl: string;
}

const mockUsers: UserType[] = [
{ nickname: '배유철', profileImageUrl: 'B' },
{ nickname: '김철수', profileImageUrl: 'K' },
{ nickname: '이영희', profileImageUrl: 'L' },
];

export default function Header({ showCrown = false, showSetting = false, showMembers = false, showProfile = false }: HeaderProps) {
const pathname = usePathname();
const title = titleMap[pathname] || '내 대시보드';
const handleSettingClick = () => {
console.log('관리 버튼 클릭');
// TODO : 디버깅 용도 console.log API 기능 구현 후 수정 예정
};

const handleInviteClick = () => {
console.log('초대 버튼 클릭');
// TODO : 디버깅 용도 console.log API 기능 구현 후 수정 예정
};
export default function Header() {
const { id } = useParams();
const isDashboardPage = !!id;

const handleProfileClick = () => {
console.log('프로필 버튼 클릭');
// TODO : 디버깅 용도 console.log API 기능 구현 후 수정 예정
};
const segment = useSelectedLayoutSegment();
const currentPageTitle = PAGE_TITLE[segment as keyof typeof PAGE_TITLE] || '';

return (
<header className='flex h-[60px] w-full items-center justify-between border-b-2 border-gray-20 py-[15.5px] sm:h-[70px] sm:px-[40px]'>
{/* 왼쪽 제목 그룹 */}
<div className='flex items-center gap-[8px]'>
<Title title={title} />
{showCrown && <Image src={Crown} alt='방장 표시' width={20} height={16} className='hidden lg:block' />}
<header className='flex h-[60px] w-full items-center justify-between border-b-2 border-gray-20 px-3 py-[15px] sm:h-[70px] md:px-8 lg:px-10'>
<div className='flex-1'>
{/* 페이지 제목 or 대시보드 정보 */}
{!isDashboardPage ? <h2 className='text-xl font-bold'>{currentPageTitle}</h2> : <DashboardInfo />}
</div>
{/* 오른쪽 버튼 그룹 */}
<div className='flex items-center'>
<div className='flex items-center gap-[16px] border-r border-gray-30 pr-[12px] sm:gap-[32px] sm:pr-[24px] md:gap-[40px] md:pr-[32px]'>
{showSetting && <SettingBtn onSettingClick={handleSettingClick} onInviteClick={handleInviteClick} />}
{showMembers && <Members users={mockUsers} />}
</div>
{showProfile && <Profile nickname={mockUsers[0].nickname} profileImageUrl={mockUsers[0].profileImageUrl} onClick={handleProfileClick} />}
<div className='ml-auto'>
<Profile />
</div>
</header>
);
Expand Down
32 changes: 32 additions & 0 deletions src/components/dashboard-header/HeaderButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ButtonHTMLAttributes, PropsWithChildren } from 'react';
import Link, { LinkProps } from 'next/link';
import { cn } from '@/utils/helper';

interface HeaderButtonProps extends PropsWithChildren<ButtonHTMLAttributes<HTMLButtonElement>> {
href?: string;
linkProps?: Omit<LinkProps, 'href'>; // href를 제외한 Link의 props 추가로 전달
}

export default function HeaderButton({ children, className, href, linkProps, ...props }: HeaderButtonProps) {
const button = (
<button
{...props}
className={cn(
'flex h-10 items-center justify-center gap-2 whitespace-nowrap rounded-lg border border-gray-30 bg-white px-4 py-[6px] font-medium leading-none text-gray-50 transition-all hover:text-gray-70 hover:shadow',
className,
)}
>
{children}
</button>
);

if (href) {
return (
<Link href={href} {...linkProps}>
{button}
</Link>
);
}

return button;
}
17 changes: 0 additions & 17 deletions src/components/dashboard-header/Members.tsx

This file was deleted.

87 changes: 77 additions & 10 deletions src/components/dashboard-header/Profile.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,83 @@
import React from 'react';
'use client';

interface ProfileProps {
nickname: string;
profileImageUrl: string;
onClick: () => void;
import { useEffect, useRef, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useQueryClient } from '@tanstack/react-query';
import { motion } from 'motion/react';
import { useGetUser } from '@/apis/users/queries';
import Avatar from '@/components/ui/Avatar/Avatar';
import axiosClientHelper from '@/utils/network/axiosClientHelper';
import useAlert from '@/hooks/useAlert';

export default function Profile() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { data, isFetching } = useGetUser();
const alert = useAlert();
const router = useRouter();
const menuRef = useRef<HTMLDivElement | null>(null);
const queryClient = useQueryClient();

const handleLogout = async () => {
await axiosClientHelper.post('/auth/logout');
await alert('로그아웃 했습니다.');
queryClient.invalidateQueries();
router.replace('/');
};

useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setIsMenuOpen(false);
}
}

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

return (
<>
{isFetching ? (
<Skeleton />
) : (
data && (
<div ref={menuRef} className='group relative flex cursor-pointer items-center gap-3 leading-none' onClick={() => setIsMenuOpen((prev) => !prev)}>
<Avatar email={data.email} className='transition-shadow group-hover:shadow-sm group-hover:shadow-slate-400' />
<span className='hidden font-medium md:block'>{data.nickname}</span>

{isMenuOpen && (
<motion.ul //
initial={{ scale: 0.8, y: -20 }}
animate={{ scale: 1, y: 0 }}
className='absolute right-0 top-[calc(100%+6px)] w-28 rounded-md border border-gray-30 bg-white p-[4px]'
>
<li>
<Link href='/mypage' className='flex h-10 w-full items-center justify-center rounded-md text-md hover:bg-violet-10 hover:font-medium hover:text-violet-20'>
내정보
</Link>
</li>
<li>
<button className='flex h-10 w-full items-center justify-center text-md hover:bg-violet-10 hover:font-medium hover:text-violet-20' onClick={handleLogout}>
로그아웃
</button>
</li>
</motion.ul>
)}
</div>
)
)}
</>
);
}

export default function Profile({ nickname, profileImageUrl, onClick }: ProfileProps) {
function Skeleton() {
return (
<button onClick={onClick} className='mr-[40px] flex items-center justify-center gap-[12px] pl-[12px] sm:pl-[24px] md:pl-[32px]'>
<span className='flex h-[34px] w-[34px] items-center justify-center rounded-full border-2 border-white bg-green-30 text-white sm:h-[38px] sm:w-[38px]'>{profileImageUrl}</span>
<span className='hidden text-[16px] md:block'>{nickname}</span>
</button>
<div className='flex items-center gap-3 leading-none'>
<div className='flex aspect-square w-9 animate-pulse rounded-full bg-gray-20' />
<span className='block h-4 w-16 animate-pulse rounded-md bg-gray-20' />
</div>
);
}
30 changes: 0 additions & 30 deletions src/components/dashboard-header/SettingBtn.tsx

This file was deleted.

13 changes: 0 additions & 13 deletions src/components/dashboard-header/Title.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/components/dashboard/DetailMembers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { isAxiosError } from 'axios';
import { useState } from 'react';
import { useParams } from 'next/navigation';
import useAlert from '@/hooks/useAlert';
import { useMembersQuery, useRemoveMember } from '@/apis/members/quries';
import { useMembersQuery, useRemoveMember } from '@/apis/members/queries';
import { getErrorMessage } from '@/utils/errorMessage';
import PaginationWithCounter from '@/components/pagination/PaginationWithCounter';
import { Table, TableBody, TableCell, TableCol, TableColGroup, TableHead, TableHeadCell, TableRow } from '@/components/ui/Table/Table';
Expand Down
Loading