diff --git a/src/pages/auth/AuthSuccessPage.tsx b/src/pages/auth/AuthSuccessPage.tsx index b5d8a3c1..d1339db6 100644 --- a/src/pages/auth/AuthSuccessPage.tsx +++ b/src/pages/auth/AuthSuccessPage.tsx @@ -2,7 +2,8 @@ import { useEffect, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { userApi } from '@/shared/apis/user'; -import { useAuth } from '@/shared/context/AuthContext'; +import { LOGIN_METHOD_STORAGE_KEY, LOGIN_PROVIDER_HINT_KEY } from '@/shared/constants/authConstants'; +import { type LoginMethod, useAuth } from '@/shared/context/AuthContext'; import { tokenUtils } from '@/shared/utils/auth'; const AuthSuccessPage = () => { @@ -12,10 +13,35 @@ const AuthSuccessPage = () => { const [errorMessage, setErrorMessage] = useState(''); useEffect(() => { + const toLoginMethod = (value: string | null | undefined): LoginMethod | null => { + if (value === 'email' || value === 'kakao' || value === 'google' || value === 'unknown') { + return value; + } + return null; + }; + + const determineLoginMethod = (): LoginMethod => { + const providerFromParam = toLoginMethod(searchParams.get('provider')); + + const providerHintRaw = sessionStorage.getItem(LOGIN_PROVIDER_HINT_KEY); + const providerFromHint = toLoginMethod(providerHintRaw); + if (providerHintRaw) { + sessionStorage.removeItem(LOGIN_PROVIDER_HINT_KEY); + } + + const storedMethod = toLoginMethod(localStorage.getItem(LOGIN_METHOD_STORAGE_KEY)); + + const resolvedMethod = providerFromParam ?? providerFromHint ?? storedMethod ?? 'unknown'; + console.log('๐Ÿงญ [AUTH SUCCESS] ๋กœ๊ทธ์ธ ๋ฐฉ์‹ ํŒ๋ณ„:', resolvedMethod); + return resolvedMethod; + }; + const handleAuthSuccess = async () => { console.log('๐Ÿ”„ [AUTH SUCCESS] OAuth ์ฝœ๋ฐฑ ์ฒ˜๋ฆฌ ์‹œ์ž‘'); console.log('๐Ÿ“ [AUTH SUCCESS] URL ํŒŒ๋ผ๋ฏธํ„ฐ:', Object.fromEntries(searchParams)); + const loginMethod = determineLoginMethod(); + // URL ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ์—๋Ÿฌ ํ™•์ธ const error = searchParams.get('error'); if (error) { @@ -71,14 +97,12 @@ const AuthSuccessPage = () => { // ํ† ํฐ์ด ์žˆ์œผ๋ฉด login ํ•จ์ˆ˜ ํ˜ธ์ถœ, ์—†์œผ๋ฉด ์‚ฌ์šฉ์ž ์ •๋ณด๋งŒ์œผ๋กœ๋„ ์ฒ˜๋ฆฌ if (accessToken || currentToken) { - login(accessToken || currentToken || '', userData); + login(accessToken || currentToken || '', userData, loginMethod); console.log('โœ… [AUTH SUCCESS] ๋กœ๊ทธ์ธ ์™„๋ฃŒ (ํ† ํฐ ์ €์žฅ)'); } else { // HttpOnly ์ฟ ํ‚ค ๋ฐฉ์‹์ธ ๊ฒฝ์šฐ, ํ† ํฐ ์—†์ด ์‚ฌ์šฉ์ž ์ •๋ณด๋งŒ ์ €์žฅ - // ์ด ๊ฒฝ์šฐ AuthContext๋ฅผ ์ˆ˜์ •ํ•ด์•ผ ํ•  ์ˆ˜๋„ ์žˆ์Œ console.log('โœ… [AUTH SUCCESS] ๋กœ๊ทธ์ธ ์™„๋ฃŒ (HttpOnly ์ฟ ํ‚ค ๋ฐฉ์‹)'); - // ์ž„์‹œ๋กœ ๋นˆ ํ† ํฐ์œผ๋กœ ์ฒ˜๋ฆฌ (๋‚˜์ค‘์— AuthContext ์ˆ˜์ • ํ•„์š”) - login('http-only-cookie', userData); + login('http-only-cookie', userData, loginMethod); } console.log('๐Ÿ  [AUTH SUCCESS] ํ™ˆ์œผ๋กœ ์ด๋™'); @@ -97,7 +121,7 @@ const AuthSuccessPage = () => { nickname: userInfo.nickname, email: userInfo.email, }; - login(existingToken, userData); + login(existingToken, userData, loginMethod); void navigate('/', { replace: true }); return; } catch (retryError) { diff --git a/src/pages/settings/components/SettingsDropdown/SettingsDropdown.tsx b/src/pages/settings/components/SettingsDropdown/SettingsDropdown.tsx index f1db9e75..6aeaa8a5 100644 --- a/src/pages/settings/components/SettingsDropdown/SettingsDropdown.tsx +++ b/src/pages/settings/components/SettingsDropdown/SettingsDropdown.tsx @@ -17,7 +17,7 @@ interface SettingsDropdownProps { const SettingsDropdown = ({ open, setOpen, onShowConsentModal }: SettingsDropdownProps) => { const dropdownRef = useRef(null!); const navigate = useNavigate(); - const { logout: authLogout } = useAuth(); + const { logout: authLogout, loginMethod } = useAuth(); const logoutMutation = useLogout(); useOutsideClick(dropdownRef, () => setOpen(false)); @@ -26,20 +26,16 @@ const SettingsDropdown = ({ open, setOpen, onShowConsentModal }: SettingsDropdow console.log('๐Ÿšช [SETTINGS] ๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผ ํด๋ฆญ'); try { - // ์„œ๋ฒ„ ๋กœ๊ทธ์•„์›ƒ API ํ˜ธ์ถœ (refresh token ๋ฌดํšจํ™”) console.log('๐Ÿ“ก [SETTINGS] ์„œ๋ฒ„ ๋กœ๊ทธ์•„์›ƒ API ํ˜ธ์ถœ ์‹œ์ž‘'); await logoutMutation.mutateAsync(); console.log('โœ… [SETTINGS] ์„œ๋ฒ„ ๋กœ๊ทธ์•„์›ƒ API ์„ฑ๊ณต'); - // ๋กœ์ปฌ ์ƒํƒœ ์ •๋ฆฌ (ํ† ํฐ ์ œ๊ฑฐ, ์‚ฌ์šฉ์ž ์ •๋ณด ์‚ญ์ œ) console.log('๐Ÿงน [SETTINGS] ๋กœ์ปฌ ์ƒํƒœ ์ •๋ฆฌ ์‹œ์ž‘'); authLogout(); - // ํ™ˆํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ console.log('๐Ÿ  [SETTINGS] ํ™ˆํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ'); void navigate('/'); } catch (error) { - // ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ๋กœ์ปฌ ์ƒํƒœ๋Š” ์ •๋ฆฌ console.error('โŒ [SETTINGS] ๋กœ๊ทธ์•„์›ƒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error); console.log('๐Ÿงน [SETTINGS] ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ์—๋„ ๋กœ์ปฌ ์ƒํƒœ ์ •๋ฆฌ'); authLogout(); @@ -47,8 +43,7 @@ const SettingsDropdown = ({ open, setOpen, onShowConsentModal }: SettingsDropdow } }; - // settingsData๋ฅผ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ ์ƒ์„ฑํ•˜์—ฌ handleLogout ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌ - const settingsData = getSettingsData(navigate, handleLogout); + const settingsData = getSettingsData({ navigate, handleLogout, loginMethod }); if (!open) return null; @@ -59,24 +54,30 @@ const SettingsDropdown = ({ open, setOpen, onShowConsentModal }: SettingsDropdow {settingsData.map((section, i) => (

{section.category}

-
    +
      {section.items.map((item) => (
    • - + {item.kind === 'info' ? ( +

      + {item.label} +

      + ) : ( + + )}
    • ))}
    diff --git a/src/pages/settings/components/SettingsDropdown/settingsData.ts b/src/pages/settings/components/SettingsDropdown/settingsData.ts index 2d1b7a96..c1e8104b 100644 --- a/src/pages/settings/components/SettingsDropdown/settingsData.ts +++ b/src/pages/settings/components/SettingsDropdown/settingsData.ts @@ -1,28 +1,71 @@ import type { NavigateFunction } from 'react-router-dom'; -export const getSettingsData = (navigate: NavigateFunction, handleLogout?: () => Promise) => [ - { - category: '๊ณ„์ •', - items: [ - { label: '๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ', path: '/settings/password' }, - { label: '์ด๋ฉ”์ผ ๋ณ€๊ฒฝ', path: '/settings/email' }, - ], - }, - { +import { LOGIN_METHOD_STORAGE_KEY } from '@/shared/constants/authConstants'; +import type { LoginMethod } from '@/shared/context/AuthContext'; + +type SettingsItemKind = 'button' | 'info'; + +export interface SettingsItem { + label: string; + path?: string; + onClick?: () => void; + kind?: SettingsItemKind; +} + +export interface SettingsSection { + category: string; + items: SettingsItem[]; +} + +interface GetSettingsDataParams { + navigate: NavigateFunction; + loginMethod?: LoginMethod | null; + handleLogout?: () => Promise; +} + +const isSocialLogin = (method?: LoginMethod | null) => method === 'kakao' || method === 'google'; + +export const getSettingsData = ({ navigate, loginMethod, handleLogout }: GetSettingsDataParams): SettingsSection[] => { + const sections: SettingsSection[] = []; + + if (!isSocialLogin(loginMethod)) { + sections.push({ + category: '๊ณ„์ •', + items: [ + { label: '๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ', path: '/settings/password' }, + { label: '์ด๋ฉ”์ผ ๋ณ€๊ฒฝ', path: '/settings/email' }, + ], + }); + } else { + const providerLabel = loginMethod === 'kakao' ? '์นด์นด์˜ค' : loginMethod === 'google' ? 'Google' : '์†Œ์…œ ๋กœ๊ทธ์ธ'; + sections.push({ + category: '๊ณ„์ •', + items: [{ label: `${providerLabel} ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ ์ค‘์ด์—์š”.`, kind: 'info' }], + }); + } + + sections.push({ category: '๊ธฐํƒ€', items: [ { label: '์ •๋ณด ๋™์˜ ์„ค์ •' }, { label: 'ํšŒ์› ํƒˆํ‡ด', path: '/settings/delete' }, { label: '๋กœ๊ทธ์•„์›ƒ', - onClick: - handleLogout || - (async () => { - // ํด๋ฐฑ: handleLogout์ด ์ „๋‹ฌ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ - localStorage.removeItem('accessToken'); - void navigate('/'); - }), + onClick: handleLogout + ? () => { + void handleLogout(); + } + : () => { + void (async () => { + // ํด๋ฐฑ: handleLogout์ด ์ „๋‹ฌ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + localStorage.removeItem('accessToken'); + localStorage.removeItem(LOGIN_METHOD_STORAGE_KEY); + void navigate('/'); + })(); + }, }, ], - }, -]; + }); + + return sections; +}; diff --git a/src/shared/components/modal/loginModal/EmailLoginForm.tsx b/src/shared/components/modal/loginModal/EmailLoginForm.tsx index 7552870a..47826468 100644 --- a/src/shared/components/modal/loginModal/EmailLoginForm.tsx +++ b/src/shared/components/modal/loginModal/EmailLoginForm.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { LOGIN_PROVIDER_HINT_KEY } from '@/shared/constants/authConstants'; import { useAuth } from '@/shared/context/AuthContext'; import { useLogin } from '@/shared/hooks/useAuth'; @@ -36,7 +37,8 @@ const EmailLoginForm = ({ onClose }: EmailLoginFormProps) => { if (response.token) { console.log('๐Ÿ” [LOGIN FORM] AuthContext login ํ˜ธ์ถœ'); - login(response.token, response.data); + sessionStorage.removeItem(LOGIN_PROVIDER_HINT_KEY); + login(response.token, response.data, 'email'); } else { console.error('โŒ [LOGIN FORM] Access Token์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'); } diff --git a/src/shared/components/modal/loginModal/LoginModal.tsx b/src/shared/components/modal/loginModal/LoginModal.tsx index 7bcbbc9e..626ccd7f 100644 --- a/src/shared/components/modal/loginModal/LoginModal.tsx +++ b/src/shared/components/modal/loginModal/LoginModal.tsx @@ -4,6 +4,7 @@ import GoogleIcon from '@/assets/GoogleIcon.svg?react'; import XIcon from '@/assets/XIcon.svg?react'; import KakaoIcon from '@/shared/assets/icons/logo-kakao.svg?react'; import SnackIcon from '@/shared/assets/snack.svg?react'; +import { LOGIN_PROVIDER_HINT_KEY } from '@/shared/constants/authConstants'; import { getGoogleAuthUrl } from '@/shared/utils/googleAuth'; import { redirectToKakaoLogin } from '@/shared/utils/kakaoAuth'; @@ -23,10 +24,12 @@ const LoginModal = ({ isOpen, onClose }: ModalProps) => { const [modalMode, setModalMode] = useState('social'); const handleEmailLoginClick = () => { + sessionStorage.removeItem(LOGIN_PROVIDER_HINT_KEY); setModalMode('emailLogin'); }; const handleEmailSignupClick = () => { + sessionStorage.removeItem(LOGIN_PROVIDER_HINT_KEY); setModalMode('emailSignup'); }; @@ -36,11 +39,13 @@ const LoginModal = ({ isOpen, onClose }: ModalProps) => { const handleKakaoLogin = () => { console.log('๐ŸŸก [LOGIN MODAL] Kakao ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ'); + sessionStorage.setItem(LOGIN_PROVIDER_HINT_KEY, 'kakao'); redirectToKakaoLogin(); }; const handleGoogleLogin = () => { console.log('๐Ÿ”ต [LOGIN MODAL] Google ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ํด๋ฆญ'); + sessionStorage.setItem(LOGIN_PROVIDER_HINT_KEY, 'google'); // Google OAuth ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ window.location.href = getGoogleAuthUrl(); }; diff --git a/src/shared/constants/authConstants.ts b/src/shared/constants/authConstants.ts new file mode 100644 index 00000000..1d00a218 --- /dev/null +++ b/src/shared/constants/authConstants.ts @@ -0,0 +1,2 @@ +export const LOGIN_METHOD_STORAGE_KEY = 'loginMethod'; +export const LOGIN_PROVIDER_HINT_KEY = 'loginProviderHint'; diff --git a/src/shared/context/AuthContext.tsx b/src/shared/context/AuthContext.tsx index c1524b7e..d2bad9be 100644 --- a/src/shared/context/AuthContext.tsx +++ b/src/shared/context/AuthContext.tsx @@ -1,6 +1,7 @@ import { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { userApi } from '@/shared/apis/user'; +import { LOGIN_METHOD_STORAGE_KEY } from '@/shared/constants/authConstants'; import { tokenUtils } from '@/shared/utils/auth'; interface User { @@ -9,12 +10,15 @@ interface User { email: string; } +export type LoginMethod = 'email' | 'kakao' | 'google' | 'unknown'; + interface AuthContextType { isAuthenticated: boolean; user: User | null; - login: (token: string, user: User) => void; + login: (token: string, user: User, method: LoginMethod) => void; logout: () => void; loading: boolean; + loginMethod: LoginMethod | null; } const AuthContext = createContext(undefined); @@ -27,6 +31,7 @@ const AuthProvider = ({ children }: AuthProviderProps) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const isInitializing = useRef(false); // ์ค‘๋ณต ์ดˆ๊ธฐํ™” ๋ฐฉ์ง€์šฉ ํ”Œ๋ž˜๊ทธ + const [loginMethod, setLoginMethod] = useState(null); const isAuthenticated = !!user && tokenUtils.hasAccessToken(); @@ -41,6 +46,13 @@ const AuthProvider = ({ children }: AuthProviderProps) => { isInitializing.current = true; // ์ดˆ๊ธฐํ™” ์‹œ์ž‘ ํ‘œ์‹œ console.log('๐Ÿ”„ [AUTH CONTEXT] ์ธ์ฆ ์ƒํƒœ ์ดˆ๊ธฐํ™” ์‹œ์ž‘'); + const savedLoginMethod = localStorage.getItem(LOGIN_METHOD_STORAGE_KEY) as LoginMethod | null; + if (savedLoginMethod) { + setLoginMethod(savedLoginMethod); + } else { + setLoginMethod(null); + } + const token = tokenUtils.getAccessToken(); if (token) { console.log('๐ŸŽซ [AUTH CONTEXT] ์ €์žฅ๋œ ํ† ํฐ ๋ฐœ๊ฒฌ'); @@ -76,10 +88,14 @@ const AuthProvider = ({ children }: AuthProviderProps) => { // ํ† ํฐ์€ ์žˆ์ง€๋งŒ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ console.log('โš ๏ธ [AUTH CONTEXT] ํ† ํฐ์€ ์žˆ์ง€๋งŒ ์‚ฌ์šฉ์ž ์ •๋ณด ์—†์Œ'); tokenUtils.removeAccessToken(); // ์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ ์ œ๊ฑฐ + localStorage.removeItem(LOGIN_METHOD_STORAGE_KEY); + setLoginMethod(null); } } } else { console.log('โŒ [AUTH CONTEXT] ์ €์žฅ๋œ ํ† ํฐ ์—†์Œ'); + localStorage.removeItem(LOGIN_METHOD_STORAGE_KEY); + setLoginMethod(null); } setLoading(false); console.log('โœ… [AUTH CONTEXT] ์ธ์ฆ ์ƒํƒœ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ'); @@ -88,13 +104,15 @@ const AuthProvider = ({ children }: AuthProviderProps) => { void initializeAuth(); }, []); - const login = useCallback((token: string, userData: User) => { + const login = useCallback((token: string, userData: User, method: LoginMethod) => { console.log('๐Ÿ” [AUTH CONTEXT] ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ์‹œ์ž‘:', { userId: userData.userId, email: userData.email }); // Access Token๋งŒ localStorage์— ์ €์žฅ (Refresh Token์€ HttpOnly ์ฟ ํ‚ค๋กœ ์ž๋™ ๊ด€๋ฆฌ) tokenUtils.setAccessToken(token); localStorage.setItem('user', JSON.stringify(userData)); + localStorage.setItem(LOGIN_METHOD_STORAGE_KEY, method); setUser(userData); + setLoginMethod(method); console.log('โœ… [AUTH CONTEXT] ๋กœ๊ทธ์ธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ'); }, []); @@ -105,7 +123,9 @@ const AuthProvider = ({ children }: AuthProviderProps) => { // Access Token๋งŒ ์‚ญ์ œ (Refresh Token์€ ์„œ๋ฒ„์—์„œ ์ฟ ํ‚ค ๋ฌดํšจํ™”) tokenUtils.removeAccessToken(); localStorage.removeItem('user'); + localStorage.removeItem(LOGIN_METHOD_STORAGE_KEY); setUser(null); + setLoginMethod(null); console.log('โœ… [AUTH CONTEXT] ๋กœ์ปฌ ์ƒํƒœ ์ •๋ฆฌ ์™„๋ฃŒ'); // NOTE: logout API ํ˜ธ์ถœ์€ useLogout hook์—์„œ ์ฒ˜๋ฆฌ @@ -118,8 +138,9 @@ const AuthProvider = ({ children }: AuthProviderProps) => { login, logout, loading, + loginMethod, }), - [isAuthenticated, user, login, logout, loading] + [isAuthenticated, user, login, logout, loading, loginMethod] ); return {children};