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: 2 additions & 4 deletions src/entities/user/api/user-api.server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { cache } from "react";

import { cookies } from "next/headers";

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

export const getUserProfileServer = cache(async (targetUserId: number) => {
export const getUserProfileServer = async (targetUserId: number) => {
const cookieStore = await cookies();
const accessToken = cookieStore.get("accessToken")?.value;

Expand Down Expand Up @@ -45,4 +43,4 @@ export const getUserProfileServer = cache(async (targetUserId: number) => {
} catch {
return null;
}
});
};
7 changes: 6 additions & 1 deletion src/entities/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@ export type { UserProfileType } from "./model/types";
export type { UserBasicInfoResponseType } from "./model/types";
export { DASHBOARD_TABS } from "./model/dashboard-tabs.config";
export type { TabIdType, TabConfig } from "./model/types";
export { getUserProfile, updateUserProfileImage, updateUserName } from "./api/user-api";
export {
getUserProfile,
getUserBasic,
updateUserProfileImage,
updateUserName,
} from "./api/user-api";
6 changes: 4 additions & 2 deletions src/features/user/api/use-my-profile.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";

import { getUserProfile, updateUserProfileImage, updateUserName } from "@/entities/user";
import type { UserProfileType } from "@/entities/user"; // 타입 위치에 맞게 수정
import type { UserProfileType } from "@/entities/user";
import { showToast } from "@/shared/lib/utils/toast/show-toast";

export const userKeys = {
profile: (userId: number) => ["user", "profile", userId] as const,
all: ["user"] as const,
basic: () => [...userKeys.all, "basic"] as const,
profile: (userId: number) => [...userKeys.all, "profile", userId] as const,
};

export function useUserProfile(userId: number, initialData?: UserProfileType) {
Expand Down
24 changes: 24 additions & 0 deletions src/features/user/api/use-user-basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";

import { useQuery } from "@tanstack/react-query";

import { getUserBasic } from "@/entities/user";
import { userKeys } from "@/features/user/api/use-my-profile";

async function fetchUserBasic() {
try {
return await getUserBasic();
} catch {
return null;
}
}

export function useUserBasic() {
return useQuery({
queryKey: userKeys.basic(),
queryFn: fetchUserBasic,
retry: false,
staleTime: 1000 * 60 * 5,
refetchOnWindowFocus: false,
});
}
32 changes: 8 additions & 24 deletions src/widgets/header/header.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
import { cookies } from "next/headers";
import { Suspense } from "react";

import Image from "next/image";
import Link from "next/link";

import { LogIn } from "lucide-react";

import Logo from "@/shared/assets/icons/windfall.svg";
import { ROUTES } from "@/shared/config/routes";
import { Container } from "@/shared/ui";
import Button from "@/shared/ui/button/button";
import HeaderActions from "@/widgets/header/ui/header-actions";
import HeaderAuthSlot from "@/widgets/header/ui/header-auth-slot";
import HeaderSearch from "@/widgets/header/ui/header-search";

export async function Header() {
const cookieStore = await cookies();
const userId = cookieStore.get("userId")?.value;

const numericUserId = Number(userId);
const hasValidUserId = Number.isInteger(numericUserId) && numericUserId > 0;

export function Header() {
return (
<header className="bg-background h-header sticky top-0 z-50 hidden select-none md:flex">
<Container className="lg:px flex items-center justify-around gap-4 px-2 min-[960px]:px-3 lg:px-5 xl:px-7">
Expand All @@ -28,18 +19,11 @@ export async function Header() {
</h1>
</Link>

<HeaderSearch />
<Suspense fallback={<div className="flex-1" aria-hidden />}>
<HeaderSearch />
</Suspense>

{hasValidUserId ? (
<HeaderActions userId={numericUserId} />
) : (
<Button asChild size="lg">
<Link href={ROUTES.login} aria-label="로그인">
<LogIn className="size-5" />
<span className="font-semibold">로그인</span>
</Link>
</Button>
)}
<HeaderAuthSlot />
</Container>
</header>
);
Expand Down
18 changes: 10 additions & 8 deletions src/widgets/header/ui/header-actions.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
"use client";

import Link from "next/link";

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

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

export default async function HeaderActions({ userId }: { userId: number }) {
const profile = await getUserProfileServer(userId);

const avatarUrl = profile?.avatarUrl;
const avatarAlt = profile?.name ?? "프로필";

export default function HeaderActions({
avatarUrl,
avatarAlt,
}: {
avatarUrl?: string;
avatarAlt: string;
}) {
return (
<div className="flex shrink-0 items-center gap-3">
<Button asChild aria-label="경매 등록" size="icon-lg" className="min-[960px]:hidden">
Expand All @@ -33,7 +35,7 @@ export default async function HeaderActions({ userId }: { userId: number }) {
<span className="absolute top-1 right-1 h-2 w-2 rounded-full bg-red-500" />
</Button>

<HeaderUserMenu avatarUrl={avatarUrl ?? undefined} avatarAlt={avatarAlt} />
<HeaderUserMenu avatarUrl={avatarUrl} avatarAlt={avatarAlt} />
</div>
);
}
36 changes: 36 additions & 0 deletions src/widgets/header/ui/header-auth-slot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";

import Link from "next/link";

import { LogIn } from "lucide-react";

import { useUserBasic } from "@/features/user/api/use-user-basic";
import { ROUTES } from "@/shared/config/routes";
import Button from "@/shared/ui/button/button";
import HeaderActions from "@/widgets/header/ui/header-actions";

export default function HeaderAuthSlot() {
const { data, isPending } = useUserBasic();

if (isPending) {
return <div className="h-10 w-24" aria-hidden />;
}

if (!data) {
return (
<Button asChild size="lg">
<Link href={ROUTES.login} aria-label="로그인">
<LogIn className="size-5" />
<span className="font-semibold">로그인</span>
</Link>
</Button>
);
}

return (
<HeaderActions
avatarUrl={data.userProfileUrl || undefined}
avatarAlt={data.username || "프로필"}
/>
);
}
4 changes: 4 additions & 0 deletions src/widgets/header/ui/header-user-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import Link from "next/link";
import { useRouter } from "next/navigation";

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

import { userKeys } from "@/features/user/api/use-my-profile";
import { ROUTES } from "@/shared/config/routes";
import { showToast } from "@/shared/lib/utils/toast/show-toast";
import {
Expand All @@ -26,6 +28,7 @@ interface HeaderUserMenuProps {

export default function HeaderUserMenu({ avatarUrl, avatarAlt }: HeaderUserMenuProps) {
const router = useRouter();
const queryClient = useQueryClient();
const { resolvedTheme, setTheme } = useTheme();
const isDarkMode = resolvedTheme === "dark";
const themeLabel = isDarkMode ? "라이트 모드" : "다크 모드";
Expand All @@ -40,6 +43,7 @@ export default function HeaderUserMenu({ avatarUrl, avatarAlt }: HeaderUserMenuP
} catch {
showToast.error("로그아웃에 실패했습니다. 잠시 후 다시 시도해주세요.");
} finally {
queryClient.removeQueries({ queryKey: userKeys.all, exact: false });
router.refresh();
router.push(ROUTES.login);
}
Expand Down