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
9 changes: 0 additions & 9 deletions service/app/public/avatar.svg

This file was deleted.

2 changes: 1 addition & 1 deletion service/app/src/app/__tests__/helpers/auth.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ setup("authenticate", async ({ page }) => {
await page.goto("/")

await page.waitForURL("/")
await expect(page.getByRole("button", { name: "내 게임" })).toBeVisible()
await expect(page.getByRole("link", { name: "내 게임" })).toBeVisible()

await page.context().storageState({ path: authFile })
})
2 changes: 1 addition & 1 deletion service/app/src/app/__tests__/home.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ test.describe("홈 화면: 비로그인 상태", () => {
page,
}) => {
await pageObj.clickKakaoLoginButton()
await page.waitForURL("/login")
await page.waitForURL(/\/login(\?|$)/, { waitUntil: "commit" })
})
})

Expand Down
6 changes: 3 additions & 3 deletions service/app/src/app/__tests__/homePOM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ export class HomePOM {
// 네비게이션 영역 (비로그인 상태)
this.homeLogoImage = page.getByAltText("홈 로고")
this.themeToggleButton = page.getByTestId("theme-toggle-button")
this.kakaoLoginButton = page.getByRole("button", {
this.kakaoLoginButton = page.getByRole("link", {
name: "간편로그인해서 게임 만들기",
})

// 네비게이션 영역 (로그인 상태)
this.myGamesButton = page.getByRole("button", { name: "내 게임" })
this.createGameButton = page.getByRole("button", {
this.myGamesButton = page.getByRole("link", { name: "내 게임" })
this.createGameButton = page.getByRole("link", {
name: "게임 만들기",
exact: true,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,15 @@ test.describe("게임 설정 - 팀 관리 E2E 테스트", () => {
}) => {
const entry = "/games"
await page.evaluate((e) => {
window.sessionStorage.setItem("entry", e)
window.sessionStorage.setItem("entry:game", e)
}, entry)
await gameSetupPage.clickHomeLogo()
await expect(page).toHaveURL(entry)
})

test("entry 정보가 없으면 홈 페이지로 이동해야 한다", async ({ page }) => {
await page.evaluate(() => {
window.sessionStorage.removeItem("entry")
window.sessionStorage.removeItem("entry:game")
})
await gameSetupPage.clickHomeLogo()
await expect(page).toHaveURL("/")
Expand All @@ -137,7 +137,7 @@ test.describe("게임 설정 - 팀 관리 E2E 테스트", () => {
}) => {
const entry = "/games"
await page.evaluate((e) => {
window.sessionStorage.setItem("entry", e)
window.sessionStorage.setItem("entry:game", e)
}, entry)
await gameSetupPage.clickExitIcon()
await expect(page).toHaveURL(entry)
Expand Down
11 changes: 9 additions & 2 deletions service/app/src/app/login/kakao/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
"use client"

import { useRouter, useSearchParams } from "next/navigation"
import { Suspense, useEffect } from "react"
import { Suspense, useEffect, useRef } from "react"

import { useAuthStore } from "@/entities/auth"
import { clearEntry, ENTRY_KEYS, getEntry } from "@/shared/lib/saveEntry"

function KakaoCallbackContent() {
const router = useRouter()
const searchParams = useSearchParams()
const { login } = useAuthStore()
const handledRef = useRef(false)

useEffect(() => {
if (handledRef.current) return
handledRef.current = true

const handleCallback = async () => {
const code = searchParams.get("code")
const error = searchParams.get("error")
Expand All @@ -19,7 +24,9 @@ function KakaoCallbackContent() {
throw new Error(error || "인증 코드를 받지 못했습니다.")

await login(code)
router.push("/")
const returnTo = getEntry(ENTRY_KEYS.auth) ?? "/"
clearEntry(ENTRY_KEYS.auth)
router.replace(returnTo)
}

handleCallback()
Expand Down
40 changes: 40 additions & 0 deletions service/app/src/app/login/kakaoLoginClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use client"

import { useRouter } from "next/navigation"
import { useEffect } from "react"

import { MSW_MOCK_CODE } from "@/mocks/handlers/auth"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

프로덕션 코드에서 mock 모듈 import는 잠재적 위험이 있습니다.

MSW_MOCK_CODE는 개발 환경에서만 사용되지만, import는 항상 실행됩니다. 프로덕션 빌드에서 mock 모듈이 제외되면 런타임 오류가 발생할 수 있습니다.

🔎 조건부 import 또는 인라인 상수 사용 제안
-import { MSW_MOCK_CODE } from "@/mocks/handlers/auth"
-import { ENTRY_KEYS, saveEntry } from "@/shared/lib/saveEntry"
+import { ENTRY_KEYS, saveEntry } from "@/shared/lib/saveEntry"
+
+const MSW_MOCK_CODE = "someValidCode"

또는 dynamic import를 사용할 수 있습니다:

useEffect(() => {
  if (process.env.NODE_ENV === "development") {
    import("@/mocks/handlers/auth").then(({ MSW_MOCK_CODE }) => {
      router.replace(`/login/kakao/?code=${MSW_MOCK_CODE}`)
    })
    return
  }
  // ...
}, [])

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In service/app/src/app/login/kakaoLoginClient.tsx around line 6, the file
statically imports MSW_MOCK_CODE from a mock module which is only for
development and can cause runtime errors in production; remove the top-level
import and instead load the mock value conditionally at runtime (either inline a
dev-only constant guarded by process.env.NODE_ENV === "development" or use
dynamic import inside the effect when NODE_ENV === "development") so production
builds never reference the mock module.

import { ENTRY_KEYS, saveEntry } from "@/shared/lib/saveEntry"

export function KakaoLoginClient({ returnTo }: { returnTo?: string }) {
const router = useRouter()

const kakaoClientId = process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID
const redirectUri = process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI

useEffect(() => {
if (returnTo) saveEntry(ENTRY_KEYS.auth, returnTo)

if (process.env.NODE_ENV === "development") {
router.replace(`/login/kakao/?code=${MSW_MOCK_CODE}`)
return
}

if (!kakaoClientId || !redirectUri) return

const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${kakaoClientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code`

router.replace(kakaoAuthUrl)
}, [router, kakaoClientId, redirectUri, returnTo])

return (
<div className="flex min-h-screen items-center justify-center bg-background-primary">
<div className="flex flex-col items-center gap-16">
<div className="size-32 animate-spin rounded-full border-4 border-blue-400 border-t-transparent"></div>
<p className="text-sm text-text-primary">
카카오 로그인으로 이동 중...
</p>
</div>
</div>
)
}
51 changes: 19 additions & 32 deletions service/app/src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,24 @@
"use client"
import { Suspense } from "react"

import { useRouter } from "next/navigation"
import { useEffect } from "react"

import { MSW_MOCK_CODE } from "@/mocks/handlers/auth"

export default function KakaoLoginPage() {
const router = useRouter()

const kakaoClientId = process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID
const redirectUri = process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI

useEffect(() => {
if (process.env.NODE_ENV === "development") {
router.replace(`/login/kakao/?code=${MSW_MOCK_CODE}`)
return
}

if (!kakaoClientId || !redirectUri) return

const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${kakaoClientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code`

router.replace(kakaoAuthUrl)
}, [router, kakaoClientId, redirectUri])
import { KakaoLoginClient } from "./kakaoLoginClient"

export default function KakaoLoginPage({
searchParams,
}: {
searchParams: { returnTo?: string }
}) {
return (
<div className="flex min-h-screen items-center justify-center bg-background-primary">
<div className="flex flex-col items-center gap-16">
<div className="size-32 animate-spin rounded-full border-4 border-blue-400 border-t-transparent"></div>
<p className="text-sm text-text-primary">
카카오 로그인으로 이동 중...
</p>
</div>
</div>
<Suspense
fallback={
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<div className="mb-4">페이지를 준비하고 있습니다</div>
<div className="mx-auto size-8 animate-spin rounded-full border-b-2 border-gray-900"></div>
</div>
</div>
}
>
<KakaoLoginClient returnTo={searchParams.returnTo} />
</Suspense>
)
}
5 changes: 4 additions & 1 deletion service/app/src/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ThemeProvider } from "next-themes"
import { NuqsAdapter } from "nuqs/adapters/next/app"
import { OverlayProvider } from "overlay-kit"

import { AuthProvider } from "@/entities/auth/ui/authProvider"
import { MSWProvider } from "@/mocks/mswProvider"
import { queryClient } from "@/shared/lib/queryClient"

Expand All @@ -23,7 +24,9 @@ export function Providers({ children }: ProvidersProps) {
>
<OverlayProvider>
<MSWProvider>
<NuqsAdapter>{children}</NuqsAdapter>
<AuthProvider>
<NuqsAdapter>{children}</NuqsAdapter>
</AuthProvider>
</MSWProvider>
</OverlayProvider>
</ThemeProvider>
Expand Down
6 changes: 6 additions & 0 deletions service/app/src/entities/auth/api/validateSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { fetchClient } from "@/shared/api/fetchClient"

export const validateSession = async () => {
const response = await fetchClient.get<null>("users/me/games?limit=1") // TODO: 엔드포인트 생성 후 변경
return response.json()
}
11 changes: 4 additions & 7 deletions service/app/src/entities/auth/model/hooks/useAuthGuard.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
"use client"

import { useRouter } from "next/navigation"
import { useEffect } from "react"

import { useAuthStore } from "../../store/useAuthStore"

export const useAuthGuard = () => {
const router = useRouter()
const { isAuthenticated, _hasHydrated } = useAuthStore()
const { authStatus } = useAuthStore()

useEffect(() => {
if (!_hasHydrated) return

if (!isAuthenticated) {
if (authStatus === "unknown") return
if (authStatus !== "authenticated") {
alert("로그인이 필요합니다.")
router.replace("/")
}
}, [isAuthenticated, _hasHydrated, router])
}, [authStatus, router])
Comment on lines 10 to +16
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find and read the useAuthGuard implementation
find . -path "*/entities/auth/model/hooks/useAuthGuard.ts" -type f | head -5

Repository: JECT-Study/JECT-5-FE

Length of output: 123


🏁 Script executed:

# Also check the structure of the auth module
fd -e ts -e tsx . service/app/src/entities/auth --type f | head -20

Repository: JECT-Study/JECT-5-FE

Length of output: 502


🏁 Script executed:

# Read the useAuthGuard implementation
cat -n service/app/src/entities/auth/model/hooks/useAuthGuard.ts

Repository: JECT-Study/JECT-5-FE

Length of output: 615


🏁 Script executed:

# Read the useAuthStore to understand the auth state structure
cat -n service/app/src/entities/auth/store/useAuthStore.ts

Repository: JECT-Study/JECT-5-FE

Length of output: 1814


🏁 Script executed:

# Read the authProvider to see how auth state is initialized
cat -n service/app/src/entities/auth/ui/authProvider.tsx

Repository: JECT-Study/JECT-5-FE

Length of output: 755


🏁 Script executed:

# Find all files that import useAuthGuard
rg -n "useAuthGuard" --type=ts --type=tsx -g '!*.test.*' -g '!*.spec.*'

Repository: JECT-Study/JECT-5-FE

Length of output: 91


🏁 Script executed:

# Also check if there's a protected route layout or wrapper
fd -e tsx -e ts . service/app/src --type f | xargs rg -l "useAuthGuard|authStatus.*unknown" | head -20

Repository: JECT-Study/JECT-5-FE

Length of output: 248


🏁 Script executed:

# Check how useAuthGuard is used in dashboard and create pages
rg -n "useAuthGuard|authStatus" service/app/src/app/dashboard/page.tsx -A 10 -B 5

Repository: JECT-Study/JECT-5-FE

Length of output: 564


🏁 Script executed:

# Check the create page as well
rg -n "useAuthGuard|authStatus" service/app/src/app/create/page.tsx -A 10 -B 5

Repository: JECT-Study/JECT-5-FE

Length of output: 877


🏁 Script executed:

# Let's read the full dashboard page to understand the structure
cat -n service/app/src/app/dashboard/page.tsx

Repository: JECT-Study/JECT-5-FE

Length of output: 641


🏁 Script executed:

# Check the root layout to see if there's a higher-level auth guard
cat -n service/app/src/app/layout.tsx

Repository: JECT-Study/JECT-5-FE

Length of output: 1862


🏁 Script executed:

# Check if there's any middleware or page-level protection
find service/app/src/app -name "layout.tsx" -o -name "middleware.ts" | head -10

Repository: JECT-Study/JECT-5-FE

Length of output: 264


🏁 Script executed:

# Also check what's in DashboardGameSection to see what's being rendered
wc -l service/app/src/app/dashboard/components/dashboardGameSection.tsx

Repository: JECT-Study/JECT-5-FE

Length of output: 133


🏁 Script executed:

# Check the Providers component to see how auth is initialized
cat -n service/app/src/app/providers.tsx

Repository: JECT-Study/JECT-5-FE

Length of output: 1279


🏁 Script executed:

# Check if there's a middleware file for auth
find . -name "middleware.ts" -o -name "middleware.js" | head -5

Repository: JECT-Study/JECT-5-FE

Length of output: 95


🏁 Script executed:

# Let's examine the DashboardGameSection to understand what content gets rendered
head -50 service/app/src/app/dashboard/components/dashboardGameSection.tsx

Repository: JECT-Study/JECT-5-FE

Length of output: 1609


🏁 Script executed:

# Check the middleware implementation
cat -n service/app/src/middleware.ts

Repository: JECT-Study/JECT-5-FE

Length of output: 1062


🏁 Script executed:

# Check if create and dashboard pages are in protected route groups
ls -la service/app/src/app/ | grep -E "dashboard|create"

Repository: JECT-Study/JECT-5-FE

Length of output: 180


🏁 Script executed:

# Let's understand the page hierarchy
find service/app/src/app -type d -name "dashboard" -o -type d -name "create" | head -10

Repository: JECT-Study/JECT-5-FE

Length of output: 120


🏁 Script executed:

# Check what useInfiniteMyGames does and if it makes API calls
cat -n service/app/src/entities/game/model/useInfiniteMyGames.ts

Repository: JECT-Study/JECT-5-FE

Length of output: 3764


🏁 Script executed:

# Also check if there are any error boundaries or loading states
grep -n "isLoading\|isFetching\|error" service/app/src/app/dashboard/components/dashboardGameSection.tsx | head -20

Repository: JECT-Study/JECT-5-FE

Length of output: 406


🏁 Script executed:

# Check the entire DashboardGameSection to see all rendering logic
cat -n service/app/src/app/dashboard/components/dashboardGameSection.tsx

Repository: JECT-Study/JECT-5-FE

Length of output: 4390


🏁 Script executed:

# Check if DashboardNavigation or other components handle auth state
cat -n service/app/src/widgets/DashboardNavigation.tsx 2>/dev/null | head -50

Repository: JECT-Study/JECT-5-FE

Length of output: 920


🏁 Script executed:

# Let's check if there's any auth state handling in the page layouts
cat -n service/app/src/app/dashboard/layout.tsx 2>/dev/null || echo "No layout file"

Repository: JECT-Study/JECT-5-FE

Length of output: 78


🏁 Script executed:

# Check the create layout as well
cat -n service/app/src/app/create/layout.tsx 2>/dev/null || echo "No layout file"

Repository: JECT-Study/JECT-5-FE

Length of output: 607


🏁 Script executed:

# Let's verify the actual bootstrap timing by checking when AuthProvider's useEffect runs
grep -n "bootstrap\|AuthProvider\|hasBootstrapped" service/app/src/entities/auth -r --type ts --type tsx | head -20

Repository: JECT-Study/JECT-5-FE

Length of output: 181


🏁 Script executed:

# Let's check the bootstrap and auth handling with rg
rg -n "bootstrap|hasBootstrapped" service/app/src/entities/auth | head -20

Repository: JECT-Study/JECT-5-FE

Length of output: 830


🏁 Script executed:

# Now let's trace the flow: When does bootstrap complete relative to page render?
# Check if there's a suspense boundary or loading state in the app
find service/app/src/app -name "loading.tsx" | head -10

Repository: JECT-Study/JECT-5-FE

Length of output: 102


🏁 Script executed:

# Let's also check if there are any error boundaries or other protections
rg -n "ErrorBoundary|Suspense" service/app/src/app/dashboard -A 3

Repository: JECT-Study/JECT-5-FE

Length of output: 46


🏁 Script executed:

# Check the getMyGames API implementation to see error handling
cat -n service/app/src/entities/game/api/index.ts 2>/dev/null || find . -name "*.ts" -path "*game/api*" | xargs grep -l "getMyGames" | head -3

Repository: JECT-Study/JECT-5-FE

Length of output: 599


🏁 Script executed:

# Find and check the getMyGames implementation
rg -n "export.*getMyGames|function.*getMyGames" --type ts | head -5

Repository: JECT-Study/JECT-5-FE

Length of output: 263


🏁 Script executed:

# Let's also check what happens when API calls fail (401 responses)
rg -n "fetchClient|401\|Unauthorized" service/app/src/shared -A 3 | head -30

Repository: JECT-Study/JECT-5-FE

Length of output: 366


🏁 Script executed:

# Check the getMyGames implementation
cat -n service/app/src/entities/game/api/getMyGames.ts

Repository: JECT-Study/JECT-5-FE

Length of output: 683


🏁 Script executed:

# Check the fetchClient implementation for error/auth handling
cat -n service/app/src/shared/api/fetchClient.ts | head -100

Repository: JECT-Study/JECT-5-FE

Length of output: 2802


🏁 Script executed:

# Let's check if bootstrap completes quickly or if there's real timing
cat -n service/app/src/entities/auth/api/validateSession.ts

Repository: JECT-Study/JECT-5-FE

Length of output: 329


🏁 Script executed:

# Check if there are query dependencies that might prevent initial API calls
rg -n "enabled" service/app/src/entities/game/model/useInfiniteMyGames.ts

Repository: JECT-Study/JECT-5-FE

Length of output: 123


🏁 Script executed:

# Let's create a simple timeline test - check if getMyGames call is immediate
rg -n "useInfiniteMyGames\|enabled" service/app/src/app/dashboard/components/dashboardGameSection.tsx

Repository: JECT-Study/JECT-5-FE

Length of output: 46


🏁 Script executed:

# Let me verify one more detail - does the page structure itself contain sensitive info
head -20 service/app/src/app/dashboard/components/dashboardGameSection.tsx

Repository: JECT-Study/JECT-5-FE

Length of output: 867


🏁 Script executed:

# Check what gets rendered in the initial skeleton/loading state
cat -n service/app/src/entities/game/ui/gameLibrarySkeleton.tsx 2>/dev/null || echo "Not found"

Repository: JECT-Study/JECT-5-FE

Length of output: 774


🏁 Script executed:

# Final check - confirm that DashboardNavigation doesn't expose sensitive user data
grep -n "user\|profile\|email\|nickname" service/app/src/widgets/DashboardNavigation.tsx

Repository: JECT-Study/JECT-5-FE

Length of output: 46


보호된 페이지가 인증 완료 전에 렌더링되지만, 실제 민감한 데이터는 API 레벨에서 보호됩니다.

authStatus === "unknown" 상태에서 조기 반환하면 부트스트랩이 진행되는 동안 페이지 구조가 렌더링됩니다. 그러나 사용자의 게임 데이터는 useInfiniteMyGames의 API 호출이 필요하며, 세션이 유효하지 않으면 401 응답이 반환되어 auth:session-expired 이벤트가 발생하고 리디렉션이 수행됩니다. 로딩 상태에서는 스켈레톤 UI만 표시되므로 실제 민감한 데이터가 노출되지는 않습니다.

authStatus === "unknown" 상태 동안 로딩 UI를 표시하는 것이 더 나은 UX 패턴입니다:

const ProtectedPage = () => {
  const { authStatus } = useAuthStore()
  useAuthGuard()

  if (authStatus === "unknown") {
    return <LoadingSpinner />
  }

  return <ProtectedContent />
}
🤖 Prompt for AI Agents
In service/app/src/entities/auth/model/hooks/useAuthGuard.ts around lines 10–16,
update the guard so it only triggers redirect when the auth status is explicitly
unauthenticated (e.g., "unauthenticated") and do not show a blocking alert;
leave the "unknown" state untouched so pages can render their loading/skeleton
UI—adjust the effect condition to return early for "unknown", remove the alert
call, and call router.replace("/") only when authStatus === "unauthenticated" so
components can show a LoadingSpinner when authStatus === "unknown".

}
55 changes: 35 additions & 20 deletions service/app/src/entities/auth/store/useAuthStore.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,68 @@
import { create } from "zustand"
import { createJSONStorage, persist } from "zustand/middleware"
import { immer } from "zustand/middleware/immer"

import { kakaoLogin } from "../api/kakaoLogin"
import { logout } from "../api/logout"
import { KakaoLoginData } from "../model/auth"
import { deleteCookie, getSessionId } from "../utils/cookieUtils"
import { validateSession } from "../api/validateSession"

type AuthStatus = "unknown" | "authenticated" | "unauthenticated"
type AuthUser = { nickname: string; profileImageUrl: string }

type AuthState = {
user: KakaoLoginData | null
isAuthenticated: boolean
_hasHydrated: boolean
authStatus: AuthStatus
user: AuthUser | null
hasBootstrapped: boolean
login: (code: string) => Promise<void>
logout: () => void
logout: () => Promise<void>
bootstrap: () => Promise<void>
setUnauthenticated: () => void
}

export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
immer((set, get) => ({
authStatus: "unknown",
user: null,
isAuthenticated: !!getSessionId(),
_hasHydrated: false,
hasBootstrapped: false,

bootstrap: async () => {
if (get().hasBootstrapped) return
try {
await validateSession()
set({ authStatus: "authenticated" })
} catch (error) {
set({ authStatus: "unauthenticated" })
}
Comment on lines +34 to +36
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

주요: 세션 무효 시 사용자 데이터 미삭제

bootstrap() 실패 시(catch 블록) authStatus"unauthenticated"로 설정하고 user 데이터는 정리하지 않습니다. 이로 인해 세션이 만료되었는데도 sessionStorage에 저장된 이전 사용자 데이터가 남아 있는 불일치 상태가 발생할 수 있습니다.

🔎 제안하는 수정
      } catch (error) {
-       set({ authStatus: "unauthenticated" })
+       set({ authStatus: "unauthenticated", user: null })
      }
🤖 Prompt for AI Agents
In service/app/src/entities/auth/store/useAuthStore.ts around lines 34 to 36,
the catch block only sets authStatus to "unauthenticated" but leaves the user
data and any sessionStorage/session cookies intact; update the catch to also
clear the in-memory user state (e.g., set user to null/undefined) and remove any
persisted auth/user keys from sessionStorage (and clear tokens/cookies if
applicable) so the store and storage stay consistent when bootstrap() fails.

set({ hasBootstrapped: true })
},

login: async (code: string) => {
try {
const res = await kakaoLogin(code)
set({ user: res.data, isAuthenticated: true })
const { profileImageUrl, nickname } = (await kakaoLogin(code)).data
set({
user: { profileImageUrl, nickname },
authStatus: "authenticated",
})
} catch (error) {
throw new Error("login failed")
}
},

logout: async () => {
await logout()
deleteCookie("JSESSIONID")
set({ user: null, isAuthenticated: false })
set({ authStatus: "unauthenticated", user: null })
},
}),

setUnauthenticated: () =>
set({ authStatus: "unauthenticated", user: null }),
})),
{
name: "auth-store",
storage: createJSONStorage(() => sessionStorage),
partialize: (state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
}),
onRehydrateStorage: () => (state) => {
if (state) {
state._hasHydrated = true
}
},
},
),
)
21 changes: 21 additions & 0 deletions service/app/src/entities/auth/ui/authProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client"

import { useEffect } from "react"

import { useAuthStore } from "../store/useAuthStore"

export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const { bootstrap, setUnauthenticated } = useAuthStore()
useEffect(() => {
bootstrap()
}, [bootstrap])

useEffect(() => {
window.addEventListener("auth:session-expired", setUnauthenticated)
return () => {
window.removeEventListener("auth:session-expired", setUnauthenticated)
}
}, [setUnauthenticated])

return <>{children}</>
}
Loading
Loading