Skip to content

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

Merged
heeeeyong merged 8 commits into
developfrom
chore/minor-updates
Sep 22, 2025
Merged

[FIX] 9월 3주차 QA 사항 - 희용#253
heeeeyong merged 8 commits into
developfrom
chore/minor-updates

Conversation

@heeeeyong
Copy link
Copy Markdown
Collaborator

@heeeeyong heeeeyong commented Sep 22, 2025

#️⃣연관된 이슈

없음

📝작업 내용

  • 회원탈퇴 API 연결
  • 소셜로그인 시, 기존회원 or 신규회원에 따라 임시토큰, 엑세스토큰 분기처리 : 임시토큰의 경우 특정 URL 가 아닌경우 파기되도록 수정하여 회원가입을 완료하지 않고 이탈한 신규 사용자가 다시 소셜로그인을 시도했을 때 기존유저로 인식되던 문제를 해결함.
  • 완료된 모임방 조회 기능 추가 및 style 수정 : 완료된 모임방의 컴포넌트를 눌렀을 때, 해당 모임방 내부로 리다이렉트하게끔 수정. 컴포넌트가 하나일 때에도 grid가 그대로 적용되는 문제를 해결

    image
  • 알림센터 조회 API 연결 : 우선 서버에서 구현한 알림센터 조회 API만 연결한 상태. 추후에 특정 알림내역 클릭 시, 리다이렉트 로직 추가 예정.

    image

💬리뷰 요구사항

없음

Summary by CodeRabbit

  • New Features
    • 계정 탈퇴 기능 추가 및 성공/실패 스낵바 안내.
    • 알림 목록 API 연동, 탭별 무한 스크롤, 읽음 상태 표시.
    • 헤더에 알림 버튼 추가(피드/그룹), 완료 그룹 카드 클릭 시 상세 이동.
  • Refactor
    • 알림 화면을 목데이터에서 실제 API 기반으로 전환.
    • 인증 흐름 개선: 신규 사용자 임시 토큰과 기존 사용자 액세스 토큰 분리 처리.
  • Style
    • 탈퇴 안내 문구·레이아웃 개선 및 경고 강조 스타일 추가.
    • 완료 모달 그리드/마진 조정, 만료 항목 데드라인 표시 숨김.

@heeeeyong heeeeyong self-assigned this Sep 22, 2025
@heeeeyong heeeeyong added ✨ Feature 기능 개발 📬 API 서버 API 통신 labels Sep 22, 2025
@vercel
Copy link
Copy Markdown

vercel Bot commented Sep 22, 2025

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

Project Deployment Preview Comments Updated (UTC)
thip Error Error Sep 22, 2025 8:12am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 22, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

신규 API 모듈 추가(사용자 삭제, 알림 조회), 인증 토큰 흐름 개편(preAuthToken/Signup 경로 허용), 토큰 응답 스키마 확장(isNewUser), 소셜 로그인 훅/회원가입 페이지 토큰 처리 업데이트, 마이페이지 탈퇴 플로우 구현, 알림 페이지를 API/무한스크롤로 전환, 헤더 우측 액션(공지 이동) 추가, 그룹 컴포넌트 조건 렌더링/타입 확장.

Changes

Cohort / File(s) Summary
API: 사용자/알림/토큰 스키마
src/api/users/deleteUsers.ts, src/api/notifications/getNotifications.ts, src/api/auth/getToken.ts
사용자 삭제 API 모듈 신설; 알림 조회 API 모듈(타입/파라미터/응답) 신설; GetTokenResponse.data에 isNewUser: boolean 필드 추가.
API 클라이언트 인터셉터
src/api/index.ts
요청 인터셉터 재구성: 공개 경로/회원가입 경로 식별, preAuthToken 허용, 그 외는 authToken 필요. 토큰 부재 시 홈으로 리다이렉트 및 요청 거부. 401 응답 시 리다이렉트 유지.
소셜 로그인/회원가입 토큰 흐름
src/hooks/useSocialLoginToken.ts, src/pages/signup/SignupNickname.tsx, src/pages/signup/SignupGenre.tsx
토큰 교환 시 isNewUser에 따라 preAuthToken 또는 authToken 저장/정리. 닉네임 단계에서 preAuthToken 검사로 변경. 회원가입 완료 시 preAuthToken 제거.
탈퇴 플로우
src/pages/mypage/WithdrawPage.tsx
deleteUsers 호출 연동: 성공 시 토큰 제거/완료 페이지 이동, 실패 시 서버 메시지 스낵바 표시. 확인 팝업/스낵바 사용 갱신 및 문구/레이아웃 조정.
알림 페이지 리팩터링
src/pages/notice/Notice.tsx
API 기반 데이터로 전환, 타입 적용, 커서 기반 페이지네이션/무한스크롤 구현, 탭 전환 시 재로딩, 렌더링 필드 교체(isChecked, notificationType, postDate, content).
헤더 우측 액션(공지 이동)
src/pages/feed/Feed.tsx, src/pages/group/Group.tsx, src/components/common/MainHeader...
rightButtonClick 사용 추가 및 핸들러 구현(공지 페이지로 이동).
그룹 컴포넌트/모달
src/components/group/CompletedGroupModal.tsx, src/components/group/GroupCard.tsx, src/components/group/MyGroupBox.tsx
Group 타입에 type?: string 추가. CompletedGroupModal에서 그룹 변환 시 type 매핑 및 카드 클릭 시 상세로 네비게이션. GroupCard에서 type==='modal' && group.type==='expired'일 때 마감 표시 비활성.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI as UI
  participant Hook as useSocialLoginToken
  participant API as apiClient (interceptor)
  participant Auth as GET /auth/token
  Note over Hook: 소셜 로그인 콜백 처리
  UI->>Hook: waitForToken()
  Hook->>Auth: 토큰 요청
  Auth-->>Hook: { token, isNewUser }
  alt isNewUser == true
    Hook->>Hook: localStorage.preAuthToken=token<br/>remove authToken
  else isNewUser == false
    Hook->>Hook: localStorage.authToken=token<br/>remove preAuthToken
  end
