Skip to content

[FIX] 9월 3주차 2차 QA 사항 - 희용#260

Merged
heeeeyong merged 7 commits intodevelopfrom
chore/minor-updates
Sep 26, 2025
Merged

[FIX] 9월 3주차 2차 QA 사항 - 희용#260
heeeeyong merged 7 commits intodevelopfrom
chore/minor-updates

Conversation

@heeeeyong
Copy link
Collaborator

@heeeeyong heeeeyong commented Sep 23, 2025

#️⃣연관된 이슈

없음

📝작업 내용

  • 구글 검색시 노출되는 메타데이터 추가

    image

  • 알림센터 스크롤 바 보이지 않게 수정

    image

  • 알림센터에서 알림을 누를 때, 해당하는 기록이 필터링된 채로 리다이렉트되게끔 구현 (만약, 댓글&답글에 해당하는 알림의 경우 해당 기록의 댓글 모달까지 열리게끔 함)

2025-09-24.11.54.56.mov
2025-09-25.3.51.04.mov

💬리뷰 요구사항

없음

Summary by CodeRabbit

  • New Features
    • 알림을 탭하면 서버 확인 후 관련 게시물·사용자·룸 등으로 즉시 이동합니다(클릭형 알림).
    • 특정 링크나 상태로 메모리 화면 진입 시 댓글 바텀시트를 자동으로 열어 바로 댓글을 확인합니다.
  • Style
    • 알림 목록의 스크롤바를 숨겨 더 깔끔한 화면을 제공합니다(스크롤 동작 유지).
  • Chores
    • 메타 설명 태그를 추가해 검색 엔진 노출 및 소셜 공유 미리보기 품질을 개선했습니다.
    • 분석 라이브러리를 교체하고 초기화/페이지뷰·이벤트 전송 로직을 정비해 통계 수집 신뢰성을 향상했습니다.

@vercel
Copy link

vercel bot commented Sep 23, 2025

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

Project Deployment Preview Comments Updated (UTC)
thip Ready Ready Preview Comment Sep 26, 2025 3:32am

@coderabbitai
Copy link

coderabbitai bot commented Sep 23, 2025

Walkthrough

알림 클릭 시 /notifications/check에 POST 요청해 반환된 route/params에 따라 앱 내 네비게이션을 수행하도록 로직이 추가되었습니다. 신규 API 래퍼, Memory 페이지의 댓글 바텀시트 자동 오픈, index.html meta description, NotificationList의 스크롤바 숨김 CSS, 그리고 GA 통합 리팩터링이 포함됩니다.

Changes

Cohort / File(s) Summary
SEO 메타 업데이트
index.html
<meta name="description" content="커뮤니티형 독서 기록 플랫폼 띱입니다." /> 추가
알림 체크 API 추가
src/api/notifications/postNotificationsCheck.ts
새 API 래퍼 추가: postNotificationsCheck(notificationId: number) 및 요청/응답 타입 정의 (PostNotificationsCheckRequest, PostNotificationsCheckResponse<Params>)
Notice: 클릭 네비게이션 및 UI
src/pages/notice/Notice.tsx
postNotificationsCheck 호출을 통한 handleNotificationClick 도입; 클릭 시 서버 응답의 route/params 기반 내부 라우팅 수행(여러 route 케이스 처리). NotificationList에 크로스브라우저 스크롤바 숨김 CSS 추가. 기존 무응답/읽음 처리 주석 유지
Memory: 댓글 바텀시트 자동 오픈
src/pages/memory/Memory.tsx
useCommentBottomSheetStoreopenCommentBottomSheet 사용 추가. location.state로 초기 페이지 범위 설정 및 댓글 바텀시트 자동 오픈 후 경로 교체로 중복 실행 방지
Google Analytics 리팩터링
package.json, src/lib/ga.ts, src/main.tsx
react-ga4 의존성 추가. src/lib/ga.ts를 ReactGA 기반으로 재작성(초기화 가드, trackEvent 추가, 기존 sendPageView 호출 방식 변경). src/main.tsx에서 sendPageView 호출 제거하고 initGA만 호출

Sequence Diagram(s)

