From bd99a1e3ef8d913eeb0be9cb7378f7119d414d66 Mon Sep 17 00:00:00 2001 From: manNomi Date: Fri, 31 Jul 2026 20:10:11 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EC=96=B4=EB=93=9C?= =?UTF-8?q?=EB=AF=BC=20=EC=9D=B8=EC=A6=9D=20=ED=86=A0=ED=81=B0=20=ED=9D=90?= =?UTF-8?q?=EB=A6=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/lib/api/auth.test.ts | 71 +++++++++++++++++++++++++++++ apps/admin/src/lib/api/auth.ts | 8 ++-- apps/admin/src/types/auth.ts | 1 - 3 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 apps/admin/src/lib/api/auth.test.ts diff --git a/apps/admin/src/lib/api/auth.test.ts b/apps/admin/src/lib/api/auth.test.ts new file mode 100644 index 000000000..001ad224c --- /dev/null +++ b/apps/admin/src/lib/api/auth.test.ts @@ -0,0 +1,71 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { create, loadAccessToken, post } = vi.hoisted(() => { + const hoistedPost = vi.fn(); + return { + create: vi.fn(() => ({ post: hoistedPost })), + loadAccessToken: vi.fn(), + post: hoistedPost, + }; +}); + +vi.mock("axios", () => ({ + default: { + create, + }, +})); + +vi.mock("@/lib/env", () => ({ + createMissingAdminApiServerUrlError: () => new Error("API server URL is required"), + getAdminApiServerUrl: () => "https://api.example.com", +})); + +vi.mock("@/lib/utils/localStorage", () => ({ + loadAccessToken, +})); + +import { adminSignInApi, adminSignOutApi, reissueAccessTokenApi } from "./auth"; + +describe("어드민 인증 API", () => { + beforeEach(() => { + loadAccessToken.mockReset(); + post.mockReset(); + }); + + it("어드민 refresh token 쿠키를 주고받도록 credentials를 포함한다", () => { + expect(create).toHaveBeenCalledWith({ + baseURL: "https://api.example.com", + withCredentials: true, + }); + }); + + it("어드민 전용 로그인 API를 호출한다", async () => { + post.mockResolvedValue({ data: { accessToken: "access-token" } }); + + await adminSignInApi("admin@example.com", "password"); + + expect(post).toHaveBeenCalledWith("/admin/auth/sign-in", { + email: "admin@example.com", + password: "password", + }); + }); + + it("어드민 전용 재발급 API를 호출한다", async () => { + post.mockResolvedValue({ data: { accessToken: "reissued-access-token" } }); + + await reissueAccessTokenApi(); + + expect(post).toHaveBeenCalledWith("/admin/auth/reissue"); + }); + + it("어드민 전용 로그아웃 API에 access token을 전달한다", async () => { + loadAccessToken.mockReturnValue("access-token"); + post.mockResolvedValue({ data: undefined }); + + await adminSignOutApi(); + + expect(post).toHaveBeenCalledWith("/admin/auth/sign-out", undefined, { + headers: { Authorization: "Bearer access-token" }, + }); + }); +}); diff --git a/apps/admin/src/lib/api/auth.ts b/apps/admin/src/lib/api/auth.ts index 49507fd0a..ffc4070f7 100644 --- a/apps/admin/src/lib/api/auth.ts +++ b/apps/admin/src/lib/api/auth.ts @@ -10,6 +10,8 @@ const authAxiosInstance = axios.create({ withCredentials: true, }); +const ADMIN_AUTH_PATH = "/admin/auth"; + const assertAdminApiServerUrl = () => { if (!API_SERVER_URL) { throw createMissingAdminApiServerUrlError(); @@ -18,12 +20,12 @@ const assertAdminApiServerUrl = () => { export const adminSignInApi = (email: string, password: string): Promise> => { assertAdminApiServerUrl(); - return authAxiosInstance.post("/auth/email/sign-in", { email, password }); + return authAxiosInstance.post(`${ADMIN_AUTH_PATH}/sign-in`, { email, password }); }; export const reissueAccessTokenApi = (): Promise> => { assertAdminApiServerUrl(); - return authAxiosInstance.post("/auth/reissue"); + return authAxiosInstance.post(`${ADMIN_AUTH_PATH}/reissue`); }; export const adminSignOutApi = (): Promise> => { @@ -31,7 +33,7 @@ export const adminSignOutApi = (): Promise> => { const accessToken = loadAccessToken(); - return authAxiosInstance.post("/auth/sign-out", undefined, { + return authAxiosInstance.post(`${ADMIN_AUTH_PATH}/sign-out`, undefined, { headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined, }); }; diff --git a/apps/admin/src/types/auth.ts b/apps/admin/src/types/auth.ts index 46fa8d298..a89f995bd 100644 --- a/apps/admin/src/types/auth.ts +++ b/apps/admin/src/types/auth.ts @@ -1,6 +1,5 @@ export interface AdminSignInResponse { accessToken: string; - refreshToken?: string; } export interface ReissueAccessTokenResponse { From 8aa7f62ae94c1459ee249d6ac89b8afecb6eea04 Mon Sep 17 00:00:00 2001 From: manNomi Date: Sat, 1 Aug 2026 13:45:45 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20=EC=96=B4?= =?UTF-8?q?=EB=93=9C=EB=AF=BC=20access=20token=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=ED=82=A4=EB=A5=BC=20=EC=96=B4=EB=93=9C=EB=AF=BC=20=EC=A0=84?= =?UTF-8?q?=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/lib/utils/localStorage.test.ts | 37 +++++++++++++++++++ apps/admin/src/lib/utils/localStorage.ts | 8 ++-- 2 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 apps/admin/src/lib/utils/localStorage.test.ts diff --git a/apps/admin/src/lib/utils/localStorage.test.ts b/apps/admin/src/lib/utils/localStorage.test.ts new file mode 100644 index 000000000..972ff6431 --- /dev/null +++ b/apps/admin/src/lib/utils/localStorage.test.ts @@ -0,0 +1,37 @@ +import { afterEach, describe, expect, it } from "vitest"; + +import { loadAccessToken, removeAccessToken, saveAccessToken } from "./localStorage"; + +const ADMIN_ACCESS_TOKEN_KEY = "adminAccessToken"; + +describe("어드민 access token localStorage 저장", () => { + afterEach(() => { + localStorage.clear(); + }); + + it("어드민 전용 키(adminAccessToken)로 저장한다", () => { + saveAccessToken("access-token"); + + expect(localStorage.getItem(ADMIN_ACCESS_TOKEN_KEY)).toBe("access-token"); + }); + + it("어드민 전용 키(adminAccessToken)에서 조회한다", () => { + localStorage.setItem(ADMIN_ACCESS_TOKEN_KEY, "access-token"); + + expect(loadAccessToken()).toBe("access-token"); + }); + + it("어드민 전용 키(adminAccessToken)를 제거한다", () => { + localStorage.setItem(ADMIN_ACCESS_TOKEN_KEY, "access-token"); + + removeAccessToken(); + + expect(localStorage.getItem(ADMIN_ACCESS_TOKEN_KEY)).toBeNull(); + }); + + it("웹 앱의 레거시 accessToken 키는 더 이상 사용하지 않는다", () => { + saveAccessToken("access-token"); + + expect(localStorage.getItem("accessToken")).toBeNull(); + }); +}); diff --git a/apps/admin/src/lib/utils/localStorage.ts b/apps/admin/src/lib/utils/localStorage.ts index d4d285da1..0d28c6ae0 100644 --- a/apps/admin/src/lib/utils/localStorage.ts +++ b/apps/admin/src/lib/utils/localStorage.ts @@ -1,6 +1,8 @@ +const ADMIN_ACCESS_TOKEN_KEY = "adminAccessToken"; + export const loadAccessToken = () => { try { - return localStorage.getItem("accessToken"); + return localStorage.getItem(ADMIN_ACCESS_TOKEN_KEY); } catch (err) { console.error("Could not load access token", err); return null; @@ -9,7 +11,7 @@ export const loadAccessToken = () => { export const saveAccessToken = (token: string) => { try { - localStorage.setItem("accessToken", token); + localStorage.setItem(ADMIN_ACCESS_TOKEN_KEY, token); } catch (err) { console.error("Could not save access token", err); } @@ -17,7 +19,7 @@ export const saveAccessToken = (token: string) => { export const removeAccessToken = () => { try { - localStorage.removeItem("accessToken"); + localStorage.removeItem(ADMIN_ACCESS_TOKEN_KEY); } catch (err) { console.error("Could not remove access token", err); } From 68456d277cc846b6869762c795dce8047208e271 Mon Sep 17 00:00:00 2001 From: manNomi Date: Sat, 1 Aug 2026 13:45:52 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=94=A5=20=EB=AF=B8=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=20deprecated=20localStorageUtils=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/utils/localStorageUtils.ts | 37 ------------------------- 1 file changed, 37 deletions(-) delete mode 100644 apps/web/src/utils/localStorageUtils.ts diff --git a/apps/web/src/utils/localStorageUtils.ts b/apps/web/src/utils/localStorageUtils.ts deleted file mode 100644 index 49f8345a7..000000000 --- a/apps/web/src/utils/localStorageUtils.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @deprecated 이 함수들은 더 이상 사용되지 않습니다. - * 대신 useAuthStore의 setAccessToken, clearAccessToken을 사용하세요. - * Zustand persist가 자동으로 localStorage를 관리합니다. - * - * @example - * // ❌ 사용하지 마세요: - * import { saveAccessTokenToLS } from "@/utils/localStorageUtils"; - * saveAccessTokenToLS(token); - * - * // ✅ 대신 이렇게 사용하세요: - * import useAuthStore from "@/lib/zustand/useAuthStore"; - * const { setAccessToken } = useAuthStore(); - * setAccessToken(token); - */ - -/** - * @deprecated useAuthStore의 setAccessToken을 사용하세요 - */ -export const saveAccessTokenToLS = (token: string) => { - localStorage.setItem("accessToken", token); -}; - -/** - * @deprecated useAuthStore의 clearAccessToken을 사용하세요 - */ -export const removeAccessTokenToLS = () => { - localStorage.removeItem("accessToken"); -}; - -/** - * @deprecated useAuthStore의 accessToken 상태를 직접 읽으세요 - */ -export const getAccessTokenFromLS = (): string | null => { - if (typeof window === "undefined") return null; - return localStorage.getItem("accessToken"); -};