Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: 로그인 상태 관리 수정 및 토큰 갱신 #32

Merged
merged 9 commits into from
Dec 3, 2024
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
Loading