Skip to content

feat: 알림센터 알림 유무 확인 기능 구현#277

Merged
heeeeyong merged 4 commits intodevelopfrom
chore/minor-updates
Oct 13, 2025
Merged

feat: 알림센터 알림 유무 확인 기능 구현#277
heeeeyong merged 4 commits intodevelopfrom
chore/minor-updates

Conversation

@heeeeyong
Copy link
Collaborator

@heeeeyong heeeeyong commented Oct 13, 2025

#️⃣연관된 이슈

없음

📝작업 내용

  • 알림센터의 알림유무에 따라, 아이콘에 빨간점 유무로 구분할 수 있는 기능을 추가했습니다.
  • 띱 목록&내 띱 목록의 totalCount가 제대로 반영되지 않는 점을 수정했습니다. 여기에, 뷰포트 기준 아랫쪽 유저 리스트 영역의 배경색이 보이지 않는 현상을 해결했습니다.
  • 해당 페이지에서 뷰포트 사이즈보다 처음 호출한 유저 리스트 목록이 짧은 경우에, 다음으로 호출할 유저 리스트가 있음에도 불구하고 다음 호출이 진행되지 않는 부분을 같이 해결했습니다.

스크린샷

image

image

💬리뷰 요구사항

  1. MainHeader에서 알림유무 확인 API를 별개로 호출하게끔 구현을 했는데 이때 토큰 발급 이전에 해당 API호출이 먼저 발생하는 문제가 생겼습니다. 현재는 토큰의 발급유무를 전역으로 확인하고 토큰 발급이 끝나면 해당 API 호출을 하는 방식으로 구현했는데 이것보다 나은 방법이 있을지 궁금합니다.
  2. 뷰포트 사이즈를 자동계산하여 다음 무한스크롤 호출을 하게끔 해놓았는데, 다른 좋은 방법이 있을지 고민중입니다.

Summary by CodeRabbit

  • 신기능
    • 알림 벨 아이콘에 읽지 않은 알림이 있을 때 시각적 표시가 추가되었습니다.
    • 팔로워/팔로잉 목록에 총 개수가 표시됩니다.
    • 피드 팔로워/팔로잉 화면의 무한 스크롤 경험이 개선되었습니다(조기 프리페치, 트리거 임계값 조정, 초기·추가 로딩 스피너 분리).
    • 소셜 로그인 후 인증 준비 상태 처리가 개선되어 초기 화면 로딩 타이밍이 더 안정적입니다.

@heeeeyong heeeeyong self-assigned this Oct 13, 2025
@heeeeyong heeeeyong added 🐞 BugFix Something isn't working ✨ Feature 기능 개발 📬 API 서버 API 통신 labels Oct 13, 2025
@vercel
Copy link

vercel bot commented Oct 13, 2025

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

Project Deployment Preview Comments Updated (UTC)
thip Ready Ready Preview Comment Oct 13, 2025 9:09am

@coderabbitai
Copy link

coderabbitai bot commented Oct 13, 2025

Walkthrough

알림 존재 여부 조회 API를 추가하고, 인증 준비 상태(Zustand store)를 도입하여 소셜 로그인 토큰 획득 흐름 완료 시 준비 완료 플래그를 설정합니다. 헤더는 준비 및 인증 상태를 감지해 알림 뱃지 아이콘을 조건부로 표시합니다. 팔로워/팔로잉 리스트는 총 개수 필드 수신과 프리페치/무한 스크롤 로직을 보강했습니다.

Changes

