Skip to content

Commit 7f5597e

Browse files
authored
Merge pull request #105 from part3-4team-Taskify/feature/Gnb
[Feat, Refactor] Gnb: 드롭다운 메뉴 'MemberListMenu' 추가 / hooks: 공통 훅 useClosePopup 추가
2 parents 8e4b86b + edd9f3b commit 7f5597e

File tree

6 files changed

+159
-61
lines changed

6 files changed

+159
-61
lines changed

src/components/gnb/Avatars.tsx

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,36 +21,34 @@ export const MemberAvatars: React.FC<MemberAvatarsProps> = ({
2121
if (variant === "mydashboard") return null;
2222

2323
return (
24-
<div className="pr-[15px] md:pr-[25px] lg:pr-[30px]">
25-
<div className="flex items-center justify-center -space-x-3">
26-
{isLoading ? (
27-
<SkeletonUser />
28-
) : (
29-
<>
30-
{members.slice(0, MAX_VISIBLE_MEMBERS).map((member) => (
31-
<div key={member.id} className="relative rounded-full">
32-
{member.profileImageUrl ? (
33-
<div className="relative w-[34px] h-[34px] md:w-[38px] md:h-[38px] rounded-full border-[2px] border-white overflow-hidden">
34-
<Image
35-
src={member.profileImageUrl}
36-
alt={member.nickname}
37-
fill
38-
className="object-cover"
39-
/>
40-
</div>
41-
) : (
42-
<RandomProfile name={member.nickname} />
43-
)}
44-
</div>
45-
))}
46-
{members.length > MAX_VISIBLE_MEMBERS && (
47-
<div className="relative w-[34px] h-[34px] md:w-[38px] md:h-[38px] rounded-full bg-[#F4D7DA] font-16m text-[#D25B68] border-[2px] border-white overflow-hidden">
48-
+{members.length - MAX_VISIBLE_MEMBERS}
49-
</div>
50-
)}
51-
</>
52-
)}
53-
</div>
24+
<div className="flex items-center justify-center -space-x-3">
25+
{isLoading ? (
26+
<SkeletonUser />
27+
) : (
28+
<>
29+
{members.slice(0, MAX_VISIBLE_MEMBERS).map((member) => (
30+
<div key={member.id} className="relative rounded-full">
31+
{member.profileImageUrl ? (
32+
<div className="relative w-[34px] h-[34px] md:w-[38px] md:h-[38px] rounded-full border-[2px] border-white overflow-hidden">
33+
<Image
34+
src={member.profileImageUrl}
35+
alt={member.nickname}
36+
fill
37+
className="object-cover"
38+
/>
39+
</div>
40+
) : (
41+
<RandomProfile name={member.nickname} />
42+
)}
43+
</div>
44+
))}
45+
{members.length > MAX_VISIBLE_MEMBERS && (
46+
<div className="relative flex items-center justify-center w-[34px] h-[34px] md:w-[38px] md:h-[38px] rounded-full bg-[#F4D7DA] font-16m text-[#D25B68] border-[2px] border-white overflow-hidden">
47+
+{members.length - MAX_VISIBLE_MEMBERS}
48+
</div>
49+
)}
50+
</>
51+
)}
5452
</div>
5553
);
5654
};

src/components/gnb/HeaderDashboard.tsx

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect, useRef } from "react";
1+
import React, { useState, useEffect } from "react";
22
import { useRouter } from "next/router";
33
import SkeletonUser from "@/shared/skeletonUser";
44
import Image from "next/image";
@@ -9,6 +9,7 @@ import { getDashboardById } from "@/api/dashboards";
99
import { TEAM_ID } from "@/constants/team";
1010
import { MemberAvatars, UserAvatars } from "@/components/gnb/Avatars";
1111
import UserMenu from "@/components/gnb/UserMenu";
12+
import MemberListMenu from "@/components/gnb/MemberListMenu";
1213
import InviteDashboard from "@/components/modal/InviteDashboard";
1314

