Skip to content

Commit 8567c5c

Browse files
authored
Merge pull request #100 from part3-4team-Taskify/feature/Gnb
[Feat, Refactor] Gnb: 드롭다운 메뉴 추가, 멤버 목록 아이콘 수정
2 parents 78a3ddd + 89c08fe commit 8567c5c

File tree

9 files changed

+234
-104
lines changed

9 files changed

+234
-104
lines changed

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@tanstack/react-query": "^5.68.0",
1313
"axios": "^1.8.3",
1414
"clsx": "^2.1.1",
15+
"lucide-react": "^0.485.0",
1516
"moment": "^2.30.1",
1617
"next": "15.2.2",
1718
"prettier-plugin-tailwindcss": "^0.6.11",

public/svgs/dummy-icon.png

-636 Bytes
Binary file not shown.

src/components/gnb/Avatars.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React from "react";
2+
import SkeletonUser from "@/shared/skeletonUser";
3+
import RandomProfile from "../table/member/RandomProfile";
4+
import Image from "next/image";
5+
import { MemberType, UserType } from "@/types/users";
6+
7+
/*멤버 프로필 아이콘*/
8+
interface MemberAvatarsProps {
9+
members: MemberType[];
10+
isLoading: boolean;
11+
variant?: "mydashboard" | "dashboard" | "mypage";
12+
}
13+
14+
const MAX_VISIBLE_MEMBERS = 4;
15+
const memberIconWrapperClass =
16+
"w-[34px] h-[34px] md:w-[38px] md:h-[38px] rounded-full border-[2px] border-white overflow-hidden";
17+
18+
export const MemberAvatars: React.FC<MemberAvatarsProps> = ({
19+
members,
20+
isLoading,
21+
variant,
22+
}) => {
23+
if (variant === "mydashboard") return null;
24+
25+
return (
26+
<div className="pr-[15px] md:pr-[25px] lg:pr-[30px]">
27+
<div className="flex -space-x-3">
28+
{isLoading ? (
29+
<SkeletonUser />
30+
) : (
31+
<>
32+
{members.slice(0, MAX_VISIBLE_MEMBERS).map((member) => (
33+
<div key={member.id}>
34+
{member.profileImageUrl ? (
35+
<Image
36+
src={member.profileImageUrl}
37+
alt={member.nickname}
38+
fill
39+
className={`${memberIconWrapperClass} object-cover`}
40+
/>
41+
) : (
42+
<RandomProfile name={member.nickname} />
43+
)}
44+
</div>
45+
))}
46+
{members.length > MAX_VISIBLE_MEMBERS && (
47+
<div
48+
className={`${memberIconWrapperClass} bg-[#F4D7DA] font-16m text-[#D25B68]`}
49+
>
50+
+{members.length - MAX_VISIBLE_MEMBERS}
51+
</div>
52+
)}
53+
</>
54+
)}
55+
</div>
56+
</div>
57+
);
58+
};
59+
60+
/*유저 프로필 아이콘*/
61+
interface UserAvatarProps {
62+
user: UserType;
63+
}
64+
65+
export const UserAvatars: React.FC<UserAvatarProps> = ({ user }) => (
66+
<div className="relative w-[34px] h-[34px] md:w-[38px] md:h-[38px] rounded-full overflow-hidden">
67+
{user.profileImageUrl ? (
68+
<Image
69+
src={user.profileImageUrl}
70+
alt="유저 프로필 아이콘"
71+
fill
72+
className="object-cover"
73+
/>
74+
) : (
75+
<RandomProfile name={user.nickname} />
76+
)}
77+
</div>
78+
);

src/components/gnb/HeaderDashboard.tsx

Lines changed: 60 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect } from "react";
1+
import React, { useState, useEffect, useRef } from "react";
22
import { useRouter } from "next/router";
33
import SkeletonUser from "@/shared/skeletonUser";
44
import Image from "next/image";
@@ -7,7 +7,8 @@ import { getMembers } from "@/api/members";
77
import { getUserInfo } from "@/api/user";
88
import { getDashboardById } from "@/api/dashboards";
99
import { TEAM_ID } from "@/constants/team";
10-
import RandomProfile from "@/components/table/member/RandomProfile";
10+
import { MemberAvatars, UserAvatars } from "@/components/gnb/Avatars";
11+
import UserMenu from "@/components/gnb/UserMenu";
1112
import InviteDashboard from "@/components/modal/InviteDashboard";
1213