sequenceDiagram
  participant User as 사용자
  participant UI as Notice UI
  participant API as /notifications/check
  participant Router as 앱 라우터

  rect #E6F2FF
  Note over UI,API: 알림 클릭 흐름 (신규)
  User->>UI: 알림 클릭 (notificationId)
  UI->>API: POST /notifications/check { notificationId }
  API-->>UI: { route, params }
  alt route == NONE
    UI->>Router: 알림 목록 유지 또는 기본 처리
  else route == FEED_USER / FEED_DETAIL / ROOM_*
    UI->>Router: navigate(route, params)
  end
  Router-->>UI: 네비게이션 완료
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

깡총깡총, 알림을 톡 눌러요 🐇
서버에게 묻고 길을 찾아가요.
메타는 속삭여요, “띱을 소개해요.”
스크롤은 숨고, 댓글창은 달려와요.
당근 하나로 배포를 축하해요! 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning PR 제목 “[FIX] 9월 3주차 2차 QA 사항 - 희용”은 메타 태그 추가, 알림 클릭 동작, GA4 연동 등 실제 변경 내용을 반영하지 않고 QA 일정만 언급해 스캔하는 동료가 주요 변경 사항을 파악하기 어렵습니다. 제목을 주요 변경 사항을 간결히 요약하는 형태로 업데이트해 주세요. 예를 들어 “[FIX] SEO 메타 설명 추가 및 알림 클릭 후 필터된 화면 리다이렉션 구현”처럼 핵심 기능을 반영하는 문구를 사용해 보시기 바랍니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/minor-updates

📜 Recent 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 3c3367c and 5c90a7a.

📒 Files selected for processing (1)
  • src/lib/ga.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/ga.ts

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.

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

🧹 Nitpick comments (4)
index.html (2)

2-2: html lang="en" → "ko"로 변경 권장 (SEO/접근성).

문서 주언어가 한국어라 lang을 ko로 지정하는 것이 적절합니다.

다음과 같이 수정 권장:

-<html lang="en">
+<html lang="ko">

15-22: viewport 메타 태그가 중복입니다 — 하나로 통일하세요.

중복 시 브라우저별 해석 차로 예기치 않은 동작이 날 수 있습니다. 아래처럼 하나만 유지 권장(라인 22 유지, 라인 15 삭제).

-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
src/pages/notice/Notice.tsx (2)

167-174: 스크롤 컨테이너 크기 확정 필요 가능성 — flex:1/min-height:0 추가 제안.

