Skip to content
Open
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
10 changes: 8 additions & 2 deletions jobdri/src/components/common/buttons/TextButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import Icon from "@/components/common/icons/Icon";

export type TextButtonSize = "small" | "large";
export type TextButtonStyle = "primary" | "secondary";
export type TextButtonIconPosition = "right" | "left";
export type TextButtonIconPosition = "right" | "left" | "null";
export type HoverType = "textOnly" | "none";

interface TextButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
label?: ReactNode;
size?: TextButtonSize;
styleType?: TextButtonStyle;
iconPosition?: TextButtonIconPosition;
hover?: HoverType;
}

const sizeStyles: Record<TextButtonSize, string> = {
Expand Down Expand Up @@ -38,6 +40,7 @@ export default function TextButton({
size = "small",
styleType = "primary",
iconPosition = "right",
hover = "none",
className,
type = "button",
...buttonProps
Expand Down Expand Up @@ -65,7 +68,10 @@ export default function TextButton({
<button
type={type}
className={clsx(
"inline-flex items-center gap-0 rounded-toast-s [font-feature-settings:'liga'_off,'clig'_off] hover:bg-fill-hover",
"inline-flex items-center gap-0 rounded-toast-s [font-feature-settings:'liga'_off,'clig'_off]",
hover === "none"
? " hover:bg-fill-hover"
: "hover:text-fill-tertiary-default-pressed hover:bg-transparent",
buttonProps.disabled ? "cursor-not-allowed" : "cursor-pointer",
isLeftLarge
? "py-1.5 pr-3 pl-2 text-b16-med"
Expand Down
39 changes: 38 additions & 1 deletion jobdri/src/components/common/lnb/Lnb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ import { createPortal } from "react-dom";
import clsx from "clsx";
import Icon, { type IconType } from "@/components/common/icons/Icon";
import { ModalNotice } from "@/components/common/modal";
import { AUTH_STORAGE_KEYS, getStoredAuthEmail } from "@/lib/auth";
import {
AUTH_STORAGE_KEYS,
getStoredAuthEmail,
requestLogout,
clearAuthTokens,
} from "@/lib/auth";
import Logo from "@/assets/ic_LOGO_minimum_favi.svg";
import { fetchCreditBalance } from "@/lib/api/credit";
import { TextButton } from "../buttons";

type LnbItemKey = "experience" | "apply";

Expand Down Expand Up @@ -100,6 +106,26 @@ export default function Lnb({ initialActiveItem, email, className }: LnbProps) {
.catch(() => {});
}, []);

const handleLogout = async () => {
const accessToken = window.localStorage.getItem(
AUTH_STORAGE_KEYS.accessToken,
);
const refreshToken = window.localStorage.getItem(
AUTH_STORAGE_KEYS.refreshToken,
);

if (accessToken && refreshToken) {
try {
await requestLogout(accessToken, refreshToken);
} catch (error) {
console.error("서버 로그아웃 처리 실패:", error);
}
}

clearAuthTokens();
router.replace("/login");
};
Comment on lines +109 to +127

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Wrap logout flow in try/finally to guarantee cleanup/navigation.

At Line 110 and Line 113, localStorage.getItem can throw in restricted-storage environments. If that happens, clearAuthTokens() and router.replace("/login") at Line 125-126 are skipped. Move cleanup/redirect to finally.

Suggested patch
-  const handleLogout = async () => {
-    const accessToken = window.localStorage.getItem(
-      AUTH_STORAGE_KEYS.accessToken,
-    );
-    const refreshToken = window.localStorage.getItem(
-      AUTH_STORAGE_KEYS.refreshToken,
-    );
-
-    if (accessToken && refreshToken) {
-      try {
-        await requestLogout(accessToken, refreshToken);
-      } catch (error) {
-        console.error("서버 로그아웃 처리 실패:", error);
-      }
-    }
-
-    clearAuthTokens();
-    router.replace("/login");
-  };
+  const handleLogout = async () => {
+    try {
+      const accessToken = window.localStorage.getItem(
+        AUTH_STORAGE_KEYS.accessToken,
+      );
+      const refreshToken = window.localStorage.getItem(
+        AUTH_STORAGE_KEYS.refreshToken,
+      );
+
+      if (accessToken && refreshToken) {
+        try {
+          await requestLogout(accessToken, refreshToken);
+        } catch (error) {
+          console.error("서버 로그아웃 처리 실패:", error);
+        }
+      }
+    } finally {
+      clearAuthTokens();
+      router.replace("/login");
+    }
+  };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jobdri/src/components/common/lnb/Lnb.tsx` around lines 109 - 127, The
handleLogout function retrieves tokens from localStorage which can throw errors
in restricted-storage environments, causing the cleanup and navigation logic at
the end (clearAuthTokens and router.replace calls) to be skipped. Refactor the
handleLogout function to wrap the entire logout flow in a try/finally block,
moving the clearAuthTokens and router.replace("/login") calls into the finally
block to ensure they always execute regardless of any errors thrown during token
retrieval or the requestLogout call.


return (
<>
<aside
Expand Down Expand Up @@ -206,6 +232,17 @@ export default function Lnb({ initialActiveItem, email, className }: LnbProps) {
{displayEmail}
</span>
</div>
<div
className={`flex w-full items-center gap-2 py-1.5 ${isFold ? "justify-center px-0" : "px-2"}`}
>
<TextButton
label="로그아웃"
styleType="secondary"
iconPosition="null"
hover="textOnly"
onClick={handleLogout}
/>
</div>
</div>
</aside>

Expand Down
13 changes: 13 additions & 0 deletions jobdri/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export interface LoginRequest {
password: string;
}

interface LogoutRequest {
accessToken: string;
refreshToken: string;
}

export interface SignupRequest {
name?: string | null;
email: string;
Expand Down Expand Up @@ -265,3 +270,11 @@ export function getAuthHeaders(): Record<string, string> {

return token ? { Authorization: `Bearer ${token}` } : {};
}

export async function requestLogout(accessToken: string, refreshToken: string) {
await postAuth<null>(
"/api/auth/logout",
{ accessToken, refreshToken },
"로그아웃",
);
}