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
1 change: 0 additions & 1 deletion src/api/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ baseAPI.interceptors.response.use(
await API.authService.refresh();
return baseAPI(originalRequest);
} catch (refreshError) {
await API.authService.logout();
if (isServer) {
const { redirect } = await import('next/navigation');
redirect('/login');
Expand Down
6 changes: 1 addition & 5 deletions src/api/service/notification-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ export const notificationServiceRemote = () => ({
},

getUnreadCount: async () => {
try {
return await apiV1.get<number>(`/notifications/unread-count`);
} catch {
return 0;
}
return await apiV1.get<number>(`/notifications/unread-count`);
},
});
2 changes: 1 addition & 1 deletion src/app/(user)/mypage/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const dynamic = 'force-dynamic';
const MyPageLayout = async ({ children }: Props) => {
const queryClient = getQueryClient();

await queryClient.fetchQuery({
await queryClient.prefetchQuery({
queryKey: userKeys.me(),
queryFn: () => API.userService.getMe(),
});
Expand Down
6 changes: 5 additions & 1 deletion src/components/pages/auth/login/login-toast-effect/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { useEffect, useRef } from 'react';

import { Toast } from '@/components/ui';
import { useToast } from '@/components/ui/toast/core';
import { useAuth } from '@/providers';

type Props = {
error?: string | string[];
};

export const LoginToastEffect = ({ error }: Props) => {
const { run } = useToast();
const { setIsAuthenticated } = useAuth();
const lastErrorRef = useRef<string>('');

useEffect(() => {
Expand All @@ -21,8 +23,10 @@ export const LoginToastEffect = ({ error }: Props) => {
if (lastErrorRef.current === normalized) return;
lastErrorRef.current = normalized;

setIsAuthenticated(false);

run(<Toast type='info'>로그인이 필요한 서비스입니다.</Toast>);
}, [error, run]);
}, [error, run, setIsAuthenticated]);

return null;
};
4 changes: 2 additions & 2 deletions src/hooks/use-auth/use-auth-login/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const useLogin = () => {
const [loginError, setLoginError] = useState<string | null>(null);
const clearLoginError = useCallback(() => setLoginError(null), []);

const { accessToken } = useAuth();
const { setIsAuthenticated } = useAuth();

const handleLogin = async (payload: LoginRequest, formApi: { reset: () => void }) => {
setLoginError(null);
Expand All @@ -72,7 +72,7 @@ export const useLogin = () => {

formApi.reset();

accessToken.set(result.accessToken);
setIsAuthenticated(true);

const nextPath = normalizePath(searchParams.get('path'));
router.replace(nextPath);
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/use-auth/use-auth-logout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const useLogout = () => {
const router = useRouter();
const queryClient = useQueryClient();

const { accessToken } = useAuth();
const { setIsAuthenticated } = useAuth();

const handleLogout = async () => {
try {
Expand All @@ -23,7 +23,7 @@ export const useLogout = () => {
// 로그인 유저 관련 캐시 정리
queryClient.removeQueries({ queryKey: userKeys.all });

accessToken.remove();
setIsAuthenticated(false);

router.push('/');
}
Expand Down
24 changes: 13 additions & 11 deletions src/hooks/use-notification/use-notification-connect-sse/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { useEffect, useRef, useState } from 'react';

import { useQueryClient } from '@tanstack/react-query';
import Cookies from 'js-cookie';

import { notificationKeys } from '@/lib/query-key/query-key-notification';
import { useAuth } from '@/providers/provider-auth';

export const useConnectSSE = () => {
const [receivedNewNotification, setReceivedNewNotification] = useState(false);

const { accessToken } = useAuth();
const { isAuthenticated } = useAuth();
const eventSourceRef = useRef<EventSource | null>(null);
const queryClient = useQueryClient();

Expand All @@ -25,54 +26,55 @@ export const useConnectSSE = () => {

// SSE 연결 관련 로직
useEffect(() => {
if (!accessToken.value) return;
if (!isAuthenticated) return;

// 기존 연결이 있으면 정리
if (eventSourceRef.current) {
console.log('SSE 기존 연결 정리');
console.log('[DEBUG] SSE 기존 연결 정리');
eventSourceRef.current.close();
}

const token = Cookies.get('accessToken');

// SSE 연결 시도
const es = new EventSource(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/notifications/subscribe?accessToken=${accessToken.value}`,
`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/notifications/subscribe?accessToken=${token}`,
);

eventSourceRef.current = es;

// SSE 연결 성공 시
es.addEventListener('connect', (event) => {
console.log('SSE 연결 확인:', event.data);
console.log('[DEBUG] SSE 연결 확인:', event.data);
});

// SSE 알림 수신 시
es.addEventListener('notification', (event) => {
try {
const data = JSON.parse(event.data);
console.log('SSE 수신 성공:', data);
console.log('[DEBUG] SSE 수신 성공:', data);
setReceivedNewNotification(true);
queryClient.invalidateQueries({ queryKey: notificationKeys.unReadCount() });
queryClient.invalidateQueries({ queryKey: notificationKeys.list() });
// TODO: 알림 타입별 처리 추가 예정
} catch (error) {
console.error('SSE 데이터 파싱 실패:', error);
console.error('[DEBUG] SSE 데이터 파싱 실패:', error);
}
});

// SSE 연결 실패 시
es.onerror = (_error) => {
console.log('SSE 오류 발생:');
console.log('[DEBUG] SSE 오류 발생:');
// todo: 재 연결 로직 추가 필요
accessToken.value = null;
};

// SSE Cleanup
return () => {
console.log('SSE 연결 정리');
console.log('[DEBUG] SSE 연결 정리');
es.close();
eventSourceRef.current = null;
};
}, [accessToken, accessToken.value, queryClient]);
}, [isAuthenticated, queryClient]);

return { receivedNewNotification };
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import { notificationKeys } from '@/lib/query-key/query-key-notification';
import { useAuth } from '@/providers';

export const useGetNotificationUnreadCount = () => {
const { accessToken } = useAuth();
const { isAuthenticated } = useAuth();
const queryResult = useQuery({
queryKey: notificationKeys.unReadCount(),
queryFn: () => API.notificationService.getUnreadCount(),
enabled: !!accessToken.value,
enabled: isAuthenticated,
throwOnError: false,
retry: false,
});

const finalData = accessToken.value ? (queryResult.data ?? 0) : 0;
const finalData = isAuthenticated ? (queryResult.data ?? 0) : 0;

return {
...queryResult,
Expand Down
47 changes: 26 additions & 21 deletions src/providers/provider-auth/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { createContext, useContext, useEffect, useState } from 'react';
import React, { createContext, SetStateAction, useContext, useEffect, useState } from 'react';

import Cookies from 'js-cookie';

import { API } from '@/api';

interface AuthContextType {
accessToken: {
value: string | null;
set: (token: string) => void;
remove: () => void;
};
isAuthenticated: boolean;
setIsAuthenticated: React.Dispatch<SetStateAction<boolean>>;
}

const AuthContext = createContext<AuthContextType | null>(null);
Expand All @@ -23,24 +22,30 @@ interface Props {
}

export const AuthProvider = ({ children }: Props) => {
const [token, setToken] = useState<string | null>('');

const accessToken = {
value: token,
set: (token: string) => setToken(token),
remove: () => setToken(null),
};
const [isAuthenticated, setIsAuthenticated] = useState(false);

// 일반 cookie를 사용하고 있기 때문에 useEffect에서 직접 Cookie 읽어오는 방식 적용
// httpOnly cookie로 변경하게 되면 로직 수정 필요
// 초기값 설정
// 페이지가 새로고침 될 때 accessToken이 없으면 refresh 시도, state update 실행
useEffect(() => {
const updateToken = () => {
const token = Cookies.get('accessToken');
if (!token) return;
setToken(token);
const updateAuthenticated = async () => {
const accessToken = Cookies.get('accessToken');
if (!accessToken) {
try {
await API.authService.refresh();
setIsAuthenticated(true);
} catch {
setIsAuthenticated(false);
}
} else {
setIsAuthenticated(true);
}
};
updateToken();
updateAuthenticated();
}, []);

return <AuthContext.Provider value={{ accessToken }}>{children}</AuthContext.Provider>;
return (
<AuthContext.Provider value={{ isAuthenticated, setIsAuthenticated }}>
{children}
</AuthContext.Provider>
);
};