1314
interface HeaderDashboardProps {
@@ -16,24 +17,22 @@ interface HeaderDashboardProps {
1617
dashboardId?: string | string[];
1718
}
1819

19-
const MAX_VISIBLE_MEMBERS = 4;
20-
const memberIconWrapperClass =
21-
"relative flex items-center justify-center w-[34px] h-[34px] md:w-[38px] md:h-[38px] rounded-full border-[2px] border-white";
22-
2320
const HeaderDashboard: React.FC<HeaderDashboardProps> = ({
2421
variant,
2522
dashboardId,
2623
}) => {
2724
const router = useRouter();
25+
const [isLoading, setIsLoading] = useState(true);
2826
const [user, setUser] = useState<UserType | null>(null);
2927
const [members, setMembers] = useState<MemberType[]>([]);
28+
const [isMenuOpen, setIsMenuOpen] = useState(false);
29+
const [errorMessage, setErrorMessage] = useState("");
3030
const [dashboard, setDashboard] = useState<{
3131
title: string;
3232
createdByMe: boolean;
3333
} | null>(null);
34-
const [errorMessage, setErrorMessage] = useState("");
35-
const [isLoading, setIsLoading] = useState(true);
3634

35+
/*초대하기 모달 상태 관리*/
3736
const [isModalOpen, setIsModalOpen] = useState(false);
3837
const openInviteModal = () => {
3938
setIsModalOpen(true);
@@ -42,23 +41,6 @@ const HeaderDashboard: React.FC<HeaderDashboardProps> = ({
4241
setIsModalOpen(false);
4342
};
4443

45-
/*유저 정보 api 호출*/
46-
useEffect(() => {
47-
const fetchUser = async () => {
48-
try {
49-
const user = await getUserInfo({ teamId: TEAM_ID });
50-
setUser(user);
51-
} catch (error) {
52-
console.error("유저 정보 불러오기 실패", error);
53-
setErrorMessage("유저 정보를 불러오지 못했습니다.");
54-
} finally {
55-
setIsLoading(false);
56-
}
57-
};
58-
59-
fetchUser();
60-
}, []);
61-
6244
/*멤버 목록 api 호출*/
6345
useEffect(() => {
6446
const fetchMembers = async () => {
@@ -77,7 +59,24 @@ const HeaderDashboard: React.FC<HeaderDashboardProps> = ({
7759
}
7860
}, [dashboardId, variant]);
7961

80-
/*대시보드 이름 api 호출*/
62+
/*유저 정보 api 호출*/
63+
useEffect(() => {
64+
const fetchUser = async () => {
65+
try {
66+
const user = await getUserInfo({ teamId: TEAM_ID });
67+
setUser(user);
68+
} catch (error) {
69+
console.error("유저 정보 불러오기 실패", error);
70+
setErrorMessage("유저 정보를 불러오지 못했습니다.");
71+
} finally {
72+
setIsLoading(false);
73+
}
74+
};
75+
76+
fetchUser();
77+
}, []);
78+
79+
/*대시보드 api 호출*/
8180
useEffect(() => {
8281
const fetchDashboard = async () => {
8382
if (variant === "dashboard" && dashboardId) {
@@ -129,14 +128,14 @@ const HeaderDashboard: React.FC<HeaderDashboardProps> = ({
129128
</div>
130129

131130
<div className="flex items-center">
132-
{/*관리 / 초대하기 버튼*/}
131+
{/*관리 버튼*/}
133132
<div className="flex gap-[6px] md:gap-[16px] pr-[40px]">
134133
<button
135134
onClick={() => {
136135
if (dashboardId) {
137136
router.push(`/dashboard/${dashboardId}/edit`);
138137
} else {
139-
router.push("/mydashboard");
138+
router.push("/mypage");
140139
}
141140
}}
142141
className="flex items-center justify-center w-[49px] h-[30px] md:w-[85px] md:h-[36px] lg:w-[88px] lg:h-[40px] rounded-[8px] border border-[#D9D9D9] gap-[10px] cursor-pointer"
@@ -150,7 +149,7 @@ const HeaderDashboard: React.FC<HeaderDashboardProps> = ({
150149
/>
151150
<span className="text-sm md:text-base text-gray1">관리</span>
152151
</button>
153-
152+
{/*초대하기 버튼*/}
154153
<button
155154
onClick={openInviteModal}
156155
className="flex items-center justify-center w-[73px] h-[30px] md:w-[109px] md:h-[36px] lg:w-[116px] lg:h-[40px] rounded-[8px] border border-[#D9D9D9] gap-[10px] cursor-pointer"
@@ -167,72 +166,39 @@ const HeaderDashboard: React.FC<HeaderDashboardProps> = ({
167166
{isModalOpen && <InviteDashboard onClose={closeInviteModal} />}
168167
</div>
169168

170-
{/*멤버 목록, 나머지 멤버 수 +n 아이콘으로 표시*/}
171-
172-
{variant !== "mydashboard" && (
173-
<div className="flex -space-x-3">
174-
{isLoading ? (
175-
<SkeletonUser />
176-
) : (
177-
<>
178-
{members.slice(0, MAX_VISIBLE_MEMBERS).map((member) => (
179-
<div className={memberIconWrapperClass} key={member.id}>
180-
{member.profileImageUrl ? (
181-
<Image
182-
src={member.profileImageUrl}
183-
alt={member.nickname}
184-
fill
185-
className="object-cover"
186-
/>
187-
) : (
188-
<RandomProfile name={member.nickname} />
189-
)}
190-
</div>
191-
))}
192-
{members.length > MAX_VISIBLE_MEMBERS && (
193-
<div
194-
className={`${memberIconWrapperClass} bg-[#F4D7DA] font-16m text-[#D25B68]`}
195-
>
196-
+{members.length - MAX_VISIBLE_MEMBERS}
197-
</div>
198-
)}
199-
</>
200-
)}
201-
</div>
202-
)}
203-
204-
{/*구분선*/}
205-
<div className="pl-[15px] pr-[20px] md:pl-[25px] md:pr-[30px] lg:pl-[30px] lg:pr-[35px]">
206-
<div className="flex items-center justify-center h-[34px] md:h-[38px] w-[1px] bg-[var(--color-gray3)]"></div>
207-
</div>
208-
209-
{/*유저 정보*/}
210-
{isLoading ? (
211-
<SkeletonUser />
212-
) : (
213-
user && (
214-
<div
215-
onClick={() => router.push("/mypage")}
216-
className="flex items-center pr-[10px] md:pr-[30px] lg:pr-[80px] gap-[12px] cursor-default"
217-
>
218-
<div className="relative w-[34px] h-[34px] md:w-[38px] md:h-[38px] rounded-full">
219-
{user.profileImageUrl ? (
220-
<Image
221-
src={user.profileImageUrl}
222-
alt="유저 프로필 아이콘"
223-
fill
224-
className="object-cover"
225-
/>
226-
) : (
227-
<RandomProfile name={user.nickname} />
228-
)}
169+
{/*멤버 목록*/}
170+
<MemberAvatars
171+
members={members}
172+
isLoading={isLoading}
173+
variant={variant}
174+
/>
175+
176+
{/*드롭다운 메뉴 너비 지정 목적의 섹션 구분용 div*/}
177+
<div className="relative flex items-center h-[60px] md:h-[70px] pr-[10px] md:pr-[30px] lg:pr-[80px]">
178+
{/*구분선*/}
179+
<div className="h-[34px] md:h-[38px] w-[1px] bg-[var(--color-gray3)]" />
180+
181+
{/*유저 정보*/}
182+
{isLoading ? (
183+
<SkeletonUser />
184+
) : (
185+
user && (
186+
<div
187+
onClick={() => setIsMenuOpen((prev) => !prev)}
188+
className="flex items-center gap-[12px] pl-[20px] md:pl-[30px] lg:pl-[35px] cursor-pointer"
189+
>
190+
<UserAvatars user={user} />
191+
<span className="hidden md:block text-black3 md:text-base md:font-medium">
192+
{user.nickname}
193+
</span>
194+
<UserMenu
195+
isMenuOpen={isMenuOpen}
196+
setIsMenuOpen={setIsMenuOpen}
197+
/>
229198
</div>
230-
<span className="hidden md:block text-black3 md:text-base md:font-medium">
231-
{user.nickname}
232-
</span>
233-
</div>
234-
)
235-
)}
199+
)
200+
)}
201+
</div>
236202
</div>
237203
</div>
238204
</header>

src/components/gnb/HeaderDefault.tsx

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
11
import React from "react";
22
import { useRouter } from "next/router";
33
import Image from "next/image";
4+
import useUserStore from "@/store/useUserStore";
45

56
interface HeaderDefaultProps {
67
variant?: "white" | "black";
78
}
89

910
const HeaderDefault: React.FC<HeaderDefaultProps> = ({ variant = "white" }) => {
1011
const router = useRouter();
12+
const { user, clearUser } = useUserStore();
1113

14+
const isLoggedIn = !user;
1215
const isWhite = variant === "white";
1316

17+
const handleAuthClick = () => {
18+
if (isLoggedIn) {
19+
clearUser();
20+
localStorage.removeItem("accessToken");
21+
localStorage.removeItem("expiresAt");
22+
window.location.reload();
23+
} else {
24+
router.push("login");
25+
}
26+
};
27+
1428
return (
1529
<header
1630
className={`w-full h-[60px] md:h-[70px] flex items-center justify-center
@@ -41,19 +55,21 @@ const HeaderDefault: React.FC<HeaderDefaultProps> = ({ variant = "white" }) => {
4155
</div>
4256
<div className="flex space-x-[24px] md:space-x-[36px]">
4357
<button
44-
onClick={() => router.push(`/login`)}
58+
onClick={handleAuthClick}
4559
className={`text-sm md:text-base cursor-pointer
4660
${isWhite ? "text-black3" : "text-white"}`}
4761
>
48-
로그인
62+
{isLoggedIn ? "로그아웃" : "로그인"}
4963
</button>
50-
<button
51-
onClick={() => router.push(`/signup`)}
52-
className={`text-sm md:text-base cursor-pointer
64+
{!isLoggedIn && (
65+
<button
66+
onClick={() => router.push(`/signup`)}
67+
className={`text-sm md:text-base cursor-pointer
5368
${isWhite ? "text-black3" : "text-white"}`}
54-
>
55-
회원가입
56-
</button>
69+
>
70+
회원가입
71+
</button>
72+
)}
5773
</div>
5874
</div>
5975
</header>

0 commit comments

Comments
 (0)