Loading
sequenceDiagram
  autonumber
  participant Page as Signup*
  participant API as apiClient (interceptor)
  Note over Page,API: 회원가입 경로 요청 흐름
  Page->>API: 보호된 회원가입 경로 호출
  API->>API: isSignupPath() == true ?
  alt true
    API->>API: Authorization: Bearer preAuthToken
  else false
    API->>API: Authorization: Bearer authToken (필요)
  end
  Note over Page: 회원가입 완료 시<br/>preAuthToken 제거
Loading
sequenceDiagram
  autonumber
  participant User as User
  participant WP as WithdrawPage
  participant Del as DELETE /users
  participant Nav as Router
  User->>WP: 탈퇴 확인
  WP->>Del: deleteUsers()
  alt isSuccess
    WP->>WP: localStorage.authToken 제거
    WP->>Nav: /mypage/withdraw/done 이동
  else 실패
    WP-->>User: 스낵바(서버 메시지)
  end
Loading
sequenceDiagram
  autonumber
  participant User as User
  participant Notice as Notice Page
  participant Get as GET /notifications
  participant Obs as IntersectionObserver
  User->>Notice: 페이지 진입/탭 전환
  Notice->>Get: getNotifications(cursor?)
  Get-->>Notice: { notifications, nextCursor, isLast }
  loop 스크롤
    Obs-->>Notice: sentinel 진입
    alt !isLast && !isLoading
      Notice->>Get: getNotifications(nextCursor)
      Get-->>Notice: 다음 페이지
    end
  end
Loading
sequenceDiagram
  autonumber
  participant Feed as Feed/Group Page
  participant Header as MainHeader
  participant Router as Router
  Feed->>Header: rightButtonClick 전달
  User->>Header: 우측 버튼 클릭
  Header->>Router: /notice 이동
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

새싹 토큰 한 잎, 깡총 뛰어 preAuth 품에
가입 피니시- 점프! auth로 살짝 갈아타네
알림은 줄지어 꼬리를 물고, 스크롤 끝에 춤추고
탈퇴는 조용히 인사하고, 길을 비워 주네
오늘도 코드밭에 바람- (•̀ᴗ•́)و 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed 제목 "[FIX] 9월 3주차 QA 사항 - 희용"은 PR 설명과 변경 내역(회원탈퇴 API 연결, 완료된 모임 조회 및 스타일 수정, 알림센터 연동 등)과 관련된 QA 수정임을 간결하게 나타내며 불필요한 파일 목록이나 이모지 없이 짧고 명확합니다. 전체 변경사항을 하나로 묶어 표현하고 있어 팀원이 히스토리를 빠르게 파악하기에도 무리가 없습니다. 다만 개별 핵심 변경을 즉시 파악하려는 경우에는 세부가 부족할 수 있습니다.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

📜 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 8ca08c9 and bf0eb68.

📒 Files selected for processing (2)
  • src/components/group/MyGroupBox.tsx (1 hunks)
  • src/pages/group/Group.tsx (2 hunks)

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
Copy Markdown

@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

Caution

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

⚠️ Outside diff range comments (2)
src/pages/mypage/WithdrawPage.tsx (2)

86-90: 키보드 접근 불가(체크박스): role/ARIA/키 핸들러 필요

현재 커스텀 체크박스는 div 클릭만 지원하여 키보드로 조작 불가. WCAG 2.1 키보드 접근성 충족을 위해 role/aria-checked/tabIndex/Space·Enter 핸들러 및 장식 아이콘 alt 빈값이 필요합니다.

-          <CheckboxContainer onClick={handleCheckboxChange}>
+          <CheckboxContainer
+            onClick={handleCheckboxChange}
+            role="checkbox"
+            aria-checked={isChecked}
+            tabIndex={0}
+            onKeyDown={e => {
+              if (e.key === ' ' || e.key === 'Enter') {
+                e.preventDefault();
+                handleCheckboxChange();
+              }
+            }}
+          >
             <CheckLabel>주의사항을 이해하였으며 이에 동의합니다.</CheckLabel>
-            <Checkbox checked={isChecked}>{isChecked && <img src={check} />}</Checkbox>
+            <Checkbox checked={isChecked}>{isChecked && <img src={check} alt="" />}</Checkbox>
           </CheckboxContainer>

Also applies to: 88-88


92-96: 버튼은 button으로, disabled 속성 사용(접근성/의도 전달)

현재 클릭 가능한 div는 스크린리더/키보드에 비표준. 시맨틱 button과 disabled를 사용하세요.