1415
interface HeaderDashboardProps {
@@ -26,6 +27,7 @@ const HeaderDashboard: React.FC<HeaderDashboardProps> = ({
2627
const [user, setUser] = useState<UserType | null>(null);
2728
const [members, setMembers] = useState<MemberType[]>([]);
2829
const [isMenuOpen, setIsMenuOpen] = useState(false);
30+
const [isListOpen, setIsListOpen] = useState(false);
2931
const [errorMessage, setErrorMessage] = useState("");
3032
const [dashboard, setDashboard] = useState<{
3133
title: string;
@@ -113,14 +115,22 @@ const HeaderDashboard: React.FC<HeaderDashboardProps> = ({
113115
</p>
114116
)}
115117
<div className="flex items-center gap-[8px]">
116-
<p className="text-base text-black3 font-bold md:text-xl">{title}</p>
118+
<p
119+
className={`text-base text-black3 font-bold md:text-xl ${variant !== "mydashboard" ? "hidden md:block" : ""}`}
120+
>
121+
{title}
122+
</p>
117123
{dashboard?.createdByMe && (
118124
<Image
119125
src="/svgs/crown.svg"
120126
alt="왕관 아이콘"
121127
width={22}
122128
height={22}
123-
className="inline-block"
129+
className={
130+
variant === "mydashboard"
131+
? "inline-block"
132+
: "hidden md:inline-block"
133+
}
124134
unoptimized
125135
priority
126136
/>
@@ -129,7 +139,9 @@ const HeaderDashboard: React.FC<HeaderDashboardProps> = ({
129139

130140
<div className="flex items-center">
131141
{/*관리 버튼*/}
132-
<div className="flex gap-[6px] md:gap-[16px] pr-[40px]">
142+
<div
143+
className={`flex gap-[6px] md:gap-[16px] ${variant === "mydashboard" ? "pr-[22px] md:pr-[32px]" : ""}`}
144+
>
133145
<button
134146
onClick={() => {
135147
if (dashboardId) {
@@ -167,13 +179,33 @@ const HeaderDashboard: React.FC<HeaderDashboardProps> = ({
167179
</div>
168180

169181
{/*멤버 목록*/}
170-
<MemberAvatars
171-
members={members}
172-
isLoading={isLoading}
173-
variant={variant}
174-
/>
182+
{variant !== "mydashboard" && (
183+
<div className="relative flex items-center justify-center w-[150px] md:w-[190px] h-[60px] md:h-[70px]">
184+
{isLoading ? (
185+
<SkeletonUser />
186+
) : (
187+
members && (
188+
<div
189+
onClick={() => setIsListOpen((prev) => !prev)}
190+
className="flex items-center pl-[15px] md:pl-[25px] lg:pl-[30px] pr-[15px] md:pr-[25px] lg:pr-[30px] cursor-pointer"
191+
>
192+
<MemberAvatars
193+
members={members}
194+
isLoading={isLoading}
195+
variant={variant}
196+
/>
197+
</div>
198+
)
199+
)}
200+
<MemberListMenu
201+
members={members}
202+
isListOpen={isListOpen}
203+
setIsListOpen={setIsListOpen}
204+
/>
205+
</div>
206+
)}
175207

176-
{/*드롭다운 메뉴 너비 지정 목적의 섹션 구분용 div*/}
208+
{/*드롭다운 메뉴 너비 지정 목적의 유저 정보 섹션 div*/}
177209
<div className="relative flex items-center h-[60px] md:h-[70px] pr-[10px] md:pr-[30px] lg:pr-[80px]">
178210
{/*구분선*/}
179211
<div className="h-[34px] md:h-[38px] w-[1px] bg-[var(--color-gray3)]" />
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React, { useRef } from "react";
2+
import { useClosePopup } from "@/hooks/useClosePopup";
3+
import { UserAvatars } from "@/components/gnb/Avatars";
4+
import { MemberType } from "@/types/users";
5+
6+
interface MemberListMenuProps {
7+
isListOpen: boolean;
8+
setIsListOpen: React.Dispatch<React.SetStateAction<boolean>>;
9+
members: MemberType[];
10+
}
11+
12+
const MemberListMenu: React.FC<MemberListMenuProps> = ({
13+
isListOpen,
14+
setIsListOpen,
15+
members,
16+
}) => {
17+
const ref = useRef<HTMLDivElement>(null);
18+
19+
useClosePopup(ref, () => setIsListOpen(false));
20+
21+
return (
22+
<div
23+
ref={ref}
24+
className={`absolute top-full right-0 w-full z-50
25+
bg-white border border-[#D9D9D9] shadow
26+
transition-all duration-200 ease-out
27+
${isListOpen ? "opacity-100 translate-y-0" : "opacity-0 -translate-y-2 pointer-events-none"}`}
28+
>
29+
<ul className="flex flex-col font-16r max-h-[300px] overflow-y-auto">
30+
{members.map((member) => (
31+
<li
32+
key={member.id}
33+
className="px-4 py-2 flex items-center gap-2 hover:bg-gray-100"
34+
>
35+
<UserAvatars user={member} />
36+
<span className="text-black3 text-sm md:text-base">
37+
{member.nickname}
38+
</span>
39+
</li>
40+
))}
41+
</ul>
42+
</div>
43+
);
44+
};
45+
46+
export default MemberListMenu;

src/components/gnb/UserMenu.tsx

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import React, { useEffect, useRef } from "react";
1+
import React, { useRef } from "react";
22
import { useRouter } from "next/router";
3-
import { User, LogOut } from "lucide-react";
3+
import { useClosePopup } from "@/hooks/useClosePopup";
4+
import { User, LogOut, FolderPen } from "lucide-react";
45

56
interface UserMenuProps {
67
isMenuOpen: boolean;
@@ -9,27 +10,13 @@ interface UserMenuProps {
910

1011
const UserMenu: React.FC<UserMenuProps> = ({ isMenuOpen, setIsMenuOpen }) => {
1112
const router = useRouter();
12-
const dropdownRef = useRef<HTMLDivElement | null>(null);
13+
const ref = useRef<HTMLDivElement>(null);
1314

14-
useEffect(() => {
15-
const handleClickOutside = (e: MouseEvent) => {
16-
if (
17-
dropdownRef.current &&
18-
!dropdownRef.current.contains(e.target as Node)
19-
) {
20-
setIsMenuOpen(false);
21-
}
22-
};
23-
24-
document.addEventListener("mousedown", handleClickOutside);
25-
return () => {
26-
document.removeEventListener("mousedown", handleClickOutside);
27-
};
28-
}, [setIsMenuOpen]);
15+
useClosePopup(ref, () => setIsMenuOpen(false));
2916

3017
return (
3118
<div
32-
ref={dropdownRef}
19+
ref={ref}
3320
className={`absolute top-full right-0 w-full
3421
bg-white border border-[#D9D9D9] shadow z-50
3522
transition-all duration-200 ease-out
@@ -40,7 +27,14 @@ const UserMenu: React.FC<UserMenuProps> = ({ isMenuOpen, setIsMenuOpen }) => {
4027
className="flex justify-center items-center w-full pt-3 pb-2 font-16r text-black3 hover:bg-[var(--color-gray5)]"
4128
>
4229
<User size={20} className="md:hidden" />
43-
<span className="hidden md:block">마이페이지</span>
30+
<span className="hidden md:block">내 정보</span>
31+
</button>
32+
<button
33+
onClick={() => router.push("/mydashboard")}
34+
className="flex justify-center items-center w-full pt-2 pb-2 font-16r text-black3 hover:bg-[var(--color-gray5)]"
35+
>
36+
<FolderPen size={20} className="md:hidden" />
37+
<span className="hidden md:block">내 대시보드</span>
4438
</button>
4539
<button
4640
onClick={() => {

src/components/table/member/RandomProfile.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default function RandomProfile({ name, index }: RandomProfileProps) {
1515

1616
return (
1717
<div
18-
className={`flex items-center justify-center text-white font-bold rounded-full ${bgColor} w-[34px] h-[34px] md:w-[38px] md:h-[38px] border-[2px]`}
18+
className={`flex items-center justify-center text-white font-16sb rounded-full ${bgColor} w-[34px] h-[34px] md:w-[38px] md:h-[38px] border-[2px]`}
1919
>
2020
{name[0]}
2121
</div>

src/hooks/useClosePopup.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useEffect } from "react";
2+
3+
export const useClosePopup = (
4+
ref: React.RefObject<HTMLElement | null>,
5+
onClose: () => void
6+
) => {
7+
useEffect(() => {
8+
const handleClickOutside = (e: MouseEvent) => {
9+
if (ref.current && !ref.current.contains(e.target as Node)) {
10+
onClose();
11+
}
12+
};
13+
14+
const handleKeyDown = (e: KeyboardEvent) => {
15+
if (e.key === "Escape") {
16+
onClose();
17+
}
18+
};
19+
20+
document.addEventListener("mousedown", handleClickOutside);
21+
document.addEventListener("keydown", handleKeyDown);
22+
23+
return () => {
24+
document.removeEventListener("mousedown", handleClickOutside);
25+
document.removeEventListener("keydown", handleKeyDown);
26+
};
27+
}, [ref, onClose]);
28+
};

0 commit comments

Comments
 (0)