Skip to content

Commit

Permalink
refactor: 로그인 상태 관리 수정 및 토큰 갱신 (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
Han-wo authored Dec 3, 2024
1 parent 4fcc789 commit 1bfa19b
Show file tree
Hide file tree
Showing 17 changed files with 499 additions and 26 deletions.
9 changes: 7 additions & 2 deletions src/api/kospi-kosdac/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ interface StockData {
async function getInitialStockData(): Promise<StockData> {
try {
const [kospiRes, kosdaqRes] = await Promise.all([
fetch(`${process.env.NEXT_PUBLIC_API_URL}/home/index/kospi`),
fetch(`${process.env.NEXT_PUBLIC_API_URL}/home/index/kosdaq`),
fetch(`${process.env.NEXT_PUBLIC_API_URL}/home/index/kospi`, {
cache: "no-store",
}),
fetch(`${process.env.NEXT_PUBLIC_API_URL}/home/index/kosdaq`, {
cache: "no-store",
}),
]);

if (!kospiRes.ok || !kosdaqRes.ok) {
Expand All @@ -37,4 +41,5 @@ async function getInitialStockData(): Promise<StockData> {
};
}
}

export default getInitialStockData;
2 changes: 2 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Metadata } from "next";

import NavBar from "@/components/nav-bar";
import AuthInitializer from "@/provider/AuthInitializer";
import AuthRefreshHandler from "@/utils/auth-handler";

import Providers from "./provider";

Expand All @@ -26,6 +27,7 @@ export default async function RootLayout({
<body className="flex">
<Providers dehydratedState={dehydratedState}>
<AuthInitializer />
<AuthRefreshHandler />
<NavBar />
<main className="ml-82 flex-1">{children}</main>
</Providers>
Expand Down
9 changes: 8 additions & 1 deletion src/app/main/_components/asset-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { useAuth } from "@/hooks/use-auth";
import coinsIcon from "@/images/coin.png";
import shieldIcon from "@/images/shield.png";

import { AssetInfoSkeleton } from "./skeleton";

interface AssetResponse {
asset: string;
}
Expand All @@ -33,7 +35,8 @@ const formatKoreanCurrency = (amount: number) => {
const INITIAL_ASSET = 100_000_000;

export default function AssetInfo() {
const { isAuthenticated, memberNickName, token, clearAuth } = useAuth();
const { isAuthenticated, memberNickName, token, clearAuth, isInitialized } =
useAuth();
const [assetInfo, setAssetInfo] = useState<AssetResponse | null>(null);

useEffect(() => {
Expand Down Expand Up @@ -95,6 +98,10 @@ export default function AssetInfo() {
await clearAuth();
};

if (!isInitialized) {
return <AssetInfoSkeleton />;
}

if (!isAuthenticated) {
return (
<div className="relative h-308 w-300 rounded-10 bg-[#D9FFE5] p-21">
Expand Down
56 changes: 56 additions & 0 deletions src/app/main/_components/skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export function AssetInfoSkeleton() {
return (
<div className="relative h-308 w-300 rounded-10 bg-[#D9FFE5] p-21">
<div>
<div className="h-7 w-20 animate-pulse rounded bg-gray-200" />
<div className="mt-10">
<div className="h-32 w-102 animate-pulse rounded bg-gray-200" />
<div className="pt-40">
<div className="h-30 w-120 animate-pulse rounded bg-gray-200" />
</div>
</div>
</div>
<div className="mt-30 flex h-105 w-264 animate-pulse flex-col items-center justify-center rounded-8 bg-gray-100">
<div className="h-4 w-32 rounded bg-gray-200" />
<div className="mt-4 h-8 w-40 rounded bg-gray-200" />
</div>
</div>
);
}

export function MyStockInfoSkeleton() {
return (
<div className="relative h-308 w-300 rounded-10 bg-[#F5F5F5] p-21">
<div className="mb-10">
<div className="h-7 w-20 animate-pulse rounded bg-gray-200" />
<div className="mt-12 space-y-2">
<div className="h-5 w-28 animate-pulse rounded bg-gray-200" />
<div className="h-5 w-20 animate-pulse rounded bg-gray-200" />
</div>
</div>
<div className="mt-90">
<div className="mb-10 h-6 w-32 animate-pulse rounded bg-gray-200" />
<div className="space-y-4">
{[1, 2, 3].map((idx) => (
<div
key={idx}
className="flex items-center justify-between border-b border-gray-100 py-12 last:border-0"
>
<div className="flex items-center gap-8">
<div className="size-24 animate-pulse rounded bg-gray-200" />
<div className="space-y-2">
<div className="h-5 w-24 animate-pulse rounded bg-gray-200" />
<div className="h-4 w-16 animate-pulse rounded bg-gray-200" />
</div>
</div>
<div className="space-y-2 text-right">
<div className="h-5 w-20 animate-pulse rounded bg-gray-200" />
<div className="h-4 w-24 animate-pulse rounded bg-gray-200" />
</div>
</div>
))}
</div>
</div>
</div>
);
}
1 change: 1 addition & 0 deletions src/app/main/_components/stock-carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ function StockIndexCarousel({ initialData }: StockIndexCarouselProps) {
initialData,
refetchInterval: 300000,
refetchOnWindowFocus: false,
retry: 1,
});

return (
Expand Down
8 changes: 7 additions & 1 deletion src/app/main/_components/stock-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { TableBody } from "@/components/common/table";
import { useAuth } from "@/hooks/use-auth";
import magnifierIcon from "@/images/stockInfo.png";

import { MyStockInfoSkeleton } from "./skeleton";

interface StockHolding {
stockName: string;
currentPrice: number;
Expand Down Expand Up @@ -87,7 +89,7 @@ function StockTable({ data }: { data: StockHolding[] }) {
}

export default function MyStockInfo() {
const { isAuthenticated, token } = useAuth();
const { isAuthenticated, token, isInitialized } = useAuth();
const [stockCount, setStockCount] = useState<string | null>(null);
const [stockHoldings, setStockHoldings] = useState<StockHolding[]>([]);

Expand Down Expand Up @@ -130,6 +132,10 @@ export default function MyStockInfo() {
}
}, [isAuthenticated, token]);

if (!isInitialized) {
return <MyStockInfoSkeleton />;
}

if (!isAuthenticated || stockCount === "0") {
return (
<div className="relative h-308 w-300 rounded-10 bg-[#F5F5F5] p-21">
Expand Down
10 changes: 8 additions & 2 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ import StockIndexCarousel from "@/app/main/_components/stock-carousel";
import MyStockInfo from "@/app/main/_components/stock-info";

export default async function Home() {
const queryClient = new QueryClient();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 0,
retry: 1,
},
},
});

const initialStockData = await getInitialStockData();
await queryClient.prefetchQuery({
Expand Down Expand Up @@ -52,7 +59,6 @@ export default async function Home() {
<NewsCarousel initialData={initialNews} />
</div>
</div>

{/* 우측 사이드바 */}
<div className="w-330 shrink-0">
<div className="sticky top-24 flex flex-col gap-16">
Expand Down
94 changes: 94 additions & 0 deletions src/components/common/auth/refresh-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"use client";

import clsx from "clsx";
import { useEffect } from "react";

import BaseModal from "@/components/common/modal";

interface RefreshModalProps {
isOpen: boolean;
onClose: () => void;
remainingRefreshes: number;
onAccept: () => void;
onDecline: () => void;
}

export default function RefreshModal({
isOpen,
onClose,
remainingRefreshes,
onAccept,
onDecline,
}: RefreshModalProps) {
useEffect(() => {
if (!isOpen) {
onClose();
}
}, [isOpen, onClose]);

const handleRefresh = () => {
onAccept();
onClose();
};

const handleLogout = () => {
onClose();
onDecline();
};

return (
<BaseModal isOpen={isOpen} onClose={onClose}>
<div className="h-210 overflow-hidden rounded-2xl bg-white p-20 shadow-xl">
<div className="flex items-center gap-3 text-yellow-600">
<h2 className="text-24-700 text-gray-900">세션 연장</h2>
</div>

<div className="mt-10">
<p className="text-16-600 text-gray-600">
로그인 세션이 5분 후에 만료됩니다.
<br />
세션을 연장하시겠습니까?
</p>
</div>

<div className="mt-10 rounded-lg bg-blue-50 p-10">
<p className="text-16-600 text-blue-700">
남은 연장 횟수:{" "}
<span className="text-14-600">{remainingRefreshes}</span>
</p>
</div>

<div className="mt-10 flex justify-end gap-8">
<button
type="button"
onClick={handleLogout}
className={clsx(
"rounded-lg border border-gray-300 px-8 py-6",
"text-16-600 text-gray-700",
"transition",
"hover:bg-gray-50",
"active:bg-gray-100",
)}
>
로그아웃
</button>
<button
type="button"
onClick={handleRefresh}
disabled={remainingRefreshes <= 0}
className={clsx(
"rounded-lg bg-blue-600 px-8 py-6",
"text-16-600 text-white",
"transition",
"hover:bg-blue-700",
"active:bg-blue-800",
"disabled:bg-gray-300",
)}
>
연장하기
</button>
</div>
</div>
</BaseModal>
);
}
99 changes: 99 additions & 0 deletions src/components/common/modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"use client";

import { AnimatePresence, motion } from "framer-motion";
import { useEffect } from "react";

interface BaseModalProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}

const overlayVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { duration: 0.2 },
},
};

const modalVariants = {
hidden: {
opacity: 0,
scale: 0.75,
y: 20,
},
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: {
type: "spring",
stiffness: 300,
damping: 30,
},
},
exit: {
opacity: 0,
scale: 0.75,
y: 20,
transition: { duration: 0.2 },
},
};

export default function BaseModal({
isOpen,
onClose,
children,
}: BaseModalProps) {
useEffect(() => {
if (!isOpen) {
return undefined;
}

function handleEscape(e: KeyboardEvent) {
if (e.key === "Escape") {
onClose();
}
}

document.body.style.overflow = "hidden";
window.addEventListener("keydown", handleEscape);

return function cleanupModalEffect() {
window.removeEventListener("keydown", handleEscape);
document.body.style.overflow = "unset";
};
}, [isOpen, onClose]);

return (
<AnimatePresence mode="wait">
{isOpen && (
<motion.div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
initial="hidden"
animate="visible"
exit="hidden"
>
<motion.div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
variants={overlayVariants}
initial="hidden"
animate="visible"
exit="hidden"
onClick={onClose}
/>
<motion.div
className="relative z-10 w-full max-w-md"
variants={modalVariants}
initial="hidden"
animate="visible"
exit="exit"
>
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
2 changes: 1 addition & 1 deletion src/components/nav-bar/_components/nav-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import ContentIcon from "@/public/icons/contents.svg";
import ContentActiveIcon from "@/public/icons/contents-active.svg";
import HomeIcon from "@/public/icons/home.svg";
import HomeActiveIcon from "@/public/icons/home-active.svg";
import LogoIcon from "@/public/icons/logo.svg";
import LogoIcon from "@/public/icons/Logo.svg";
import AccountIcon from "@/public/icons/mypage.svg";
import AccountActiveIcon from "@/public/icons/mypage-active.svg";

Expand Down
Loading

0 comments on commit 1bfa19b

Please sign in to comment.