-
Notifications
You must be signed in to change notification settings - Fork 0
인증 로직 수정 #257
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
인증 로직 수정 #257
Changes from all commits
a05217c
7e15ba1
fce6808
0c61e01
a1d6f84
1f289d1
bfb1989
0d203ea
088da57
ab55e25
57a5753
bd5c79f
b0b9a2c
14bc8ef
d41510e
f9aaa86
9f3e643
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| 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" | ||
| 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> | ||
| ) | ||
| } | ||
| 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> | ||
| ) | ||
| } |
| 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() | ||
| } |
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, find and read the useAuthGuard implementation
find . -path "*/entities/auth/model/hooks/useAuthGuard.ts" -type f | head -5Repository: 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 -20Repository: 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.tsRepository: 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.tsRepository: 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.tsxRepository: 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 -20Repository: 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 5Repository: 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 5Repository: 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.tsxRepository: 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.tsxRepository: 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 -10Repository: 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.tsxRepository: 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.tsxRepository: 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 -5Repository: 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.tsxRepository: JECT-Study/JECT-5-FE Length of output: 1609 🏁 Script executed: # Check the middleware implementation
cat -n service/app/src/middleware.tsRepository: 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 -10Repository: 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.tsRepository: 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 -20Repository: 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.tsxRepository: 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 -50Repository: 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 -20Repository: 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 -20Repository: 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 -10Repository: 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 3Repository: 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 -3Repository: 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 -5Repository: 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 -30Repository: JECT-Study/JECT-5-FE Length of output: 366 🏁 Script executed: # Check the getMyGames implementation
cat -n service/app/src/entities/game/api/getMyGames.tsRepository: 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 -100Repository: 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.tsRepository: 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.tsRepository: 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.tsxRepository: 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.tsxRepository: 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.tsxRepository: JECT-Study/JECT-5-FE Length of output: 46 보호된 페이지가 인증 완료 전에 렌더링되지만, 실제 민감한 데이터는 API 레벨에서 보호됩니다.
const ProtectedPage = () => {
const { authStatus } = useAuthStore()
useAuthGuard()
if (authStatus === "unknown") {
return <LoadingSpinner />
}
return <ProtectedContent />
}🤖 Prompt for AI Agents |
||
| } | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 주요: 세션 무효 시 사용자 데이터 미삭제
🔎 제안하는 수정 } catch (error) {
- set({ authStatus: "unauthenticated" })
+ set({ authStatus: "unauthenticated", user: null })
}🤖 Prompt for AI Agents |
||
| 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 | ||
| } | ||
| }, | ||
| }, | ||
| ), | ||
| ) | ||
| 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}</> | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
프로덕션 코드에서 mock 모듈 import는 잠재적 위험이 있습니다.
MSW_MOCK_CODE는 개발 환경에서만 사용되지만, import는 항상 실행됩니다. 프로덕션 빌드에서 mock 모듈이 제외되면 런타임 오류가 발생할 수 있습니다.🔎 조건부 import 또는 인라인 상수 사용 제안
또는 dynamic import를 사용할 수 있습니다:
🤖 Prompt for AI Agents