Cohort / File(s) Summary
Auth readiness 상태 도입
src/stores/useAuthReadyStore.ts, src/hooks/useSocialLoginToken.ts
새로운 Zustand 스토어 useAuthReadyStore(isReady, setReady) 추가. 소셜 로그인 토큰 처리 흐름 종료 시 setReady(true) 호출하도록 훅 수정. 로그인 흐름이 아닌 경우에도 즉시 준비 완료로 전환.
알림 존재 여부 API 및 헤더 연동
src/api/notifications/getNotificationExist.ts, src/components/common/MainHeader.tsx
/notifications/exists-unchecked GET 호출 모듈 추가(getNotificationExist, GetNotificationExistResponse). 헤더에서 인증 준비 및 토큰 확인 후 알림 존재 여부를 조회해 벨 아이콘을 exist-bell.svg/bell.svg로 분기 렌더링.
팔로워/팔로잉 카운트 타입 확장
src/api/users/getFollowerList.ts, src/api/users/getFollowingList.ts
응답 타입에 선택 필드 totalFollowerCount?, totalFollowingCount? 추가. 기존 필드와 동작은 유지.
팔로워 리스트 페이지 로딩/스크롤 개선
src/pages/feed/FollowerListPage.tsx
총 개수 상태 반영, 초기/추가 로딩 스피너 분리, 최소 높이 조정, 스크롤 트리거 임계값 200→100px, 뷰포트 미충족 시 프리페치 useEffect 추가, 조건부 렌더링 정리.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant H as MainHeader
  participant AR as useAuthReadyStore
  participant SL as useSocialLoginToken
  participant API as apiClient
  participant S as Server

  U->>SL: 앱 진입 / 소셜 로그인 콜백 처리
  SL->>AR: setReady(true) (성공/실패/비적용 경로 포함)
  Note right of AR: isReady = true

  H->>AR: isReady 구독
  H->>H: 로컬스토리지에서 authToken 확인
  alt isReady && authToken 존재
    H->>API: GET /notifications/exists-unchecked
    API->>S: 요청
    S-->>API: { exists: boolean }
    API-->>H: 응답 데이터
    H->>H: hasUnchecked 업데이트
    H->>U: exist-bell.svg 또는 bell.svg 렌더
  else
    H->>U: 기본 bell.svg 렌더
  end
Loading
sequenceDiagram
  autonumber
  actor U as User
  participant P as FollowerListPage
  participant API as users.get(Followers/Following)
  participant S as Server

  U->>P: 페이지 진입
  P->>API: 첫 페이지 로드
  API->>S: 요청
  S-->>API: data, nextCursor, totalCount?
  API-->>P: 응답
  P->>P: 목록/TotalBar 렌더, 초기 스피너 해제

  alt 콘텐츠 높이 < 뷰포트
    P->>API: 다음 페이지 프리페치
    API->>S: 요청
    S-->>API: 응답
    API-->>P: 데이터 추가
  end

  loop 스크롤 근접(100px)
    P->>API: 다음 페이지 로드
    API->>S: 요청
    S-->>API: 응답
    API-->>P: 목록 추가 / 바닥 스피너 토글
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Poem

종소리 또르르, 알림이 왔나봥🔔
토큰은 준비 완료, 깃발은 펄럭펄럭🏳️
팔로워 수를 세어보며, 리스트는 술술슥—
뷰포트 비면 척! 또 한 장 더 펼치고
토끼는 깡총, 코드도 깡총, 오늘도 배포 끝! 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 제목이 알림센터에서 알림 유무 확인 기능 구현이라는 주된 변경사항을 간결하고 명확하게 요약하고 있어 주요 변경의 의도를 잘 전달합니다.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/minor-updates

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

@heeeeyong heeeeyong merged commit 061408d into develop Oct 13, 2025
3 checks passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
src/pages/feed/FollowerListPage.tsx (1)

32-36: 중복 요청 위험: loading의 최신값을 보지 못하는 stale closure 문제

  • handleScroll에서 loading을 참조하지만 deps에 포함되지 않아 오래된 값으로 판단합니다.
  • loadUserListloading을 캡처하지만 deps에 없어서 최신값을 보지 못할 수 있습니다.
  • 결과적으로 스크롤 중 중복 fetch가 발생할 수 있습니다. 또한 스크롤 리스너는 passive로 등록하는 게 성능상 안전합니다.

아래 최소 수정으로 막을 수 있습니다.