Wrapper가 overflow:hidden이고 페이지 스크롤을 막는 구조라면, 리스트가 자체 스크롤 되려면 컨테이너에 높이를 확정해주는 편이 안전합니다.

 const NotificationList = styled.div`
   display: flex;
   flex-direction: column;
   gap: 12px;
   padding: 0 20px 20px 20px;
   width: 100%;
+  flex: 1;
+  min-height: 0; /* flex 컨테이너 내 내부 스크롤 보장 */
   overflow-y: auto;

61-79: 무한 스크롤 관찰 루트 확인 요청.

NotificationList가 내부 스크롤 컨테이너라면 IntersectionObserver의 root를 해당 엘리먼트로 지정해야 트리거가 안정적입니다. 현재 root:null(뷰포트)입니다.

필요 시: NotificationList에 ref 부여 후 new IntersectionObserver(cb, { root: listEl, ... }) 형태로 전환을 검토해주세요.

📜 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 d03a4a0 and d0f427a.

📒 Files selected for processing (2)
  • index.html (1 hunks)
  • src/pages/notice/Notice.tsx (1 hunks)
🔇 Additional comments (2)
index.html (1)

18-18: 메타 description 추가 좋습니다.

검색 스니펫 품질 향상에 기여합니다.

src/pages/notice/Notice.tsx (1)

174-179: 브라우저별 스크롤바 숨김 처리 적절합니다.

IE/Firefox/WebKit 모두 커버합니다. 키보드 스크롤/스크린리더 사용성도 한번 실제 기기에서 확인 부탁드립니다.

@heeeeyong heeeeyong added 🐞 BugFix Something isn't working 📃 Docs Improvements or additions to documentation labels Sep 23, 2025
@heeeeyong heeeeyong self-assigned this Sep 23, 2025
@heeeeyong heeeeyong added this to the 9월 3주차 QA milestone Sep 23, 2025
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

🧹 Nitpick comments (8)
src/api/notifications/postNotificationsCheck.ts (2)

7-15: route/params를 구체 타입으로 선언해 분기에서 타입 안전성 확보

string 대신 리터럴 유니온과 분기별 params 맵으로 모델링하면 Notice.tsx의 as 캐스트를 제거할 수 있고, 잘못된 분기/파라미터가 컴파일 타임에 잡힙니다.

백엔드 응답 스펙(라우트 키/파라미터 키·타입)을 한 번만 확인해 주세요.

-export interface PostNotificationsCheckResponse<Params = Record<string, unknown>> {
-  isSuccess: boolean;
-  code: number;
-  message: string;
-  data: {
-    route: string; // e.g., 'POST_DETAIL'
-    params?: Params; // e.g., { postId: 123 }
-  };
-}
+export type NotificationRoute =
+  | 'NONE'
+  | 'FEED_USER'
+  | 'FEED_DETAIL'
+  | 'ROOM_MAIN'
+  | 'ROOM_DETAIL'
+  | 'ROOM_POST_DETAIL'
+  | 'ROOM_RECORD_DETAIL'
+  | 'ROOM_VOTE_DETAIL';
+
+export type RouteParamsMap = {
+  NONE: Record<string, never>;
+  FEED_USER: { userId: number };
+  FEED_DETAIL: { feedId: number };
+  ROOM_MAIN: { roomId: number };
+  ROOM_DETAIL: { roomId: number };
+  ROOM_POST_DETAIL: { roomId: number; postId?: number; page?: number; postType?: 'RECORD' | 'VOTE' };
+  ROOM_RECORD_DETAIL: { roomId: number; postId?: number; page?: number; postType: 'RECORD' };
+  ROOM_VOTE_DETAIL: { roomId: number; postId?: number; page?: number; postType: 'VOTE' };
+};
+
+export interface PostNotificationsCheckResponse {
+  isSuccess: boolean;
+  code: number;
+  message: string;
+  data:
+    | { route: 'NONE' }
+    | { route: 'FEED_USER'; params: RouteParamsMap['FEED_USER'] }
+    | { route: 'FEED_DETAIL'; params: RouteParamsMap['FEED_DETAIL'] }
+    | { route: 'ROOM_MAIN'; params: RouteParamsMap['ROOM_MAIN'] }
+    | { route: 'ROOM_DETAIL'; params: RouteParamsMap['ROOM_DETAIL'] }
+    | { route: 'ROOM_POST_DETAIL'; params: RouteParamsMap['ROOM_POST_DETAIL'] }
+    | { route: 'ROOM_RECORD_DETAIL'; params: RouteParamsMap['ROOM_RECORD_DETAIL'] }
+    | { route: 'ROOM_VOTE_DETAIL'; params: RouteParamsMap['ROOM_VOTE_DETAIL'] };
+}

18-25: 반환 타입을 명시해 사용처 추론 안정화

함수 반환 타입을 명시하면 사용처에서 res.data의 타입이 안정적으로 추론됩니다.

-export const postNotificationsCheck = async (notificationId: number) => {
+export const postNotificationsCheck = async (
+  notificationId: number,
+): Promise<PostNotificationsCheckResponse> => {
   const body: PostNotificationsCheckRequest = { notificationId };
   const response = await apiClient.post<PostNotificationsCheckResponse>(
     '/notifications/check',
     body,
   );
   return response.data;
 };
src/pages/notice/Notice.tsx (6)

8-8: 타입도 함께 가져와 as 단언 제거 준비

API 응답 타입(예: PostNotificationsCheckResponse/NotificationRoute)을 함께 import하면 아래 res.data as { ... } 단언을 없애고 분기별 파라미터를 타입 안전하게 처리할 수 있습니다. (API 타입 정비가 선행되면 적용)


86-97: 이미 읽은 알림 중복 호출 방지 + 낙관적 읽음 처리 활성화

  • 이미 읽은 항목은 API 재호출을 막아 불필요한 트래픽/레이스 방지
  • 즉시 읽음 UI 반영(낙관적 업데이트)으로 체감 반응성 개선
-const handleNotificationClick = async (notif: NotificationItem) => {
+const handleNotificationClick = async (notif: NotificationItem) => {
+  if (notif.isChecked) return; // 이미 읽은 알림은 무시
   try {
-    const res = await postNotificationsCheck(notif.notificationId);
+    // UI 즉시 반영: 읽음 처리(낙관적)
+    setNotifications(prev =>
+      prev.map(item =>
+        item.notificationId === notif.notificationId ? { ...item, isChecked: true } : item,
+      ),
+    );
+    const res = await postNotificationsCheck(notif.notificationId);
     if (!res.isSuccess) return;
 
-    // UI 즉시 반영: 읽음 처리
-    // setNotifications(prev =>
-    //   prev.map(item =>
-    //     item.notificationId === notif.notificationId ? { ...item, isChecked: true } : item,
-    //   ),
-    // );
+    // 성공 시 그대로 유지

98-156: 서버 params를 숫자로 안전 변환 후 검증

as number는 런타임 변환이 없어 '123' 같은 문자열이 그대로 통과합니다. Number() 변환 + isFinite 검증으로 방어하세요. 또한 알 수 없는 route는 경고 로그를 남기면 추적이 쉽습니다.

-      const { route, params } = res.data as { route: string; params?: Record<string, unknown> };
+      const { route, params } = res.data as { route: string; params?: Record<string, unknown> };

       // 서버 라우팅 키 → 실제 앱 경로 매핑
       switch (route) {
@@
-        case 'FEED_USER': {
-          const userId = (params?.userId as number) ?? undefined;
-          if (userId !== undefined) {
-            navigate(`/otherfeed/${userId}`);
-          }
+        case 'FEED_USER': {
+          const userId = Number(params?.userId);
+          if (Number.isFinite(userId)) {
+            navigate(`/otherfeed/${userId}`);
+          } else {
+            console.warn('알림 이동 실패: 잘못된 userId', params?.userId);
+          }
           break;
         }
@@
-        case 'FEED_DETAIL': {
-          const feedId = (params?.feedId as number) ?? undefined;
-          if (feedId !== undefined) {
-            navigate(`/feed/${feedId}`);
-          }
+        case 'FEED_DETAIL': {
+          const feedId = Number(params?.feedId);
+          if (Number.isFinite(feedId)) {
+            navigate(`/feed/${feedId}`);
+          } else {
+            console.warn('알림 이동 실패: 잘못된 feedId', params?.feedId);
+          }
           break;
         }
@@
-        case 'ROOM_MAIN': {
-          const roomId = (params?.roomId as number) ?? undefined;
-          if (roomId !== undefined) navigate(`/group/detail/joined/${roomId}`);
+        case 'ROOM_MAIN': {
+          const roomId = Number(params?.roomId);
+          if (Number.isFinite(roomId)) navigate(`/group/detail/joined/${roomId}`);
+          else console.warn('알림 이동 실패: 잘못된 roomId', params?.roomId);
           break;
         }
@@
-        case 'ROOM_DETAIL': {
-          const roomId = (params?.roomId as number) ?? undefined;
-          if (roomId !== undefined) navigate(`/group/detail/${roomId}`);
+        case 'ROOM_DETAIL': {
+          const roomId = Number(params?.roomId);
+          if (Number.isFinite(roomId)) navigate(`/group/detail/${roomId}`);
+          else console.warn('알림 이동 실패: 잘못된 roomId', params?.roomId);
           break;
         }
@@
-        case 'ROOM_POST_DETAIL':
-        case 'ROOM_RECORD_DETAIL':
-        case 'ROOM_VOTE_DETAIL': {
-          const roomId = (params?.roomId as number) ?? undefined;
-          const postId = (params?.postId as number) ?? undefined;
-          const page = (params?.page as number) ?? undefined;
-          const postType = params?.postType as 'RECORD' | 'VOTE';
-          if (roomId !== undefined) {
-            navigate(`/rooms/${roomId}/memory`, {
-              state: { focusPostId: postId, postType, page },
-            });
-          }
+        case 'ROOM_POST_DETAIL':
+        case 'ROOM_RECORD_DETAIL':
+        case 'ROOM_VOTE_DETAIL': {
+          const roomId = Number(params?.roomId);
+          const postId = params?.postId != null ? Number(params?.postId) : undefined;
+          const page = params?.page != null ? Number(params?.page) : undefined;
+          const postType = params?.postType as 'RECORD' | 'VOTE' | undefined;
+          if (Number.isFinite(roomId)) {
+            navigate(`/rooms/${roomId}/memory`, { state: { focusPostId: postId, postType, page } });
+          } else {
+            console.warn('알림 이동 실패: 잘못된 roomId', params?.roomId);
+          }
           break;
         }
@@
-        default:
-          break;
+        default:
+          console.warn('알 수 없는 알림 route:', route, params);
+          break;
       }

추가 확인:

  • 'ROOM_POST_DETAIL'에서 postType이 'POST' 등 다른 값일 수 있으면 상위 API 타입/분기 정의도 함께 조정이 필요합니다. 백엔드 스펙을 확인해 주세요.

157-160: API 실패 시 낙관적 읽음 처리 롤백

성공 시점 이전에 읽음 표시했으므로, 실패하면 되돌려야 합니다.

-    } catch (e) {
-      // noop: 실패 시 네비게이션 없이 무시
-      console.error('알림 확인 처리 실패:', e);
-    }
+    } catch (e) {
+      // 실패 시 읽음 처리 롤백
+      setNotifications(prev =>
+        prev.map(item =>
+          item.notificationId === notif.notificationId ? { ...item, isChecked: false } : item,
+        ),
+      );
+      console.error('알림 확인 처리 실패:', e);
+    }

187-187: onClick 바인딩 OK

아이템별 클릭 처리 연결이 명확합니다. 위 가드/낙관적 업데이트 적용 시 UX가 더 좋아집니다.


246-251: 스크롤바 숨김은 접근성에 영향—대체 신호 제공 검토

시각적 스크롤 표시가 사라지면 스크롤 가능성을 인지하기 어렵습니다. 그라데이션 페이드, “더 보기” 힌트, 키보드 포커스 이동 지원(tabIndex/role="region" aria-label) 등 보완책을 검토해 주세요.

📜 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 d0f427a and c9828fe.

📒 Files selected for processing (2)
  • src/api/notifications/postNotificationsCheck.ts (1 hunks)
  • src/pages/notice/Notice.tsx (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/pages/notice/Notice.tsx (2)
src/api/notifications/getNotifications.ts (1)
  • NotificationItem (3-10)
src/api/notifications/postNotificationsCheck.ts (1)
  • postNotificationsCheck (18-25)
src/api/notifications/postNotificationsCheck.ts (1)
src/api/index.ts (1)
  • apiClient (7-13)

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

🧹 Nitpick comments (3)
src/pages/memory/Memory.tsx (3)

35-46: voteItems 미존재 시 런타임 에러 가능 + O(n^2) 연산 — 안전 가드 및 1-pass로 개선 제안

  • postType이 RECORD인데 voteItems가 undefined/null이면 map에서 크래시 납니다.
  • maxCount를 매 아이템마다 계산해 O(n^2)입니다. 한 번만 계산하도록 바꿔주세요.

아래처럼 postType 가드 + items 축약 + reduce 1-pass로 개선을 제안드립니다.

-    pollOptions: post.voteItems.map(item => {
-      const maxCount = Math.max(...post.voteItems.map(v => v.count || 0));
-      return {
-        id: item.voteItemId.toString(),
-        text: item.itemName,
-        percentage: item.percentage,
-        count: item.count || 0,
-        isHighest: (item.count || 0) === maxCount && maxCount > 0,
-        voteItemId: item.voteItemId,
-        isVoted: item.isVoted,
-      };
-    }),
+    pollOptions: post.postType === 'VOTE'
+      ? (() => {
+          const items = post.voteItems ?? [];
+          const maxCount = items.reduce((m, v) => Math.max(m, v.count ?? 0), 0);
+          return items.map((item) => ({
+            id: String(item.voteItemId),
+            text: item.itemName,
+            percentage: item.percentage,
+            count: item.count ?? 0,
+            isHighest: (item.count ?? 0) === maxCount && maxCount > 0,
+            voteItemId: item.voteItemId,
+            isVoted: item.isVoted,
+          }));
+        })()
+      : [],

54-54: Zustand selector로 함수만 구독하여 불필요 리렌더 방지

스토어 전체가 아닌 액션만 구독하면 다른 상태 변경 시 이 컴포넌트 리렌더를 피할 수 있습니다.

-  const { openCommentBottomSheet } = useCommentBottomSheetStore();
+  const openCommentBottomSheet = useCommentBottomSheetStore(s => s.openCommentBottomSheet);

156-179: 라우팅 state 처리 useEffect 의존성/가드 보강 및 ESLint 주석 제거

  • focusPostId/page는 truthy 체크 대신 number 체크로 안전하게 처리하세요.
  • navigate, openCommentBottomSheet, pathname 등을 deps에 포함해 ESLint 주석을 제거할 수 있습니다.
-  // Notice에서 넘어온 state(page, focusPostId 등)로 초기 필터 적용
-  useEffect(() => {
-    type MemoryLocationState = {
-      page?: number;
-      focusPostId?: number;
-      postType?: 'RECORD' | 'VOTE';
-      openComments?: boolean;
-    } | null;
-    const state = (location.state as MemoryLocationState) || null;
-    const initialPage = state?.page;
-    if (initialPage && !selectedPageRange) {
-      setSelectedPageRange({ start: initialPage, end: initialPage });
-      setActiveFilter('page');
-    }
-
-    // 댓글 모달 자동 오픈 처리
-    if (state?.openComments && state.focusPostId && state.postType) {
-      openCommentBottomSheet(state.focusPostId, state.postType);
-      // 동일 경로 재진입 시 중복 오픈 방지를 위해 state 제거
-      navigate(location.pathname, { replace: true });
-    }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [location.state, roomId]);
+  // Notice에서 넘어온 state(page, focusPostId 등)로 초기 필터/댓글 처리
+  useEffect(() => {
+    type MemoryLocationState = {
+      page?: number;
+      focusPostId?: number;
+      postType?: 'RECORD' | 'VOTE';
+      openComments?: boolean;
+    } | null;
+    const state = (location.state as MemoryLocationState) || null;
+    const initialPage = state?.page;
+    if (typeof initialPage === 'number' && !selectedPageRange) {
+      setSelectedPageRange({ start: initialPage, end: initialPage });
+      setActiveFilter('page');
+    }
+
+    // 댓글 모달 자동 오픈 처리
+    if (state?.openComments && typeof state.focusPostId === 'number' && state.postType) {
+      openCommentBottomSheet(state.focusPostId, state.postType);
+      // 동일 경로 재진입 시 중복 오픈 방지를 위해 state 제거
+      navigate(location.pathname, { replace: true });
+    }
+  }, [location.state, location.pathname, roomId, selectedPageRange, openCommentBottomSheet, navigate]);

추가로, MemoryLocationState 타입은 파일 상단에 선언해 재사용/가독성을 높이는 것을 권장합니다.

📜 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 c9828fe and 03dcdba.

📒 Files selected for processing (1)
  • src/pages/memory/Memory.tsx (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/pages/memory/Memory.tsx (1)
src/stores/useCommentBottomSheetStore.ts (1)
  • useCommentBottomSheetStore (14-26)
🔇 Additional comments (1)
src/pages/memory/Memory.tsx (1)

9-9: 알림 댓글 바텀시트 스토어 의존성 추가 — LGTM

주입 방향 자연스럽고, 파일 경로 별칭도 일관됩니다.

@heeeeyong heeeeyong added the ✨ Feature 기능 개발 label Sep 24, 2025
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: 1

🧹 Nitpick comments (4)
src/pages/notice/Notice.tsx (4)

86-165: as number 캐스트는 런타임에서 무효 — params를 안전하게 파싱/검증하세요

서버에서 문자열("123")이나 예기치 않은 타입이 올 경우에도 as number는 런타임에서 아무 일도 하지 않아 잘못된 값이 경로에 삽입될 수 있습니다. 숫자/유니온을 런타임에서 안전하게 파싱/검증하도록 보완을 권장합니다.

아래처럼 간단한 파서(helper)를 추가하고 각 분기에서 사용해 주세요:

-      const { route, params } = res.data as { route: string; params?: Record<string, unknown> };
+      const { route, params } = res.data as { route: string; params?: Record<string, unknown> };
+      const toNum = (v: unknown) => {
+        const n = Number(v);
+        return Number.isFinite(n) ? n : undefined;
+      };
+      const toPostType = (v: unknown) =>
+        v === 'RECORD' || v === 'VOTE' ? (v as 'RECORD' | 'VOTE') : undefined;

@@
-        case 'FEED_USER': {
-          const userId = (params?.userId as number) ?? undefined;
+        case 'FEED_USER': {
+          const userId = toNum(params?.userId);
           if (userId !== undefined) {
             navigate(`/otherfeed/${userId}`);
           }
           break;
         }
@@
-        case 'FEED_DETAIL': {
-          const feedId = (params?.feedId as number) ?? undefined;
+        case 'FEED_DETAIL': {
+          const feedId = toNum(params?.feedId);
           if (feedId !== undefined) {
             navigate(`/feed/${feedId}`);
           }
           break;
         }
@@
-        case 'ROOM_MAIN': {
-          const roomId = (params?.roomId as number) ?? undefined;
+        case 'ROOM_MAIN': {
+          const roomId = toNum(params?.roomId);
           if (roomId !== undefined) navigate(`/group/detail/joined/${roomId}`);
           break;
         }
@@
-        case 'ROOM_DETAIL': {
-          const roomId = (params?.roomId as number) ?? undefined;
+        case 'ROOM_DETAIL': {
+          const roomId = toNum(params?.roomId);
           if (roomId !== undefined) navigate(`/group/detail/${roomId}`);
           break;
         }
@@
-        case 'ROOM_POST_DETAIL': {
-          const roomId = (params?.roomId as number) ?? undefined;
-          const postId = (params?.postId as number) ?? undefined;
-          const page = (params?.page as number) ?? undefined;
-          const postType = params?.postType as 'RECORD' | 'VOTE';
-          const shouldOpenComments = (params as { openComments?: boolean })?.openComments === true;
+        case 'ROOM_POST_DETAIL': {
+          const roomId = toNum(params?.roomId);
+          const postId = toNum(params?.postId);
+          const page = toNum(params?.page);
+          const postType = toPostType(params?.postType);
+          const shouldOpenComments =
+            (params as { openComments?: unknown })?.openComments === true;
           if (roomId !== undefined) {
             navigate(`/rooms/${roomId}/memory`, {
               state: {
                 focusPostId: postId,
                 postType,
                 page,
                 ...(shouldOpenComments ? { openComments: true } : {}),
               },
             });
           }
           break;
         }

추가로, 중복 클릭 방지(연속 탭 시 다중 호출)용으로 processingId ref를 사용해 가드하는 것도 추천드립니다.


91-97: 읽음 상태 UI 반영이 주석 처리됨 — 성공 후 바로 반영하거나 낙관적 업데이트로 전환하세요

현재 UX상 클릭 후 읽음 처리 표시가 필요해 보입니다. 최소한 API 성공 시 즉시 UI를 갱신해 주세요.

-      // UI 즉시 반영: 읽음 처리
-      // setNotifications(prev =>
-      //   prev.map(item =>
-      //     item.notificationId === notif.notificationId ? { ...item, isChecked: true } : item,
-      //   ),
-      // );
+      // UI 반영: 읽음 처리 (성공 후 업데이트)
+      setNotifications(prev =>
+        prev.map(item =>
+          item.notificationId === notif.notificationId ? { ...item, isChecked: true } : item,
+        ),
+      );

원하시면 낙관적 업데이트(+실패 시 롤백)로 바꾸는 코드도 제공하겠습니다.


250-255: 스크롤바 숨김 처리 LGTM — 모바일 모멘텀/오버스크롤 보완 제안

iOS/안드로이드 경험 개선을 위해 모멘텀 스크롤과 오버스크롤 제어를 추가하는 것을 권장합니다.

   /* Hide scrollbar but keep scroll */
   -ms-overflow-style: none; /* IE and Edge */
   scrollbar-width: none; /* Firefox */
   &::-webkit-scrollbar {
     display: none; /* Chrome, Safari, Opera */
   }
+  /* Mobile scrolling polish */
+  -webkit-overflow-scrolling: touch;
+  overscroll-behavior-y: contain;

158-164: 알 수 없는 route/실패 케이스에 대한 사용자 피드백·관측성 보강

default 분기와 catch에서 단순 무시/console.error만 하면 사용자 혼란이 생길 수 있습니다. 토스트 안내와 모니터링(Sentry 등) 연동을 검토해 주세요.

📜 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 03dcdba and 0cc35c7.

📒 Files selected for processing (1)
  • src/pages/notice/Notice.tsx (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/pages/notice/Notice.tsx (2)
src/api/notifications/getNotifications.ts (1)
  • NotificationItem (3-10)
src/api/notifications/postNotificationsCheck.ts (1)
  • postNotificationsCheck (18-25)
🔇 Additional comments (1)
src/pages/notice/Notice.tsx (1)

8-8: 새 API 래퍼 import LGTM

의존성/경로 문제 없어 보입니다.

Comment on lines 188 to 193
<NotificationCard
key={notif.notificationId ?? idx}
read={notif.isChecked}
// onClick={() => handleReadNotification(idx)}
onClick={() => handleNotificationClick(notif)}
>
{!notif.isChecked && <UnreadDot />}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

클릭 가능한 div의 키보드 접근성 부족 — role/tabIndex/onKeyDown 추가 필요

현재 마우스 클릭만 가능하고 키보드로는 활성화할 수 없습니다. 최소한 role/button 시맨틱과 Enter/Space 처리 추가가 필요합니다.

-            <NotificationCard
+            <NotificationCard
               key={notif.notificationId ?? idx}
               read={notif.isChecked}
-              onClick={() => handleNotificationClick(notif)}
+              onClick={() => handleNotificationClick(notif)}
+              role="button"
+              tabIndex={0}
+              onKeyDown={(e) => {
+                if (e.key === 'Enter' || e.key === ' ') {
+                  e.preventDefault();
+                  handleNotificationClick(notif);
+                }
+              }}
             >
📝 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
<NotificationCard
key={notif.notificationId ?? idx}
read={notif.isChecked}
// onClick={() => handleReadNotification(idx)}
onClick={() => handleNotificationClick(notif)}
>
{!notif.isChecked && <UnreadDot />}
<NotificationCard
key={notif.notificationId ?? idx}
read={notif.isChecked}
onClick={() => handleNotificationClick(notif)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleNotificationClick(notif);
}
}}
>
{!notif.isChecked && <UnreadDot />}
🤖 Prompt for AI Agents
In src/pages/notice/Notice.tsx around lines 188-193, the NotificationCard is not
keyboard-accessible; add role="button", tabIndex={0}, and an onKeyDown handler
that maps Enter and Space keys to the same action as the onClick (i.e., call
handleNotificationClick(notif)), and preventDefault for Space to avoid page
scroll; ensure the onKeyDown uses the same notif variable and preserves existing
onClick behavior.

Comment on lines +115 to +122
// 피드 2~6번 (피드상세페이지로 이동)
case 'FEED_DETAIL': {
const feedId = (params?.feedId as number) ?? undefined;
if (feedId !== undefined) {
navigate(`/feed/${feedId}`);
}
break;
}

Choose a reason for hiding this comment

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

오호 이렇게 navigate 하는 군요

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

예 맞습니다 별거없습니다 ㅋㅋ

case 'ROOM_POST_DETAIL':
case 'ROOM_RECORD_DETAIL':
case 'ROOM_VOTE_DETAIL': {
case 'ROOM_POST_DETAIL': {

Choose a reason for hiding this comment

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

하나로 통일하니 깔끔하네요

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: 1

📜 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 0cc35c7 and 3c3367c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • package.json (1 hunks)
  • src/lib/ga.ts (2 hunks)
  • src/main.tsx (1 hunks)
🔇 Additional comments (2)
package.json (1)

25-25: react-ga4 의존성 추가 확인 완료

GA 초기화 로직과 연동되는 필수 의존성이며 다른 충돌 요소도 없어 보입니다.

src/main.tsx (1)

4-6: 초기 페이지뷰 전송 여부 확인 필요

Line 4에서 기존 sendPageView(...) 호출이 제거되었는데, initGA() 내부에서 최초 페이지뷰 전송까지 보장되는지 점검 부탁드립니다. 누락 시 첫 진입 세션 데이터가 빠질 수 있으니, ReactGA.send({ hitType: 'pageview', ... }) 등에 해당하는 처리가 포함돼 있는지 확인해 주세요.

@heeeeyong heeeeyong merged commit 3cb24d0 into develop Sep 26, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 BugFix Something isn't working 📃 Docs Improvements or additions to documentation ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants