Skip to content

feat: 알림 조회 요청 관련 enable 옵션 추가 및 레드닷 로직 추가 - #294

Merged
shinwokkang merged 2 commits into
devfrom
feat/293-header-alert
May 2, 2026
Merged

shinwokkang merged 2 commits into
devfrom
feat/293-header-alert

Conversation

@shinwokkang

@shinwokkang shinwokkang commented May 2, 2026 •

Copy link
Copy Markdown
Contributor

주요 수정 사항
useNotificationPreviewQuery 훅 개선:
비로그인 상태에서 불필요한 알림 조회 요청이 발생하지 않도록 enabled 옵션을 추가했습니다.
Header.tsx 레드닷 로직 추가:
헤더에서 알림 데이터를 가져와 읽지 않은 알림(!notif.read)이 하나라도 존재할 경우 hasUnread 상태를 계산합니다.
hasUnread가 true일 때 알림 아이콘 우측 상단에 bg-Red 색상의 둥근 점을 렌더링하도록 UI를 추가했습니다.
데이터 동기화 및 성능 최적화:
TanStack Query의 캐시를 공유하므로, 알림 드롭다운을 열거나 알림을 읽었을 때(캐시 무효화) 헤더의 빨간 점도 즉각적으로 반응하여 사라지거나 나타납니다.
중복 네트워크 요청 없이 효율적으로 상태를 관리합니다.

Summary by CodeRabbit

  • New Features

    • The notification bell icon now displays a small red unread indicator badge when you have new notifications, making it easier to spot pending alerts at a glance.
  • Improvements

    • Notification data fetching has been enhanced to respect your authentication state, ensuring notifications are appropriately loaded based on whether you are logged in.

@shinwokkang shinwokkang self-assigned this May 2, 2026
@shinwokkang shinwokkang added ✨ feat 새로운 기능 추가 ♻️ refactor 리팩토링 🎨 style 스타일, 포맷, 세미콜론 등 코드 비동작 변경 labels May 2, 2026
@vercel

vercel Bot commented May 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
check-mo Ready Ready Preview, Comment May 2, 2026 3:38pm
checkmo Ready Ready Preview, Comment May 2, 2026 3:38pm

@coderabbitai

coderabbitai Bot commented May 2, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 82072f01-ed4a-48e5-96d1-983f5a89d5c9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/293-header-alert

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements a notification unread indicator in the header and updates the notification preview query to respect the user's authentication status. Review feedback highlighted a potential issue where cached data from a previous session might cause the unread indicator or dropdown to display incorrect information after logout; suggestions were made to explicitly filter the data based on the login state. Furthermore, it was noted that relying on a limited preview of five notifications to determine the unread status may lead to inaccuracies if older unread notifications exist.

Comment thread src/components/layout/Header.tsx Outdated
Comment on lines +46 to +47
const { data: notifications } = useNotificationPreviewQuery(5, isLoggedIn);
const hasUnread = notifications?.some((notif) => !notif.read) ?? false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

로그아웃 시 TanStack Query 캐시에 이전 사용자의 알림 데이터가 남아있을 수 있습니다. isLoggedIn이 false가 되어도 캐시된 데이터로 인해 hasUnread가 true로 계산되어 레드닷이 잘못 표시될 수 있습니다. isLoggedIn 상태를 확인하여 데이터를 필터링하는 것이 안전합니다.

Suggested change
const { data: notifications } = useNotificationPreviewQuery(5, isLoggedIn);
const hasUnread = notifications?.some((notif) => !notif.read) ?? false;
const { data: notificationsData } = useNotificationPreviewQuery(5, isLoggedIn);
const notifications = isLoggedIn ? notificationsData : [];
const hasUnread = notifications?.some((notif) => !notif.read) ?? false;