-  useEffect(() => {
+  useEffect(() => {
     const handleScroll = () => {
       // 로딩 중이거나 마지막 페이지이거나 에러가 있거나 재시도 횟수 초과시 요청하지 않음
       if (loading || isLast || error || retryCount >= 3 || !nextCursor) {
         console.log('스크롤 요청 차단:', { loading, isLast, error, retryCount, nextCursor });
         return;
       }
@@
-    window.addEventListener('scroll', handleScroll);
+    window.addEventListener('scroll', handleScroll, { passive: true });
     return () => window.removeEventListener('scroll', handleScroll);
-  }, [isLast, error, retryCount, nextCursor, loadUserList]);
+  }, [isLast, error, retryCount, nextCursor, loading, loadUserList]);

그리고 loadUserList도 최신 loading을 보도록 deps를 보강하세요:

-  const loadUserList = useCallback(
+  const loadUserList = useCallback(
     async (cursor?: string) => {
       if (loading) return;
@@
-    [type, userId],
+    [type, userId, loading],
   );

대안(권장): 중복 요청 방지를 확실히 하려면 useRef로 in-flight 플래그를 둬도 됩니다.

// 상단
const isFetchingRef = useRef(false);

// 내부
if (isFetchingRef.current) return;
isFetchingRef.current = true;
try {
  ...
} finally {
  isFetchingRef.current = false;
}

Based on learnings (React 19의 useEffectEvent도 이벤트 핸들러 최신 상태 관찰에 유용).

Also applies to: 101-103, 104-125

♻️ Duplicate comments (1)
src/api/users/getFollowerList.ts (1)

12-12: follower 쪽도 동일하게 정렬되어 좋습니다. 명명 통합 어댑터 권장

  • following과 대칭으로 잘 추가되었습니다.
  • 위 파일 코멘트와 동일: 도메인 매핑으로 totalCount 통합, nextCursor null 가능성 계약 확인.
🧹 Nitpick comments (10)
src/stores/useAuthReadyStore.ts (1)

8-11: 잘 추가되었습니다. 작고 명확한 스토어입니다.

  • v5의 named import 사용 OK.
  • 선택자 상수화와 명명 명확화 제안:
    • 예: isAuthReady로 명확히 하고, selector를 export 해서 재사용/리렌더 최소화.

예시:

  • export const selectIsAuthReady = (s: AuthReadyState) => s.isReady
  • export const selectSetAuthReady = (s: AuthReadyState) => s.setReady

Based on learnings

src/api/notifications/getNotificationExist.ts (1)

13-18: API 모듈 OK. 소비자에 boolean만 노출하는 래퍼 추가를 권장

  • 현재 형태 유지하되, UI에서는 boolean을 바로 쓰도록 작은 헬퍼를 추가하면 사용성이 좋아집니다.

추가 함수 예시(동파일 내):

export const hasUncheckedNotifications = async (): Promise<boolean> => {
  const res = await getNotificationExist();
  return res.isSuccess && !!res.data?.exists;
};

또한 React Query를 사용할 경우, enabled 플래그로 인증 준비 상태를 연동하면 호출 시점을 더 깔끔하게 제어할 수 있습니다(아래 MainHeader 코멘트 참고).

src/hooks/useSocialLoginToken.ts (1)

55-69: ready 처리 타이밍 안정화 및 소비 측 대안 제안

  • setReady(true)를 모든 경로에서 호출한 점 좋습니다. 안전성을 위해 finally로 감싸면 조기 리턴/예외에도 확실합니다.

예시:

-      } catch (error) {
-        console.error('💥 토큰 발급 중 오류 발생:', error);
-      }
-      // 토큰 발급 시도 완료 시점에 ready true
-      setReady(true);
+      } catch (error) {
+        console.error('💥 토큰 발급 중 오류 발생:', error);
+      } finally {
+        setReady(true);
+      }
  • MainHeader가 전역 ready를 구독하는 대신, 이 훅이 노출하는 waitForToken을 사용해 소비 측에서 await하는 방법도 고려해볼 수 있습니다.
    • 장점: 전역 상태 결합도↓, 호출 지점에서 의도를 명확히 표현.
    • 단점: 여러 소비자가 있으면 각자 await 호출 필요.
src/components/common/MainHeader.tsx (2)

22-37: 토큰 발급 전 호출 이슈: enabled 기반의 데이터 패칭으로 단순화 권장

현재 isAuthReady + localStorage 검사로 잘 가드하고 있습니다. 더 견고/단순하게 하려면:

  • React Query 도입 시:
    • enabled: isAuthReady && !!authToken
    • refetchOnWindowFocus: true, staleTime으로 중복 요청 감소
    • 에러/로딩 상태 표준화

간단 예시:

const authToken = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null;

const { data } = useQuery({
  queryKey: ['notifications', 'exists'],
  queryFn: getNotificationExist,
  enabled: isAuthReady && !!authToken,
  staleTime: 60_000,
  refetchOnWindowFocus: true,
});

useEffect(() => {
  if (data?.isSuccess) setHasUnchecked(data.data.exists);
}, [data]);
  • 대안: axios 인터셉터 + TokenManager로 “토큰 준비 Promise”를 모든 요청이 자동 대기하도록 구성하면 컴포넌트마다 가드 로직이 불필요해집니다.

장단:

  • React Query: 구현 난이도 낮고 컴포넌트 단위로 제어 용이.
  • 인터셉터: 전역 일관성↑, 초기 설정 난이도↑.

48-52: 접근성 소소 개선 제안

  • alt를 상태 반영형으로 분기하면 보조기기 사용성이 좋아집니다.
    • 예: alt={hasUnchecked ? '읽지 않은 알림 있음' : '알림 없음'}
src/pages/feed/FollowerListPage.tsx (5)

82-90: totalCount 설정 로직: 첫 페이지/후속 페이지 분리 + 폴백 추가 권장

API에 total 값이 없으면 “전체 0”이 노출될 수 있습니다. 첫 페이지에서는 API total을 사용하되, 없으면 길이로 폴백하고, 이후 페이지는 누적으로 합산하는 편이 견고합니다.

-        // 총합 카운트 설정 (API별 키 분기)
-        if (type === 'followerlist') {
-          const total = (response.data as { totalFollowerCount?: number }).totalFollowerCount;
-          if (typeof total === 'number') setTotalCount(total);
-        } else {
-          const total = (response.data as { totalFollowingCount?: number }).totalFollowingCount;
-          if (typeof total === 'number') setTotalCount(total);
-        }
-        // setTotalCount(prev => prev + userData.length);
+        // 총합 카운트 설정: 첫 페이지는 API total, 없으면 길이로 폴백 / 이후 페이지는 누적
+        if (!cursor) {
+          const total = type === 'followerlist'
+            ? (response.data as { totalFollowerCount?: number }).totalFollowerCount
+            : (response.data as { totalFollowingCount?: number }).totalFollowingCount;
+          setTotalCount(typeof total === 'number' ? total : userData.length);
+        } else {
+          setTotalCount(prev => prev + userData.length);
+        }

104-125: 무한스크롤: scroll 계산 대신 IntersectionObserver로 단순·정확하게

현재 스크롤 계산 + 프리페치 이펙트 두 경로를 합쳐, sentinel을 관찰하는 IntersectionObserver 한 경로로 단순화하면 정확도와 성능이 좋아집니다(리플로우/리스너 부담 감소).

예시:

// import { useRef } from 'react'
const sentinelRef = useRef<HTMLDivElement>(null);

useEffect(() => {
  if (isLast || error || retryCount >= 3) return;
  const el = sentinelRef.current;
  if (!el) return;
  const io = new IntersectionObserver(
    ([entry]) => {
      if (entry.isIntersecting && !loading && nextCursor) {
        loadUserList(nextCursor);
      }
    },
    { root: null, rootMargin: '100px' } // 기존 threshold 유지
  );
  io.observe(el);
  return () => io.disconnect();
}, [isLast, error, retryCount, nextCursor, loading, loadUserList]);

// 렌더 하단
<div ref={sentinelRef} style={{ height: 1 }} />

Based on learnings

Also applies to: 131-139


143-169: 로딩 스피너 접근성 보완

보조기기에 로딩 상태를 알려주도록 live region/role을 추가하세요.

-      {loading && userList.length === 0 ? (
-        <LoadingSpinner size="medium" fullHeight={true} />
+      {loading && userList.length === 0 ? (
+        <div aria-live="polite" role="status">
+          <LoadingSpinner size="medium" fullHeight={true} />
+        </div>
       ) : (
@@
-          {loading && userList.length > 0 && (
-            <div style={{ display: 'flex', justifyContent: 'center', padding: '16px 0' }}>
-              <LoadingSpinner size="small" />
-            </div>
-          )}
+          {loading && userList.length > 0 && (
+            <div aria-live="polite" role="status" style={{ display: 'flex', justifyContent: 'center', padding: '16px 0' }}>
+              <LoadingSpinner size="small" />
+            </div>
+          )}

180-180: 중첩된 100vh로 여백 과다/스크롤 이슈 가능

Wrapper와 List 모두 min-height: 100vh라 중첩되어 여백이 과해질 수 있습니다. List는 header 높이를 뺀 값으로 계산하는 게 안전합니다.

-  min-height: 100vh;
+  min-height: calc(100vh - 105px);

Also applies to: 204-207


20-20: PR 요구사항 1 답변: 알림 API 호출 시 토큰 준비 문제 — enabled/selector 기반 접근 추천

MainHeader에서 전역 토큰/준비 플래그를 폴링하기보다, 데이터 패칭 레이어에서 토큰 준비를 제어하세요.

  • React Query 권장 패턴:
    • fetcher는 항상 현재 토큰을 읽고(예: zustand selector),
    • useQuery에 enabled: isAuthReady && !!accessToken로 조건부 활성화를 거세요.
  • 필요 시 axios 인스턴스에 요청 인터셉터로 토큰 주입(또는 준비 전 대기) 구현.
  • 토큰 변경에 따른 재호출은 query key에 토큰 또는 userId를 일부로 포함해 자연스럽게 재검증.

예시:

const accessToken = useAuthStore(s => s.token)
const isAuthReady = useAuthStore(s => s.isReady)

useQuery({
  queryKey: ['notification-exists', { userId, token: !!accessToken }],
  queryFn: () => api.get('/notifications/exists'),
  enabled: isAuthReady && !!accessToken,
  staleTime: 60_000,
  refetchOnMount: false,
})

이 방식이면 “토큰 발급 이전 호출” 자체가 발생하지 않습니다. 현재 구현과 비교해볼지 확인 부탁드립니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1c3dfbd and 953465a.

⛔ Files ignored due to path filters (2)
  • src/assets/header/bell.svg is excluded by !**/*.svg
  • src/assets/header/exist-bell.svg is excluded by !**/*.svg
📒 Files selected for processing (7)
  • src/api/notifications/getNotificationExist.ts (1 hunks)
  • src/api/users/getFollowerList.ts (1 hunks)
  • src/api/users/getFollowingList.ts (1 hunks)
  • src/components/common/MainHeader.tsx (3 hunks)
  • src/hooks/useSocialLoginToken.ts (2 hunks)
  • src/pages/feed/FollowerListPage.tsx (7 hunks)
  • src/stores/useAuthReadyStore.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/api/notifications/getNotificationExist.ts (1)
src/api/index.ts (1)
  • apiClient (7-13)
src/pages/feed/FollowerListPage.tsx (1)
src/types/user.ts (1)
  • UserProfileType (1-1)
src/hooks/useSocialLoginToken.ts (1)
src/stores/useAuthReadyStore.ts (1)
  • useAuthReadyStore (8-11)
src/components/common/MainHeader.tsx (3)
src/stores/useAuthReadyStore.ts (1)
  • useAuthReadyStore (8-11)
src/api/notifications/getNotificationExist.ts (1)
  • getNotificationExist (13-18)
src/components/common/IconButton.tsx (1)
  • IconButton (3-7)
🔇 Additional comments (1)
src/api/users/getFollowingList.ts (1)

12-12: totalFollowingCount 추가 — 타입/명명 합의 필요

  • follower/following 공통 처리를 위해 도메인 계층에서 totalCount로 매핑하는 어댑터 도입 권장(조건 분기 감소)
  • nextCursor가 빈 문자열("") 또는 null로 내려올 가능성을 백엔드 계약에서 확인하고, 필요 시 응답 타입을 string | null로 조정하세요

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📬 API 서버 API 통신 🐞 BugFix Something isn't working ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant