Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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 = () => {
if (unReadCount === 0) return;
try {
mutateAsync();
} catch {
alert('요청 처리에 실패했습니다.');
}
};
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

비동기 mutation을 await 하지 않아 에러 처리가 작동하지 않습니다.

mutateAsync()는 Promise를 반환하지만 await 없이 호출되고 있습니다. 이로 인해 mutation이 실패해도 catch 블록이 실행되지 않습니다.

🔎 수정 제안
- const handleReadAllClick = () => {
+ const handleReadAllClick = async () => {
    if (unReadCount === 0) return;
    try {
-     mutateAsync();
+     await mutateAsync();
    } catch {
      alert('요청 처리에 실패했습니다.');
    }
  };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleReadAllClick = () => {
if (unReadCount === 0) return;
try {
mutateAsync();
} catch {
alert('요청 처리에 실패했습니다.');
}
};
const handleReadAllClick = async () => {
if (unReadCount === 0) return;
try {
await mutateAsync();
} catch {
alert('요청 처리에 실패했습니다.');
}
};
🤖 Prompt for AI Agents
In src/components/pages/notification/notification-header/index.tsx around lines
19 to 26, the handler calls mutateAsync() without awaiting it so the try/catch
won't catch rejected promises; make the handler async and await mutateAsync()
(or return the promise and handle .catch) so errors are properly caught, and
keep the existing alert inside the catch block to show failure.


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, queryClient]);
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:

#!/bin/bash
# Find useAuth hook implementation
echo "=== Finding useAuth hook ==="
fd -n "useAuth" src/ -t f

echo -e "\n=== Checking auth provider/hook implementations ==="
find src/ -type f -name "*auth*" | head -20

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 559


🏁 Script executed:

#!/bin/bash
echo "=== Searching for useAuth hook definition ==="
rg -n "export.*useAuth|const useAuth" src/ -t ts -t tsx

echo -e "\n=== Checking use-notification file for accessToken usage ==="
cat src/hooks/use-notification/use-notification-connect-sse/index.ts | head -100

echo -e "\n=== Searching for accessToken in hooks/auth or providers ==="
find src/hooks -type f -name "*auth*"
find src/providers -type f -name "*auth*"

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 2455


🏁 Script executed:

#!/bin/bash
echo "=== Finding provider-auth implementation ==="
find src/providers -type f | grep -i auth

echo -e "\n=== Checking provider-auth files ==="
ls -la src/providers/

echo -e "\n=== Reading provider-auth implementation ==="
cat src/providers/provider-auth.ts 2>/dev/null || cat src/providers/provider-auth.tsx 2>/dev/null || echo "File not found"

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 751


🏁 Script executed:

#!/bin/bash
echo "=== Reading provider-auth/index.tsx ==="
cat src/providers/provider-auth/index.tsx

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 1258


의존성 배열을 수정하세요: [accessToken.value, queryClient]로 변경이 필요합니다.

accessToken 객체는 AuthProvider가 렌더링될 때마다 새로 생성되므로 참조가 매번 변경됩니다. 현재 [accessToken, queryClient] 의존성은 토큰 값이 변경되지 않아도 매 렌더링마다 effect를 재실행하게 되어, SSE 연결이 불필요하게 계속 재연결됩니다. 대신 [accessToken.value, queryClient]를 사용하면 실제 토큰 값이 변경될 때만 effect가 재실행됩니다.

🤖 Prompt for AI Agents
In src/hooks/use-notification/use-notification-connect-sse/index.ts around line
75, the effect dependency currently uses the accessToken object which is
recreated on every render; change the dependency to accessToken.value (i.e., use
[accessToken.value, queryClient]) so the effect only re-runs when the actual
token string changes; update the dependency array accordingly and ensure any
linting references to accessToken are adjusted to use its value.


return { receivedNewNotification };
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { GetNotificationListQueryParams } from '@/types/service/notification';

export const useGetNotificationsInfinite = (params?: GetNotificationListQueryParams) => {
return useInfiniteQuery({
queryKey: notificationKeys.list(params),
queryKey: notificationKeys.list(),
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Find GetNotificationListQueryParams definition
echo "=== GetNotificationListQueryParams type definition ==="
rg -n "GetNotificationListQueryParams" --type=ts --type=tsx -B 2 -A 8

echo -e "\n=== useGetNotificationsInfinite usage ==="
rg -n "useGetNotificationsInfinite" --type=ts --type=tsx -B 2 -A 5

echo -e "\n=== notificationKeys.list usage ==="
rg -n "notificationKeys\.list" --type=ts --type=tsx -B 1 -A 1

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 306


🏁 Script executed:

#!/bin/bash

# Find GetNotificationListQueryParams definition
echo "=== GetNotificationListQueryParams type definition ==="
rg -n "GetNotificationListQueryParams" -B 2 -A 8

echo -e "\n=== useGetNotificationsInfinite usage ==="
rg -n "useGetNotificationsInfinite" -B 2 -A 5

echo -e "\n=== notificationKeys structure ==="
rg -n "notificationKeys" -B 1 -A 1 | head -50

echo -e "\n=== Checking the hook file context ==="
cat -n src/hooks/use-notification/use-notification-get-list/index.ts

Repository: WeGo-Together/WeGo_FrontEnd

Length of output: 11371


queryKey에 params의 size 파라미터를 포함하세요.

현재 useGetNotificationsInfinitesize 파라미터를 받지만 queryKey에 포함하지 않습니다.

  • useGetNotificationsInfinite({ size: 20 })useGetNotificationsInfinite({ size: 50 })을 순차적으로 호출하면 둘 다 동일한 캐시 키 ['notifications', 'list']를 사용합니다.
  • 결과적으로 size: 50으로 호출했을 때 size: 20의 캐시된 데이터가 반환되어 의도하지 않은 데이터가 표시됩니다.

notificationKeys.list()notificationKeys.list(params?.size)로 변경하거나, 더 포괄적으로는 params를 queryKey에 포함하는 것을 권장합니다.

🤖 Prompt for AI Agents
In src/hooks/use-notification/use-notification-get-list/index.ts around line 9,
the queryKey uses notificationKeys.list() but omits the params.size, causing
different size requests to share the same cache key; update the queryKey to
include the size (e.g., notificationKeys.list(params?.size)) or include the
whole params object in the key so calls with different size values produce
distinct cache entries.

queryFn: ({ pageParam }) => API.notificationService.getList({ ...params, cursor: pageParam }),
initialPageParam: params?.cursor,
getNextPageParam: (lastPage) => lastPage.nextCursor,
Expand Down
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
8 changes: 1 addition & 7 deletions src/lib/query-key/query-key-notification/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import { GetNotificationListQueryParams } from '@/types/service/notification';

export const notificationKeys = {
all: ['notifications'] as const,
list: (params?: GetNotificationListQueryParams) => [
'notifications',
'list',
...(params ? [params] : []),
],
list: () => ['notifications', 'list'],
unReadCount: () => [...notificationKeys.all, 'unread-count'],
};
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;
}