export default function NotificationDropdown() {
const { data: notifications, isLoading } = useNotificationPreviewQuery(5);
const { isLoggedIn } = useAuthStore();
const { data: notifications, isLoading } = useNotificationPreviewQuery(5, isLoggedIn);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

로그아웃 후에도 TanStack Query 캐시에 이전 사용자의 데이터가 남아있을 수 있어, 비로그인 상태에서 드롭다운을 열었을 때 이전 사용자의 알림이 노출될 위험이 있습니다. isLoggedIn 상태일 때만 데이터를 사용하도록 제한하는 것이 안전합니다.

Suggested change
const { data: notifications, isLoading } = useNotificationPreviewQuery(5, isLoggedIn);
const { data: notificationsData, isLoading } = useNotificationPreviewQuery(5, isLoggedIn);
const notifications = isLoggedIn ? notificationsData : [];

const notificationRef = useRef<HTMLDivElement>(null);

const { data: notifications } = useNotificationPreviewQuery(5, isLoggedIn);
const hasUnread = notifications?.some((notif) => !notif.read) ?? false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

현재 로직은 최신 5개의 알림(size: 5)만 확인하여 읽지 않은 상태를 판단합니다. 만약 읽지 않은 알림이 6번째 이후에 있다면 레드닷이 표시되지 않는 문제가 발생할 수 있습니다. 전체 읽지 않은 알림 여부를 반환하는 별도의 API가 있다면 해당 API를 사용하는 것이 더 정확합니다.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/layout/Header.tsx (1)

136-151: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Static aria-label doesn't reflect unread notification state

The button's aria-label is always "알림" regardless of hasUnread. Screen-reader users get no indication that there are unread notifications. Updating it dynamically is a one-liner:

♿ Proposed fix
  <button
    onClick={() => setIsNotificationOpen(!isNotificationOpen)}
-   aria-label="알림"
+   aria-label={hasUnread ? "알림 (읽지 않은 알림 있음)" : "알림"}
    className="relative w-6 h-6 cursor-pointer"
  >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/Header.tsx` around lines 136 - 151, The notification
button in Header.tsx always uses a static aria-label "알림"; update the button
element to compute a dynamic aria-label based on the hasUnread state (and the
unread count if available) so screen readers announce when there are unread
notifications (e.g., "알림, 새 알림 있음" or "알림, 새 알림 N개"). Keep the existing onClick
handler (setIsNotificationOpen) and visual unread dot, but replace the hardcoded
aria-label with a conditional string that reflects hasUnread (and count) so
assistive tech gets the correct state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/layout/Header.tsx`:
- Around line 46-47: The computed hasUnread currently uses cached notifications
even when the query is disabled, so guard it with the login state: change the
calculation in Header.tsx to only consider notifications when isLoggedIn is true
(e.g. hasUnread = isLoggedIn && (notifications?.some(n => !n.read) ?? false));
update references to hasUnread to rely on this guarded value so the badge hides
immediately on logout; keep useNotificationPreviewQuery as-is but ensure
notifications is only used when isLoggedIn.

---

Outside diff comments:
In `@src/components/layout/Header.tsx`:
- Around line 136-151: The notification button in Header.tsx always uses a
static aria-label "알림"; update the button element to compute a dynamic
aria-label based on the hasUnread state (and the unread count if available) so
screen readers announce when there are unread notifications (e.g., "알림, 새 알림 있음"
or "알림, 새 알림 N개"). Keep the existing onClick handler (setIsNotificationOpen) and
visual unread dot, but replace the hardcoded aria-label with a conditional
string that reflects hasUnread (and count) so assistive tech gets the correct
state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: be4acb3b-29fb-44eb-89e5-3227ff943411

📥 Commits

Reviewing files that changed from the base of the PR and between eee2abb and 0467c45.

📒 Files selected for processing (3)
  • src/components/layout/Header.tsx
  • src/components/layout/NotificationDropdown.tsx
  • src/hooks/queries/useNotificationQueries.ts

Comment thread src/components/layout/Header.tsx Outdated
Comment on lines +46 to +47
const { data: notifications } = useNotificationPreviewQuery(5, isLoggedIn);
const hasUnread = notifications?.some((notif) => !notif.read) ?? false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

hasUnread can remain true after logout due to stale TanStack Query cache

Disabling a query only prevents it from running; it may still return cached data. When the user logs out, isLoggedIn becomes false and enabled is set to false, but the cached notification array is still returned as data. This means hasUnread can be true and the red dot can persist until the cache entry is garbage-collected (default 5 minutes).

Guard hasUnread with isLoggedIn to ensure the badge is hidden immediately on logout:

🐛 Proposed fix
- const hasUnread = notifications?.some((notif) => !notif.read) ?? false;
+ const hasUnread = isLoggedIn && (notifications?.some((notif) => !notif.read) ?? false);
📝 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 { data: notifications } = useNotificationPreviewQuery(5, isLoggedIn);
const hasUnread = notifications?.some((notif) => !notif.read) ?? false;
const { data: notifications } = useNotificationPreviewQuery(5, isLoggedIn);
const hasUnread = isLoggedIn && (notifications?.some((notif) => !notif.read) ?? false);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/Header.tsx` around lines 46 - 47, The computed
hasUnread currently uses cached notifications even when the query is disabled,
so guard it with the login state: change the calculation in Header.tsx to only
consider notifications when isLoggedIn is true (e.g. hasUnread = isLoggedIn &&
(notifications?.some(n => !n.read) ?? false)); update references to hasUnread to
rely on this guarded value so the badge hides immediately on logout; keep
useNotificationPreviewQuery as-is but ensure notifications is only used when
isLoggedIn.

@shinwokkang

Copy link
Copy Markdown
Contributor Author

@coderabbitai TanStack Query 캐시에 이전 사용자의 데이터가 남아있을 수 있는 문제들에 대해서 모두 자세하게 알려주세요.

@coderabbitai

coderabbitai Bot commented May 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and read key files
echo "=== useNotificationQueries.ts ==="
fd "useNotificationQueries" --exec cat {}

echo "=== useAuthStore ==="
fd "useAuthStore" --exec cat {}

echo "=== QueryClient / Provider setup ==="
fd -e ts -e tsx "queryClient\|QueryClient\|ReactQueryProvider\|TanstackProvider\|queryProvider" -i --exec cat {}

echo "=== logout related logic ==="
rg -n "logout\|signOut\|clearCache\|queryClient.clear\|removeQueries\|invalidateQueries" --type=ts --type=tsx -C2

echo "=== Header.tsx ==="
fd "Header.tsx" --exec cat {}

echo "=== NotificationDropdown.tsx ==="
fd "NotificationDropdown.tsx" --exec cat {}

Length of output: 18608


@shinwokkang 코드베이스를 분석한 결과, TanStack Query 캐시에 이전 사용자 데이터가 잔류할 수 있는 문제점들을 아래와 같이 정리했습니다.


🔍 발견된 문제 목록

1. 🚨 [가장 심각] 로그아웃 시 캐시를 초기화하지 않음

useAuthStore의 logout() 함수는 쿠키 제거와 Zustand 상태 리셋만 수행합니다.

// src/store/useAuthStore.ts
logout: () => {
  Cookies.remove("accessToken");
  set({ user: null, isLoggedIn: false, isInitialized: true });
  // ❌ queryClient.clear() 또는 removeQueries() 호출 없음
},

TanStack Query의 기본 gcTime(구 cacheTime)은 5분입니다. 즉, 로그아웃 후 5분 이내에 다른 사용자가 로그인하면, 이전 사용자의 알림 데이터가 캐시에 그대로 남아있어 새 사용자에게 노출될 수 있습니다.

해결책: logout() 시 queryClient.clear() 호출

// useAuthStore.ts 수정 예시
import { getQueryClient } from "@/lib/queryClient"; // QueryClient 싱글톤 참조

logout: () => {
  Cookies.remove("accessToken");
  set({ user: null, isLoggedIn: false, isInitialized: true });
  getQueryClient().clear(); // ✅ 모든 캐시 제거
},

2. 🚨 사용자 식별자가 없는 쿼리 키

현재 모든 알림 관련 쿼리 키에 사용자 ID가 포함되지 않습니다.

// src/hooks/queries/useNotificationQueries.ts
export const notificationKeys = {
  all: ["notifications"] as const,
  settings: () => [...notificationKeys.all, "settings"] as const,
  infiniteList: () => [...notificationKeys.all, "infiniteList"] as const,
  preview: (size?: number) => [...notificationKeys.all, "preview", size] as const,
  // ❌ 예: ["notifications", "preview", 5] → 사용자 구분 없음
};

사용자 A가 로그아웃하고 사용자 B가 로그인하더라도 쿼리 키가 동일하기 때문에, TanStack Query는 기존 캐시를 그대로 재사용합니다. 위 Issue 1이 해결되어도, 이 구조가 유지되는 한 근본적으로 안전하지 않습니다.

해결책: 쿼리 키에 userId 포함

export const notificationKeys = {
  all: (userId: string) => ["notifications", userId] as const,
  preview: (userId: string, size?: number) =>
    [...notificationKeys.all(userId), "preview", size] as const,
  // ...
};

3. ⚠️ useNotificationSettingsQuery, useInfiniteNotificationsQuery에 enabled 가드 없음

이번 PR에서 useNotificationPreviewQuery에는 enabled 옵션이 추가되었지만, 나머지 두 쿼리에는 아직 적용되지 않았습니다.

// ❌ enabled 옵션 없음 → 비로그인 상태에서도 요청 발생
export const useNotificationSettingsQuery = () => {
  return useQuery({
    queryKey: notificationKeys.settings(),
    queryFn: () => notificationService.getSettings(),
    // enabled 없음
  });
};

export const useInfiniteNotificationsQuery = () => {
  return useInfiniteQuery({
    queryKey: notificationKeys.infiniteList(),
    queryFn: ...,
    // enabled 없음
  });
};

4. ⚠️ 로그인 직후 이전 사용자 데이터 순간 노출 (Flash of Wrong Content)

Issue 1, 2가 해결되지 않은 상태에서, 사용자 B가 로그인하면:

  1. TanStack Query가 캐시에서 사용자 A의 데이터를 즉시 렌더링 (stale-while-revalidate 동작)
  2. 백그라운드에서 사용자 B의 데이터를 fetch
  3. 응답이 오기 전까지 사용자 A의 알림 내용이 화면에 표시됨

hasUnread 레드닷도 이전 사용자의 읽지 않은 알림 여부를 기준으로 잘못 표시될 수 있습니다.


✅ 권장 수정 요약

문제 위치 수정 방법
로그아웃 시 캐시 미정리 useAuthStore.ts logout()에 queryClient.clear() 추가
사용자 미식별 쿼리 키 useNotificationQueries.ts 키에 userId 포함
enabled 가드 누락 useNotificationQueries.ts useNotificationSettingsQuery, useInfiniteNotificationsQuery에도 enabled 추가

가장 빠른 해결은 logout() 함수에서 queryClient.clear()를 호출하는 것입니다. queryClient를 Zustand store 외부에서 접근하려면 싱글톤 패턴으로 QueryClient 인스턴스를 관리하거나, 로그아웃 처리 로직을 컴포넌트 레이어로 올려서 useQueryClient() 훅을 활용하는 방법을 고려해 보세요.

@shinwokkang
shinwokkang merged commit ed9cc56 into dev May 2, 2026
4 checks passed
@shinwokkang
shinwokkang deleted the feat/293-header-alert branch May 2, 2026 15:40
@coderabbitai coderabbitai Bot mentioned this pull request May 6, 2026
8 tasks

This branch was successfully deployed

2 active deployments
Preview – check-mo — f4312984 Deployed May 2, 2026 by vercel[bot]
Preview – checkmo — f4312984 Deployed May 2, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat 새로운 기능 추가 ♻️ refactor 리팩토링 🎨 style 스타일, 포맷, 세미콜론 등 코드 비동작 변경

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant