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: 1 addition & 0 deletions src/api/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ baseAPI.interceptors.response.use(
await API.authService.refresh();
return baseAPI(originalRequest);
} catch (refreshError) {
await API.authService.logout();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find the authService implementation
find . -type f -name "*.ts" -o -name "*.js" | head -20

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 656


🏁 Script executed:

# Look at the context around line 59 in src/api/core/index.ts
cat -n src/api/core/index.ts | sed -n '45,75p'

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 1367


🏁 Script executed:

# Find authService logout implementation
rg "logout\s*\(" --type ts -A 10 | head -50

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 2186


🏁 Script executed:

cat -n src/api/service/auth-service/index.ts

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 1533


logout() 호출이 에러 처리 없이 사용자 리다이렉션을 차단할 수 있습니다.

logout()은 API 요청을 수행하며 예외를 던질 수 있습니다. 현재 코드에서 이 호출이 try-catch로 보호되지 않으면, 예외 발생 시 60-69번 줄의 리다이렉션 로직이 실행되지 않아 사용자가 로그인 페이지로 이동하지 못합니다.

try-catch로 감싸서 안전하게 처리하세요:

try {
  await API.authService.logout();
} catch (error) {
  console.error('[LOGOUT ERROR]', error);
}
// 예외 발생 여부와 관계없이 리다이렉션 진행
🤖 Prompt for AI Agents
In src/api/core/index.ts around line 59, the call to await
API.authService.logout() can throw and prevent the subsequent redirect logic
from running; wrap the logout call in a try-catch, log the caught error (e.g.
with console.error or the project logger) and allow execution to continue so the
redirect logic on lines ~60-69 runs regardless of whether logout failed.

if (isServer) {
const { redirect } = await import('next/navigation');
redirect('/login');
Expand Down
25 changes: 16 additions & 9 deletions src/app/notification/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,33 @@

import { EmptyState } from '@/components/layout/empty-state';
import { NotificationCard, NotificationHeader } from '@/components/pages/notification';
import { useIntersectionObserver } from '@/hooks/use-intersection-observer';
import { useGetNotificationsInfinite } from '@/hooks/use-notification/use-notification-get-list';

export default function NotificationPage() {
const { data: list, fetchNextPage } = useGetNotificationsInfinite({ size: 1 });
const {
data: list,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useGetNotificationsInfinite({ size: 20 });

const fetchObserverRef = useIntersectionObserver({
onIntersect: () => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
},
});

if (!list) return;

return (
<section>
<NotificationHeader />
<div className='flex h-10 flex-row items-center justify-end gap-2'>
<p className='text-mono-white bg-mint-500 flex-center size-4 rounded-full'>v</p>
<p className='text-mono-black text-text-sm mr-3 text-right'>모두 읽음 처리</p>
</div>

{list.length > 0 && list.map((item) => <NotificationCard key={item.id} item={item} />)}
{hasNextPage && <div ref={fetchObserverRef}>다음</div>}
{list.length === 0 && <EmptyState>아직 받은 알림이 없어요.</EmptyState>}
<button className='text-black' onClick={() => fetchNextPage()}>
다음
</button>
</section>
);
}
22 changes: 13 additions & 9 deletions src/components/layout/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,19 @@ export const Header = () => {
receivedNewNotification && 'animate-ring text-mint-500',
)}
/>
<span
className={cn(
'bg-mint-300 absolute top-1 right-1.75 aspect-square size-3.5 rounded-full',
receivedNewNotification && 'animate-ping',
)}
/>
<span className='bg-mint-500 text-mono-white text-text-2xs-semibold flex-center absolute top-1 right-1.75 aspect-square size-3.5 rounded-full'>
{unReadCount}
</span>
{unReadCount > 0 && (
<>
<span
className={cn(
'bg-mint-300 absolute top-1 right-1.75 aspect-square size-3.5 rounded-full',
receivedNewNotification && 'animate-ping',
)}
/>
<span className='bg-mint-500 text-mono-white text-text-2xs-semibold flex-center absolute top-1 right-1.75 aspect-square size-3.5 rounded-full'>
{unReadCount}
</span>
</>
)}
</Link>
</div>
</nav>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const NotificationCard = ({ item }: Props) => {
<article
className={cn(
'bg-mono-white flex cursor-pointer flex-row gap-3 px-5 py-6',
!item.readAt && 'bg-mint-100',
!item.readAt && 'bg-mint-50',
)}
onClick={handleNotificationClick}
>
Expand Down
26 changes: 26 additions & 0 deletions src/components/pages/notification/notification-header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,29 @@
import { useRouter } from 'next/navigation';

import { Icon } from '@/components/icon';
import { useUpdateNotificationReadAll } from '@/hooks/use-notification';
import { cn } from '@/lib/utils';
import { useNotification } from '@/providers';

export const NotificationHeader = () => {
const router = useRouter();

const { unReadCount } = useNotification();
const { mutateAsync } = useUpdateNotificationReadAll();

const handleHistoryBackClick = () => {
router.back();
};

const handleReadAllClick = async () => {
if (unReadCount === 0) return;
try {
await mutateAsync();
} catch {
alert('요청 처리에 실패했습니다.');
}
};

return (
<nav className='bg-mono-white flex-center sticky top-14 z-10 h-12 border-b-1 border-gray-200'>
<button
Expand All @@ -20,6 +35,17 @@ export const NotificationHeader = () => {
<Icon id='chevron-left-2' className='text-gray-500' />
</button>
<h2 className='text-text-md-bold text-gray-800'>알림</h2>
<button
className={cn(
'text-text-sm-semibold absolute right-5',
unReadCount > 0 && 'text-mint-500',
unReadCount === 0 && 'text-gray-500',
)}
disabled={unReadCount === 0}
onClick={handleReadAllClick}
>
모두 읽음
</button>
</nav>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const useConnectSSE = () => {
console.log('SSE 수신 성공:', data);
setReceivedNewNotification(true);
queryClient.invalidateQueries({ queryKey: notificationKeys.unReadCount() });
queryClient.invalidateQueries({ queryKey: notificationKeys.list() });
// TODO: 알림 타입별 처리 추가 예정
} catch (error) {
console.error('SSE 데이터 파싱 실패:', error);
Expand All @@ -62,6 +63,7 @@ export const useConnectSSE = () => {
es.onerror = (_error) => {
console.log('SSE 오류 발생:');
// todo: 재 연결 로직 추가 필요
accessToken.value = null;
};

// SSE Cleanup
Expand All @@ -70,7 +72,7 @@ export const useConnectSSE = () => {
es.close();
eventSourceRef.current = null;
};
}, [accessToken.value, queryClient]);
}, [accessToken, accessToken.value, queryClient]);

return { receivedNewNotification };
};
2 changes: 2 additions & 0 deletions src/lib/formatDateTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export const formatTimeAgo = (isoString: string) => {
const dateInput = new Date(isoString.endsWith('Z') ? isoString : `${isoString}Z`);
const dateNow = new Date();

if (dateInput.getTime() >= dateNow.getTime()) return '0초 전';

const diffPerSec = Math.floor((dateNow.getTime() - dateInput.getTime()) / 1000);
if (diffPerSec < 60) return `${diffPerSec}초 전`;

Expand Down
4 changes: 4 additions & 0 deletions src/styles/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
button {
cursor: pointer;
}

button:disabled {
cursor: not-allowed;
}