Skip to content

Commit a6f723e

Browse files
committed
refactor(auth): 헤더 인증 로직을 클라이언트 유저 상태로 전환
- 헤더의 인증/프로필 로직을 React Query 기반 클라이언트 상태로 이동 - 사용자 기본 정보를 조회하는 useUserBasic 훅 추가 - 로그인 상태 분기를 담당하는 HeaderAuthSlot 컴포넌트 추가 - HeaderActions가 서버에서 프로필을 조회하지 않고 avatar props를 받도록 수정 - 로그아웃 시 사용자 관련 query cache를 정리하도록 처리
1 parent 4bdacba commit a6f723e

7 files changed

Lines changed: 84 additions & 35 deletions

File tree

src/entities/user/api/user-api.server.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { cache } from "react";
2-
31
import { cookies } from "next/headers";
42

53
import { fetch as apiFetch } from "@/shared/api/server";
@@ -14,7 +12,7 @@ interface UserApiResponse {
1412
isOwner: boolean;
1513
}
1614

17-
export const getUserProfileServer = cache(async (targetUserId: number) => {
15+
export const getUserProfileServer = async (targetUserId: number) => {
1816
const cookieStore = await cookies();
1917
const accessToken = cookieStore.get("accessToken")?.value;
2018

@@ -45,4 +43,4 @@ export const getUserProfileServer = cache(async (targetUserId: number) => {
4543
} catch {
4644
return null;
4745
}
48-
});
46+
};

src/entities/user/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,9 @@ export type { UserProfileType } from "./model/types";
33
export type { UserBasicInfoResponseType } from "./model/types";
44
export { DASHBOARD_TABS } from "./model/dashboard-tabs.config";
55
export type { TabIdType, TabConfig } from "./model/types";
6-
export { getUserProfile, updateUserProfileImage, updateUserName } from "./api/user-api";
6+
export {
7+
getUserProfile,
8+
getUserBasic,
9+
updateUserProfileImage,
10+
updateUserName,
11+
} from "./api/user-api";
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"use client";
2+
3+
import { useQuery } from "@tanstack/react-query";
4+
5+
import { getUserBasic } from "@/entities/user";
6+
7+
const userBasicKey = ["user", "basic"] as const;
8+
9+
async function fetchUserBasic() {
10+
try {
11+
return await getUserBasic();
12+
} catch {
13+
return null;
14+
}
15+
}
16+
17+
export function useUserBasic() {
18+
return useQuery({
19+
queryKey: userBasicKey,
20+
queryFn: fetchUserBasic,
21+
retry: false,
22+
staleTime: 1000 * 60 * 5,
23+
refetchOnWindowFocus: false,
24+
});
25+
}

src/widgets/header/header.tsx

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,12 @@
1-
import { cookies } from "next/headers";
21
import Image from "next/image";
32
import Link from "next/link";
43

5-
import { LogIn } from "lucide-react";
6-
74
import Logo from "@/shared/assets/icons/windfall.svg";
8-
import { ROUTES } from "@/shared/config/routes";
95
import { Container } from "@/shared/ui";
10-
import Button from "@/shared/ui/button/button";
11-
import HeaderActions from "@/widgets/header/ui/header-actions";
6+
import HeaderAuthSlot from "@/widgets/header/ui/header-auth-slot";
127
import HeaderSearch from "@/widgets/header/ui/header-search";
138

149
export async function Header() {
15-
const cookieStore = await cookies();
16-
const userId = cookieStore.get("userId")?.value;
17-
18-
const numericUserId = Number(userId);
19-
const hasValidUserId = Number.isInteger(numericUserId) && numericUserId > 0;
20-
2110
return (
2211
<header className="bg-background h-header sticky top-0 z-50 hidden select-none md:flex">
2312
<Container className="lg:px flex items-center justify-around gap-4 px-2 min-[960px]:px-3 lg:px-5 xl:px-7">
@@ -30,16 +19,7 @@ export async function Header() {
3019

3120
<HeaderSearch />
3221

33-
{hasValidUserId ? (
34-
<HeaderActions userId={numericUserId} />
35-
) : (
36-
<Button asChild size="lg">
37-
<Link href={ROUTES.login} aria-label="로그인">
38-
<LogIn className="size-5" />
39-
<span className="font-semibold">로그인</span>
40-
</Link>
41-
</Button>
42-
)}
22+
<HeaderAuthSlot />
4323
</Container>
4424
</header>
4525
);

src/widgets/header/ui/header-actions.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1+
"use client";
2+
13
import Link from "next/link";
24

35
import { Bell, Plus } from "lucide-react";
46

5-
import { getUserProfileServer } from "@/entities/user/api/user-api.server";
67
import { ROUTES } from "@/shared/config/routes";
78
import { Button } from "@/shared/ui";
89
import HeaderUserMenu from "@/widgets/header/ui/header-user-menu";
910

10-
export default async function HeaderActions({ userId }: { userId: number }) {
11-
const profile = await getUserProfileServer(userId);
12-
13-
const avatarUrl = profile?.avatarUrl;
14-
const avatarAlt = profile?.name ?? "프로필";
15-
11+
export default function HeaderActions({
12+
avatarUrl,
13+
avatarAlt,
14+
}: {
15+
avatarUrl?: string;
16+
avatarAlt: string;
17+
}) {
1618
return (
1719
<div className="flex shrink-0 items-center gap-3">
1820
<Button asChild aria-label="경매 등록" size="icon-lg" className="min-[960px]:hidden">
@@ -33,7 +35,7 @@ export default async function HeaderActions({ userId }: { userId: number }) {
3335
<span className="absolute top-1 right-1 h-2 w-2 rounded-full bg-red-500" />
3436
</Button>
3537

36-
<HeaderUserMenu avatarUrl={avatarUrl ?? undefined} avatarAlt={avatarAlt} />
38+
<HeaderUserMenu avatarUrl={avatarUrl} avatarAlt={avatarAlt} />
3739
</div>
3840
);
3941
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
5+
import { LogIn } from "lucide-react";
6+
7+
import { useUserBasic } from "@/features/user/api/use-user-basic";
8+
import { ROUTES } from "@/shared/config/routes";
9+
import Button from "@/shared/ui/button/button";
10+
import HeaderActions from "@/widgets/header/ui/header-actions";
11+
12+
export default function HeaderAuthSlot() {
13+
const { data, isPending } = useUserBasic();
14+
15+
if (isPending) {
16+
return <div className="h-10 w-24" aria-hidden />;
17+
}
18+
19+
if (!data) {
20+
return (
21+
<Button asChild size="lg">
22+
<Link href={ROUTES.login} aria-label="로그인">
23+
<LogIn className="size-5" />
24+
<span className="font-semibold">로그인</span>
25+
</Link>
26+
</Button>
27+
);
28+
}
29+
30+
return (
31+
<HeaderActions
32+
avatarUrl={data.userProfileUrl || undefined}
33+
avatarAlt={data.username || "프로필"}
34+
/>
35+
);
36+
}

src/widgets/header/ui/header-user-menu.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import Link from "next/link";
44
import { useRouter } from "next/navigation";
55

6+
import { useQueryClient } from "@tanstack/react-query";
67
import { LogOut, Moon, Sun, User } from "lucide-react";
78
import { useTheme } from "next-themes";
89

@@ -26,6 +27,7 @@ interface HeaderUserMenuProps {
2627

2728
export default function HeaderUserMenu({ avatarUrl, avatarAlt }: HeaderUserMenuProps) {
2829
const router = useRouter();
30+
const queryClient = useQueryClient();
2931
const { resolvedTheme, setTheme } = useTheme();
3032
const isDarkMode = resolvedTheme === "dark";
3133
const themeLabel = isDarkMode ? "라이트 모드" : "다크 모드";
@@ -40,6 +42,7 @@ export default function HeaderUserMenu({ avatarUrl, avatarAlt }: HeaderUserMenuP
4042
} catch {
4143
showToast.error("로그아웃에 실패했습니다. 잠시 후 다시 시도해주세요.");
4244
} finally {
45+
queryClient.setQueryData(["user", "basic"], null);
4346
router.refresh();
4447
router.push(ROUTES.login);
4548
}

0 commit comments

Comments
 (0)