-      <WithdrawButton isActive={isChecked} onClick={handleWithdraw}>
+      <WithdrawButton type="button" disabled={!isChecked} onClick={handleWithdraw} aria-disabled={!isChecked}>
         <img src={withdraw} alt="회원탈퇴" />
         <ButtonText>Thip 떠나기</ButtonText>
       </WithdrawButton>
-const WithdrawButton = styled.div<{ isActive: boolean }>`
+const WithdrawButton = styled.button`
   position: fixed;
   bottom: 0;
   left: 0;
   right: 0;
   display: flex;
   flex-direction: row;
   align-items: center;
   justify-content: center;
   margin: 0 auto;
   gap: 8px;
   width: 100%;
   max-width: 767px;
   min-width: 320px;
   height: 50px;
-  background-color: ${props => (props.isActive ? colors.purple.main : colors.grey[300])};
-  cursor: ${props => (props.isActive ? 'pointer' : 'not-allowed')};
+  background-color: ${props => (props.disabled ? colors.grey[300] : colors.purple.main)};
+  cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
+  border: none;
+  outline: none;
   z-index: 2000;
`;

Also applies to: 184-202

🧹 Nitpick comments (7)
src/components/group/CompletedGroupModal.tsx (1)

37-39: 내비게이션 시 모달 잔존 가능성 — 닫기 처리 권장

라우팅만 수행하면 부모가 모달을 즉시 언마운트하지 않는 경우 오버레이가 남을 수 있습니다. 클릭 시 모달도 함께 닫도록 처리하는 편이 안전합니다.

적용 diff:

-  const handleGroupCardClick = (group: Group) => {
-    navigate(`/group/detail/joined/${group.id}`);
-  };
+  const handleGroupCardClick = (group: Group) => {
+    navigate(`/group/detail/joined/${group.id}`);
+    onClose();
+  };

추가로, 접근성/SEO가 중요하다면 GroupCard 쪽에서 Link 컴포넌트를 지원하도록 확장하는 것도 고려해 주세요.

src/api/users/deleteUsers.ts (2)

3-8: API 응답 계약 재확인: data를 null 고정 대신 optional로 열어두는 것이 안전

현재 data: null로 고정되어 있어 서버가 추후 페이로드를 포함시키면 타입 불일치가 발생합니다. 계약이 “항상 null”이 아니라면 optional/unknown으로 두는 편이 변경에 더 유연합니다.

 export interface DeleteUsersResponse {
   isSuccess: boolean;
   code: number;
   message: string;
-  data: null;
+  data?: unknown; // 현재는 null이나, 서버 변경에 대비
 }

10-13: 요청 취소(AbortSignal) 지원으로 UX/회수성 개선

확인 모달 닫힘/페이지 이탈 시 DELETE 요청을 취소할 수 있도록 AbortSignal을 받도록 해두면 안전합니다. (Axios 최신 버전은 signal 지원)

-export const deleteUsers = async (): Promise<DeleteUsersResponse> => {
-  const response = await apiClient.delete<DeleteUsersResponse>('/users');
+export const deleteUsers = async (signal?: AbortSignal): Promise<DeleteUsersResponse> => {
+  const response = await apiClient.delete<DeleteUsersResponse>('/users', { signal });
   return response.data;
 };
src/pages/mypage/WithdrawPage.tsx (4)

31-59: 확인 핸들러 정리: 중복 close 제거, 네비게이션 replace, 요청 취소 지원

  • IIFE 내부에서 중복 closePopup() 호출 대신 finally에서 1회만 호출.
  • 완료 페이지 이동 시 replace: true로 히스토리 오염 방지.
  • deleteUsersAbortSignal 전달(상단 API 수정 반영).
-          void (async () => {
-            try {
-              const response = await deleteUsers();
-              if (response.isSuccess) {
-                closePopup();
-                navigate('/mypage/withdraw/done');
-                localStorage.removeItem('authToken');
-              } else {
-                closePopup();
-                openSnackbar({
-                  message: response.message,
-                  variant: 'top',
-                  onClose: () => {},
-                });
-              }
-            } catch (error) {
-              let serverMessage = '요청 처리 중 오류가 발생했어요.';
-              if (error && typeof error === 'object' && 'response' in error) {
-                const axiosError = error as { response?: { data?: { message?: string } } };
-                serverMessage = axiosError.response?.data?.message || serverMessage;
-              }
-              closePopup();
-              openSnackbar({
-                message: serverMessage,
-                variant: 'top',
-                onClose: () => {},
-              });
-            }
-          })();
+          (async () => {
+            const controller = new AbortController();
+            try {
+              const response = await deleteUsers(controller.signal);
+              if (response.isSuccess) {
+                localStorage.removeItem('authToken');
+                navigate('/mypage/withdraw/done', { replace: true });
+              } else {
+                openSnackbar({
+                  message: response.message,
+                  variant: 'top',
+                });
+              }
+            } catch (error) {
+              let serverMessage = '요청 처리 중 오류가 발생했어요.';
+              if (error && typeof error === 'object' && 'response' in error) {
+                const axiosError = error as { response?: { data?: { message?: string }; status?: number } };
+                serverMessage = axiosError.response?.data?.message || serverMessage;
+              }
+              openSnackbar({
+                message: serverMessage,
+                variant: 'top',
+              });
+            } finally {
+              closePopup();
+            }
+          })();

120-121: iOS 하단 안전영역 대응

고정 하단 버튼과 겹침 방지를 위해 safe-area inset을 고려하면 좋습니다.

-  padding: 40px 20px 105px 20px;
+  padding: 40px 20px calc(105px + env(safe-area-inset-bottom)) 20px;

79-84: 개인정보/보존기간 문구 정책 일치 여부 확인

“즉시 삭제/최대 90일 내 자동 삭제/법정 보존” 안내는 법무·개인정보처리방침과 정확히 일치해야 합니다. 실제 백업/로그 보존 기간, 예외(분쟁 대비 등)와 불일치 시 컴플라이언스 리스크가 큽니다.

필요 시 정책 문구에 맞춰 텍스트/링크(개인정보처리방침) 반영안 제안 가능합니다.


145-147: 색상 하드코딩 지양 → 디자인 토큰 사용 권장

#ff9496 대신 글로벌 팔레트 토큰을 사용해 테마 일관성과 다크모드/브랜딩 변경 대응성을 확보하세요.

📜 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 dead57d and 6799046.

📒 Files selected for processing (3)
  • src/api/users/deleteUsers.ts (1 hunks)
  • src/components/group/CompletedGroupModal.tsx (3 hunks)
  • src/pages/mypage/WithdrawPage.tsx (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/api/users/deleteUsers.ts (1)
src/api/index.ts (1)
  • apiClient (7-13)
src/components/group/CompletedGroupModal.tsx (2)
src/components/group/MyGroupBox.tsx (1)
  • Group (11-23)
src/components/group/GroupCard.tsx (1)
  • GroupCard (18-52)
src/pages/mypage/WithdrawPage.tsx (3)
src/hooks/usePopupActions.ts (1)
  • usePopupActions (9-35)
src/api/users/deleteUsers.ts (1)
  • deleteUsers (10-13)
src/styles/global/global.ts (1)
  • typography (56-77)
🔇 Additional comments (7)
src/components/group/CompletedGroupModal.tsx (2)

18-18: LGTM

라우터 훅 초기화 적절합니다.


11-11: 라우트 정의/버전 확인 필요

  • package.json에서 react-router-dom: ^7.6.0 확인 — v6 아님; useNavigate 사용은 v7에서도 적절합니다.
  • 레포 검색에서 "/group/detail/joined" 또는 path="/group/detail/joined/:id"를 찾지 못했습니다(검색 도구가 파일을 건너뜀). 해당 라우트가 정의된 파일 경로를 알려주시거나 라우트 정의를 확인해 주세요.
src/api/users/deleteUsers.ts (1)

1-1: LGTM – apiClient 경로/모듈 사용 적절

상대 경로 ../index 임포트 정상이며, 생성된 클라이언트와의 결합도도 적절합니다.

src/pages/mypage/WithdrawPage.tsx (4)

10-10: LGTM – 신규 API 임포트 위치/경로 적절


14-14: LGTM – usePopupActions에서 openSnackbar 포함한 비구조화 사용 적절


179-179: LGTM – 본문 대비 작은 라벨 폰트사이즈(sm) 적용 적절

가독성과 위계에 맞는 선택입니다.


33-38: 토큰 정리 범위 추가 확인 요망: localStorage.removeItem 외에도 axios 인터셉터, 메모리 캐시 및 전역 상태 관리(Zustand/Redux 등)에서 토큰 참조 제거가 필요한지 직접 검토해주세요.

Comment on lines +93 to +100
convertedGroups.map(group => (
<GroupCard
key={group.id}
group={group}
type={'modal'}
onClick={() => handleGroupCardClick(group)}
/>
))
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

완료된 방 라벨 오표기 가능성 — isOngoing 전달 필요

GroupCardisOngoing === true일 때 ‘종료’를, 그렇지 않으면 ‘모집 마감’을 표시합니다. 현재 prop을 전달하지 않아 완료된 방이 ‘모집 마감’으로 보일 수 있습니다. 완료된 목록에서는 isOngoing={true}를 넘겨 ‘종료’가 노출되도록 해주세요.

적용 diff:

               <GroupCard
                 key={group.id}
                 group={group}
                 type={'modal'}
-                onClick={() => handleGroupCardClick(group)}
+                isOngoing={true}
+                onClick={() => handleGroupCardClick(group)}
               />

참고: convertRoomToGroup에서 isOnGoing(O 대문자) 필드를 세팅하지만 GroupCardisOngoing(o 소문자) prop을 받습니다. 네이밍 불일치로 혼동될 수 있으니 통일을 검토해 주세요.

📝 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
convertedGroups.map(group => (
<GroupCard
key={group.id}
group={group}
type={'modal'}
onClick={() => handleGroupCardClick(group)}
/>
))
convertedGroups.map(group => (
<GroupCard
key={group.id}
group={group}
type={'modal'}
isOngoing={true}
onClick={() => handleGroupCardClick(group)}
/>
))
🤖 Prompt for AI Agents
In src/components/group/CompletedGroupModal.tsx around lines 93 to 100, the
GroupCard entries are missing the isOngoing prop so completed rooms may display
the wrong label; pass isOngoing={true} (or the appropriate boolean from the
converted object) to each GroupCard via its JSX (e.g., isOngoing={true} for
completed list) to force the “종료” label, and also align naming by checking
convertRoomToGroup which sets isOnGoing (capital G) — rename or map that field
to isOngoing (lowercase g) to avoid future confusion.

Copy link
Copy Markdown

@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: 4

🧹 Nitpick comments (9)
src/pages/signup/SignupNickname.tsx (1)

34-43: preAuthToken 미존재 시 UX 보완: authToken 보유자 분기 및 리디렉션 제안

신규 가입 플로우에서는 preAuthToken 체크가 맞습니다. 다만 사용자가 이미 로그인이 된 상태(authToken 존재)로 이 화면에 진입한 경우, 현재는 “인증 토큰이 없습니다”로 보여 혼란할 수 있습니다. authToken 존재 시 홈/피드로 보내거나, 메시지를 분기하는 쪽이 UX에 낫습니다.

-      const preAuthToken = localStorage.getItem('preAuthToken');
-      if (!preAuthToken) {
-        console.log('❌ 임시 토큰이 없어 닉네임 검증을 할 수 없습니다.');
-        setError('인증 토큰이 없습니다. 다시 시도해주세요.');
-        return;
-      }
+      const preAuthToken = localStorage.getItem('preAuthToken');
+      const authToken = localStorage.getItem('authToken');
+      if (!preAuthToken) {
+        if (authToken) {
+          console.log('ℹ️ 이미 로그인된 사용자로 판단되어 홈으로 이동합니다.');
+          navigate('/');
+          return;
+        }
+        console.log('❌ 임시 토큰이 없어 닉네임 검증을 할 수 없습니다.');
+        setError('인증 토큰이 없습니다. 다시 로그인해주세요.');
+        return;
+      }
src/api/index.ts (2)

20-25: 경로 매칭 startsWith는 오탑재 위험 — 경계 인식 매칭으로 보강 제안

/auth/tokenize 같은 경로가 /auth/token에 오인 매칭될 수 있습니다. 경계(끝, 슬래시, 쿼리) 기반 매칭으로 보강하세요.

-    const isPublic = publicPaths.some(path => config.url?.startsWith(path));
-    const isSignupPath = signupPaths.some(path => config.url?.startsWith(path));
+    const url = config.url ?? '';
+    const matches = (base: string) =>
+      url === base || url.startsWith(`${base}/`) || url.startsWith(`${base}?`);
+    const isPublic = publicPaths.some(matches);
+    const isSignupPath = signupPaths.some(matches);

26-31: 요청 전 리다이렉트+취소 정책: 사용자 경험/에러 처리 영향 점검

사전 차단은 네트워크 낭비를 줄이지만, 호출자 단에서 에러 유형 식별이 어려워집니다(일반 Error). 필요 시 식별 가능한 에러 클래스로 거절하거나, 화면 단에서 ‘인증 만료’ 등으로 명확히 안내되는지 확인해 주세요.

-      // 요청 자체를 취소하여 불필요한 네트워크 왕복 방지
-      return Promise.reject(new Error('Request cancelled: missing auth token'));
+      // 호출부에서 구분 가능한 에러 메시지/코드 제공
+      const err = new Error('AUTH_MISSING: Request cancelled before send');
+      // (선택) err.name = 'AuthMissingError';
+      return Promise.reject(err);
src/api/notifications/getNotifications.ts (2)

16-21: nextCursor 타입을 null 허용으로 수정 필요

응답을 사용하는 측에서 res.data.nextCursor || null로 처리하고 있어 API 스키마도 string | null이 일관됩니다.

   data: {
     notifications: NotificationItem[];
-    nextCursor: string;
+    nextCursor: string | null;
     isLast: boolean;
   };

23-26: 요청 파라미터에서 cursor는 ‘없음’과 ‘null’을 구분하세요

첫 페이지 로드시 파라미터 자체를 생략하는 편이 안전합니다. 타입도 null 대신 생략 가능(옵셔널)로 두세요.

 export interface GetNotificationsParams {
-  cursor?: string | null;
+  cursor?: string; // 첫 페이지일 때는 생략
   type?: 'feed' | 'room';
 }
src/hooks/useSocialLoginToken.ts (3)

2-7: Router 상태와 동기화: window.history 대신 useNavigate로 URL 정리

window.history.replaceState는 React Router 위치 상태를 갱신하지 않아 불일치가 생길 수 있습니다. useNavigate로 대체하세요.

-import { useLocation } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom';
@@
   const location = useLocation();
+  const navigate = useNavigate();
@@
-          const newUrl = window.location.pathname;
-          window.history.replaceState({}, document.title, newUrl);
+          // Router 상태와 브라우저 URL을 함께 갱신
+          navigate(location.pathname, { replace: true });

Also applies to: 45-47


66-70: 오류 전달 선택지: waitForToken이 성공/실패를 구분하도록 개선 제안

현재 waitForToken은 실패해도 단순 resolve됩니다. 호출 측 분기 처리가 필요하면 성공 boolean 반환 또는 실패 throw로 신호를 주는 것이 안전합니다. 호환성 이슈가 있으면 새 메서드(예: waitForTokenOrThrow)로 병행 제공을 고려하세요.


11-21: 중복 호출 방지 강화(선택): 동일 키로 재실행되는 경우 가드

이미 발급 중이면 기존 tokenPromise.current를 재사용하도록 가드하면 네트워크 중복 요청을 줄일 수 있습니다.

   useEffect(() => {
     const handleSocialLoginToken = async (): Promise<void> => {
@@
       const loginTokenKey = params.get('loginTokenKey');
@@
-    if (isSocialLoginComplete) {
-      // 토큰 발급 Promise를 저장
-      tokenPromise.current = handleSocialLoginToken();
-    }
+    if (isSocialLoginComplete) {
+      // 이미 진행 중이면 재사용
+      tokenPromise.current ??= handleSocialLoginToken();
+    }

Also applies to: 59-63

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

119-124: 타입값 직접 노출 대신 사용자 친화 라벨 매핑 제안(선택)

notificationType이 'feed'/'room' 등 내부 코드라면 뱃지에 노출 시 라벨 매핑을 권장합니다.

-                <Badge>{notif.notificationType}</Badge>
+                <Badge>{notif.notificationType === 'feed' ? '피드' : notif.notificationType === 'room' ? '모임' : notif.notificationType}</Badge>
📜 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 6799046 and f7f1e7f.

📒 Files selected for processing (12)
  • src/api/auth/getToken.ts (1 hunks)
  • src/api/index.ts (1 hunks)
  • src/api/notifications/getNotifications.ts (1 hunks)
  • src/components/group/CompletedGroupModal.tsx (5 hunks)
  • src/components/group/GroupCard.tsx (1 hunks)
  • src/components/group/MyGroupBox.tsx (1 hunks)
  • src/hooks/useSocialLoginToken.ts (2 hunks)
  • src/pages/feed/Feed.tsx (2 hunks)
  • src/pages/group/Group.tsx (1 hunks)
  • src/pages/notice/Notice.tsx (6 hunks)
  • src/pages/signup/SignupGenre.tsx (1 hunks)
  • src/pages/signup/SignupNickname.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/group/CompletedGroupModal.tsx
🧰 Additional context used
🧬 Code graph analysis (4)
src/api/notifications/getNotifications.ts (1)
src/api/index.ts (1)
  • apiClient (7-13)
src/pages/group/Group.tsx (1)
src/components/group/MyGroupModal.tsx (1)
  • MyGroupModal (16-253)
src/hooks/useSocialLoginToken.ts (1)
src/api/auth/getToken.ts (1)
  • getToken (17-20)
src/pages/notice/Notice.tsx (2)
src/api/notifications/getNotifications.ts (2)
  • NotificationItem (3-10)
  • getNotifications (28-35)
src/styles/global/global.ts (2)
  • typography (56-77)
  • colors (4-53)
🔇 Additional comments (11)
src/pages/feed/Feed.tsx (2)

56-59: 알림 페이지 네비게이션 핸들러 추가 적합

단순 라우팅으로 부작용 없고 명확합니다.


182-186: MainHeader: rightButtonClick 프롭 확인 — 조치 불필요

src/components/common/MainHeader.tsx에 rightButtonClick?: () => void로 선언되어 있으며 IconButton의 onClick에 사용됩니다. Feed.tsx와 Group.tsx에서 정상 전달되고 있습니다.

src/pages/group/Group.tsx (2)

86-89: 알림 페이지 네비게이션 핸들러 추가 적합

피드와 동일한 패턴으로 일관성 좋습니다.


94-98: MainHeader 우측 액션 연결 — 확인 완료

MainHeaderProps에 rightButtonClick이 정의되어 있고 컴포넌트 내부에서 IconButton onClick={rightButtonClick}로 렌더되므로 type="group"에서도 우측 버튼이 정상 동작합니다.

src/api/index.ts (1)

35-38: 토큰 주입 우선순위 적절 — signup 경로 preAuthToken 사용 여부 검증 필요

authToken 우선, 없으면 signup 경로에서만 preAuthToken을 사용하는 로직은 적절합니다. 다만 리포지토리 검색이 결과를 반환하지 않아 signupPaths가 가입 플로우의 모든 엔드포인트를 포괄하는지 확인할 수 없습니다. src/api/index.ts (35–38)에서 signupPaths에 누락된 엔드포인트가 없는지 검증하세요.

src/components/group/GroupCard.tsx (1)

38-47: 검증 결과 — CompletedGroupModal은 이미 group.type을 설정합니다; 조건 가독성 개선만 권장

  • src/components/group/CompletedGroupModal.tsx의 convertRoomToGroup에 type: room.type이 있습니다(줄 34) — 'type 누락' 지적은 부정확합니다.
  • 권장: src/components/group/GroupCard.tsx의 조건을 가독성 있게 변경하세요. 예: {!(type === 'modal' && group.type === 'expired') && ...}

Likely an incorrect or invalid review comment.

src/api/auth/getToken.ts (1)

12-14: isNewUser 필드 소비 확인 — 다운스트림 전파 완료

useSocialLoginToken 훅에서 isNewUser에 따라 임시 토큰(preAuthToken) / 액세스 토큰(authToken) 분기 저장이 구현되어 있으며, 회원가입 페이지에서도 가입 성공 시 accessToken을 authToken으로 저장하도록 구현되어 있습니다.

  • src/hooks/useSocialLoginToken.ts (약 라인 30–41): isNewUser 분기 및 localStorage set/remove 처리 확인.
  • src/pages/signup/SignupGenre.tsx (약 라인 65–69): 회원가입 성공 시 authToken 저장 로직 확인.
src/pages/signup/SignupGenre.tsx (1)

66-71: 임시 토큰 정리 타이밍 적절 — 이후 요청이 authToken을 사용하도록 확인됨

검증: src/api/index.ts의 signupPaths는 ['/users/nickname','/users/signup']로 '/signup/guide' 등 클라이언트 라우트는 포함되지 않음. src/pages/signup/SignupGenre.tsx에서 accessToken을 localStorage에 저장한 직후 preAuthToken을 제거(약 66–71행)하므로 이후 인터셉터는 authToken을 사용함.

src/components/group/MyGroupBox.tsx (1)

23-24: Group 타입에 type?: string 추가 — 검증 완료

검증 결과: 모달에서 'expired' 체크를 사용하는 곳은 src/components/group/GroupCard.tsx이고, expired 값을 필요로 하는 변환기는 src/components/group/CompletedGroupModal.tsx에서 type: room.type으로 설정되어 있어 문제 없음. MyGroupModal/GroupDetail의 변환기는 modal에 전달되는 경우에도 expired를 전달할 가능성이 낮아 추가 조치 불필요.

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

113-124: 뷰 로직은 깔끔합니다 — 키/읽음 상태 매핑 LGTM

keynotificationId ?? idx, read={notif.isChecked}로 매핑한 부분은 안정적으로 보입니다. 이후 서버에서 항상 고유 ID를 보장하면 ?? idx는 제거해도 됩니다.

src/hooks/useSocialLoginToken.ts (1)

1-74: 민감 로그(토큰/키) 잔존 여부 — 자동 검사 실패, 수동 확인 필요

rg 실행 결과 "No files were searched"가 반환되어 레포 전체 자동 스캔이 실행되지 않았습니다. 로컬에서 아래 명령으로 재검증하세요.

# 레포 루트에서 실행
grep -nR --exclude-dir={.git,node_modules,dist,public} -E "console\.(log|info|debug)\s*\(|logger\.(log|info|debug)\s*\(|authToken|preAuthToken|loginTokenKey|authorization|token" .
  • PR 파일 src/hooks/useSocialLoginToken.ts에 loginTokenKey를 console.log로 출력하는 코드가 있습니다 — 해당 로그 제거 또는 민감값 마스킹 필요.

Comment on lines +31 to +35
const response = await apiClient.get<GetNotificationsResponse>('/notifications', {
params,
});
return response.data;
};
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

null 파라미터 전송 차단: cursor=null 이 쿼리스트링에 노출될 수 있습니다

Axios가 객체를 직렬화할 때 null이 문자열 "null"로 전송될 수 있어 서버 필터링/캐싱에 악영향을 줄 수 있습니다. null/빈값은 아예 파라미터에서 제거하세요.

다음처럼 쿼리 객체를 정제해 전송하십시오:

 export const getNotifications = async (
   params?: GetNotificationsParams,
 ): Promise<GetNotificationsResponse> => {
-  const response = await apiClient.get<GetNotificationsResponse>('/notifications', {
-    params,
-  });
+  const { cursor, type } = params ?? {};
+  const query: Record<string, string> = {};
+  if (cursor) query.cursor = cursor;
+  if (type) query.type = type;
+  const response = await apiClient.get<GetNotificationsResponse>('/notifications', {
+    params: query,
+  });
   return response.data;
 };
📝 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 response = await apiClient.get<GetNotificationsResponse>('/notifications', {
params,
});
return response.data;
};
export const getNotifications = async (
params?: GetNotificationsParams,
): Promise<GetNotificationsResponse> => {
const { cursor, type } = params ?? {};
const query: Record<string, string> = {};
if (cursor) query.cursor = cursor;
if (type) query.type = type;
const response = await apiClient.get<GetNotificationsResponse>('/notifications', {
params: query,
});
return response.data;
};
🤖 Prompt for AI Agents
In src/api/notifications/getNotifications.ts around lines 31 to 35, the current
axios call may serialize null values like cursor=null into the query string;
remove null/undefined/empty parameters before sending by creating a cleaned
params object (e.g., filter Object.entries(params) to exclude values that are
null or undefined or empty string) and pass that cleanedParams to apiClient.get
instead of the raw params so nulls are never included in the query string.

Comment on lines 23 to 25
console.log('🔑 소셜 로그인 토큰 발급 요청');
console.log('📋 loginTokenKey:', loginTokenKey);

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

민감 정보 로그 노출: loginTokenKey 콘솔 출력 제거

loginTokenKey는 인증 토큰 교환 키로 간주되어야 하며 콘솔/로그에 남기면 위험합니다. 즉시 제거하세요.

   try {
     console.log('🔑 소셜 로그인 토큰 발급 요청');
-    console.log('📋 loginTokenKey:', loginTokenKey);
📝 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
console.log('🔑 소셜 로그인 토큰 발급 요청');
console.log('📋 loginTokenKey:', loginTokenKey);
console.log('🔑 소셜 로그인 토큰 발급 요청');
🤖 Prompt for AI Agents
In src/hooks/useSocialLoginToken.ts around lines 23 to 25, remove the
console.log that prints the sensitive loginTokenKey; do not output the raw token
to logs. Replace it with a non-sensitive message (e.g., "social login token
request initiated") or, if you must indicate presence, log a boolean or masked
value (e.g., show length or first/last char only) and use the application logger
with redaction/secure logging enabled. Ensure no other code paths print the full
token and add a brief comment noting that tokens must never be logged.

Comment on lines +33 to +36
const params: { cursor?: string | null; type?: 'feed' | 'room' } = { cursor };
if (selected === '피드') params.type = 'feed';
if (selected === '모임') params.type = 'room';

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

null 파라미터 전송 방지: 초기 로드에서 cursor는 생략해야 합니다

현재 { cursor }로 null을 그대로 전달할 수 있습니다. 파라미터를 조건부로 추가하세요. (API 레이어도 함께 수정 권장)

-        const params: { cursor?: string | null; type?: 'feed' | 'room' } = { cursor };
-        if (selected === '피드') params.type = 'feed';
-        if (selected === '모임') params.type = 'room';
+        const params: { cursor?: string; type?: 'feed' | 'room' } = {};
+        if (cursor) params.cursor = cursor;
+        if (selected === '피드') params.type = 'feed';
+        else if (selected === '모임') params.type = 'room';
📝 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 params: { cursor?: string | null; type?: 'feed' | 'room' } = { cursor };
if (selected === '피드') params.type = 'feed';
if (selected === '모임') params.type = 'room';
const params: { cursor?: string; type?: 'feed' | 'room' } = {};
if (cursor) params.cursor = cursor;
if (selected === '피드') params.type = 'feed';
else if (selected === '모임') params.type = 'room';
🤖 Prompt for AI Agents
In src/pages/notice/Notice.tsx around lines 33 to 36, the params object
currently always includes cursor which may be null on initial load; change to
only add cursor when it is non-null/defined (e.g., create params as an empty
object then set params.cursor = cursor only if cursor != null), keep the
conditional logic for type (set params.type = 'feed' or 'room' only when
selected matches), and consider making the API layer tolerant by not sending
null values or filtering them out before request serialization.

Comment on lines +61 to +79
useEffect(() => {
if (!sentinelRef.current) return;
const el = sentinelRef.current;
const observer = new IntersectionObserver(
entries => {
const entry = entries[0];
if (entry.isIntersecting && !isLoading && !isLast && nextCursor !== null) {
void loadNotifications(nextCursor);
}
},
{ root: null, rootMargin: '0px', threshold: 0.1 },
);
};

const filteredNotifications =
selected === '' ? notifications : notifications.filter(notif => notif.category === selected);
observer.observe(el);
return () => {
observer.unobserve(el);
observer.disconnect();
};
}, [isLoading, isLast, nextCursor, loadNotifications]);
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

무한 스크롤 센티넬 위치/루트 설정 오류로 스크롤이 트리거되지 않을 수 있습니다

현재 센티넬이 스크롤 컨테이너(NotificationList) 외부에 있어 리스트 스크롤로는 관찰 상태가 변하지 않습니다. 센티넬을 리스트 내부로 옮기고, IntersectionObserver의 root를 리스트 엘리먼트로 지정하세요.

@@
-  const isLoadingRef = useRef<boolean>(false);
-  const sentinelRef = useRef<HTMLDivElement | null>(null);
+  const isLoadingRef = useRef<boolean>(false);
+  const sentinelRef = useRef<HTMLDivElement | null>(null);
+  const listRef = useRef<HTMLDivElement | null>(null);
@@
-  useEffect(() => {
-    if (!sentinelRef.current) return;
-    const el = sentinelRef.current;
+  useEffect(() => {
+    if (!sentinelRef.current || !listRef.current) return;
+    const el = sentinelRef.current;
+    const rootEl = listRef.current;
     const observer = new IntersectionObserver(
       entries => {
         const entry = entries[0];
         if (entry.isIntersecting && !isLoading && !isLast && nextCursor !== null) {
           void loadNotifications(nextCursor);
         }
       },
-      { root: null, rootMargin: '0px', threshold: 0.1 },
+      { root: rootEl, rootMargin: '0px', threshold: 0.1 },
     );
@@
-      <NotificationList>
+      <NotificationList ref={listRef}>
@@
-          filteredNotifications.map((notif, idx) => (
+          filteredNotifications.map((notif, idx) => (
             <NotificationCard
               key={notif.notificationId ?? idx}
               read={notif.isChecked}
               // onClick={() => handleReadNotification(idx)}
             >
               {!notif.isChecked && <UnreadDot />}
               <TitleRow>
                 <Badge>{notif.notificationType}</Badge>
                 <Title>{notif.title}</Title>
                 <Time>{notif.postDate}</Time>
               </TitleRow>
               <Description>{notif.content}</Description>
             </NotificationCard>
           ))
         )}
-      </NotificationList>
-
-      {/* 무한 스크롤 감지용 센티넬 */}
-      <Sentinel ref={sentinelRef} />
+        {/* 무한 스크롤 감지용 센티넬 */}
+        <Sentinel ref={sentinelRef} />
+      </NotificationList>

Also applies to: 107-128, 129-131, 15-17

@heeeeyong heeeeyong merged commit 2115fed into develop Sep 22, 2025
1 of 3 checks passed
@heeeeyong heeeeyong added this to the 9월 3주차 QA milestone Sep 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📬 API 서버 API 통신 ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant