Skip to content

main 배포#194

Merged
ljh130334 merged 11 commits into
mainfrom
develop
Aug 21, 2025
Merged

main 배포#194
ljh130334 merged 11 commits into
mainfrom
develop

Conversation

@ljh130334
Copy link
Copy Markdown
Member

@ljh130334 ljh130334 commented Aug 21, 2025

#️⃣연관된 이슈

ex) #이슈번호, #이슈번호

📝작업 내용

이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능)

스크린샷 (선택)

💬리뷰 요구사항(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

Summary by CodeRabbit

  • New Features
    • 게시글 수정 시 남은 이미지 목록을 항상 전송하여 빈 배열로 이미지 삭제를 지원.
  • Bug Fixes
    • 방 접근 권한/모집기간 만료 시 상황별 안내 및 적절한 페이지로 자동 이동.
    • 그룹 멤버/참여 그룹 상세에서 권한 오류 시 목록 페이지로 이동 처리.
    • 내 기록의 고정 기능을 텍스트 기록에만 노출/동작하도록 정정.
  • Style
    • 생성 폼 섹션에 구분선 추가(여러 섹션에 적용).
    • 댓글·기록 섹션의 클릭 가능한 영역을 섹션 전체로 확대하고 포커스 시 아웃라인 추가.
    • 기본 활동 종료일을 오늘+2일로 조정.

@vercel
Copy link
Copy Markdown

vercel Bot commented Aug 21, 2025

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

Project Deployment Preview Comments Updated (UTC)
thip Ready Ready Preview Comment Aug 21, 2025 1:35am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 21, 2025

Walkthrough

rooms API의 catch 타입을 unknown으로 변경하고 AxiosError 및 응답 코드(100004, 140011)를 검사해 명시적 오류를 던지도록 수정했습니다. 관련 페이지들은 해당 메시지에 따라 네비게이션 리다이렉트를 추가했습니다. 그룹 생성 UI에 여러 구분선과 기본 종료일(+2일) 변경, 섹션 클릭 영역 확대, 게시글 업데이트에서 remainImageUrls 항상 포함 등이 적용되었습니다.

Changes

Cohort / File(s) Summary
Rooms API: 에러 타입·코드 처리
src/api/rooms/getRoomDetail.ts, src/api/rooms/getRoomMembers.ts, src/api/rooms/getRoomPlaying.ts
catch 매개변수 → unknown, AxiosError 검사 추가. 에러 코드 100004/140011에 대해 한국어 메시지로 Error throw; 그 외는 로그 후 재throw.
페이지: 에러 기반 리다이렉트
src/pages/groupDetail/GroupDetail.tsx, src/pages/groupDetail/ParticipatedGroupDetail.tsx, src/pages/groupMembers/GroupMembers.tsx
catch를 unknown으로 변경하고 로그 추가. 특정 오류 메시지에 따라 navigate로 리다이렉트(권한없음→/group, 모집기간만료→참여중 상세 등).
그룹 생성: UI 구분선 추가
src/components/creategroup/GenreSelectionSection.tsx, src/components/creategroup/MemberLimitSection.tsx, src/components/creategroup/PrivacySettingSection/PrivacySettingSection.tsx, src/components/creategroup/RoomInfoSection.tsx
내부에 Section showDivider 삽입하여 시각적 구분선 추가(로직/상태 변경 없음).
그룹 생성: 기본 종료일 변경
src/components/creategroup/ActivityPeriodSection/ActivityPeriodSection.tsx
getInitialEndDate 계산을 today+1 → today+2로 변경.
섹션 클릭 영역 확대(스타일/컴포넌트)
src/components/group/CommentSection.styled.ts, src/components/group/CommentSection.tsx, src/components/group/RecordSection.styled.ts, src/components/group/RecordSection.tsx
onClick 및 cursor: pointer를 헤더에서 컨테이너로 이동. 전체 섹션이 클릭 가능해짐; 포커스 스타일 추가.
메모리 레코드: 핀 동작 조건화
src/components/memory/RecordItem/RecordItem.tsx
내 레코드에서 type === 'text'인 경우에만 pin 옵션/버튼 노출 및 메뉴에 onPin 포함. handleClick deps에 type 추가.
게시글 업데이트: remainImageUrls 항상 전송
src/pages/post/UpdatePost.tsx
payload에 remainImageUrls를 조건 없이 포함(빈 배열 전송 시 서버의 삭제 처리 유도).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as 사용자
  participant Page as GroupDetail 페이지
  participant API as getRoomDetail()
  participant S as 서버

  U->>Page: 그룹 상세 진입
  Page->>API: 방 상세 조회 요청
  API->>S: GET /rooms/{id}
  S-->>API: 200 OK / 에러(code)
  alt 200 OK
    API-->>Page: data
    Page-->>U: 상세 렌더링
  else 에러 code=100004
    API-->>Page: Error("모집기간이 만료된 방입니다.")
    Page->>Page: navigate(`/group/detail/joined/${id}`, replace=true)
  else 에러 code=140011
    API-->>Page: Error("방 접근 권한이 없습니다.")
    Page->>Page: navigate('/group', replace=true)
  else 기타
    API-->>Page: 원본 에러 재throw
    Page-->>U: setError(...) 및 에러 표시
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • heeeeyong

Poem

토끼가 깡충, 코드에 점 하나 넣네 🥕
에러는 잡고, 구분선 쫙—눈에 띄게
클릭은 넓게, 핀은 글에만 몰래
빈 배열 툭, 서버야 정리해줘 친구야! ✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@ljh130334 ljh130334 added the 🌏 Deploy 배포 관련 label Aug 21, 2025
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: 10

Caution

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

⚠️ Outside diff range comments (3)
src/components/creategroup/ActivityPeriodSection/ActivityPeriodSection.tsx (2)

88-96: 기본 종료일 계산이 startDate를 고려하지 않아 자동 보정이 깨질 수 있습니다

현재 getInitialEndDate는 무조건 "오늘+2일"을 반환합니다. startDate가 오늘+2일보다 더 미래인 경우(예: 시작일이 10일 뒤) 종료일이 시작일보다 과거로 자동 보정될 수 있어 초기·재보정 로직이 깨집니다.

  • 재현 예: 시작일=9/10, 사용자가 종료일을 9/5로 선택 → 보정이 오늘+2일로 되는데, 여전히 시작일(9/10)보다 과거일 수 있습니다.
  • 기대: 최소 종료일은 max(오늘+2일, 시작일) 이어야 합니다.

아래처럼 기본 종료일을 startDate와 정책(오늘+2) 중 더 늦은 값으로 계산해주세요.

-  const getInitialEndDate = () => {
-    const tomorrow = new Date();
-    tomorrow.setDate(tomorrow.getDate() + 2);
-    return {
-      year: tomorrow.getFullYear(),
-      month: tomorrow.getMonth() + 1,
-      day: tomorrow.getDate(),
-    };
-  };
+  const getInitialEndDate = () => {
+    const today = new Date();
+    today.setHours(0, 0, 0, 0);
+
+    const policyMinEnd = new Date(today);
+    policyMinEnd.setDate(policyMinEnd.getDate() + 2); // 기본 정책: 오늘+2
+
+    const start = new Date(startDate.year, startDate.month - 1, startDate.day);
+    start.setHours(0, 0, 0, 0);
+
+    const minEnd = policyMinEnd.getTime() < start.getTime() ? start : policyMinEnd;
+    return {
+      year: minEnd.getFullYear(),
+      month: minEnd.getMonth() + 1,
+      day: minEnd.getDate(),
+    };
+  };

120-129: 종료일 자동 보정 시 기준 날짜를 ‘max(오늘+2, 시작일)’로 통일하세요

현재 종료일 검증은 “내일(+1)”과 “시작일”만 비교하고, 보정은 getInitialEndDate(오늘+2)로 되돌립니다. 이 불일치로 시작일이 오늘+2보다 미래인 경우 보정 후에도 여전히 무효가 될 수 있습니다. 검증/보정 모두 동일한 기준(minAllowedEnd)을 사용하도록 수정이 필요합니다.

-    if (isEndDate) {
-      const startDateObj = new Date(startDate.year, startDate.month - 1, startDate.day);
-      const tomorrow = new Date();
-      tomorrow.setDate(tomorrow.getDate() + 1);
-
-      // 종료일이 내일보다 이르거나 시작일보다 이른 경우 조정
-      if (selectedDate < tomorrow || selectedDate < startDateObj) {
-        adjustedDate = getInitialEndDate();
-      }
-    }
+    if (isEndDate) {
+      const startDateObj = new Date(startDate.year, startDate.month - 1, startDate.day);
+      startDateObj.setHours(0, 0, 0, 0);
+
+      const today = new Date();
+      today.setHours(0, 0, 0, 0);
+      const policyMinEnd = new Date(today);
+      policyMinEnd.setDate(policyMinEnd.getDate() + 2); // 정책 기준일
+
+      const minAllowedEnd =
+        policyMinEnd.getTime() < startDateObj.getTime() ? startDateObj : policyMinEnd;
+
+      if (selectedDate < minAllowedEnd) {
+        adjustedDate = {
+          year: minAllowedEnd.getFullYear(),
+          month: minAllowedEnd.getMonth() + 1,
+          day: minAllowedEnd.getDate(),
+        };
+      }
+    }
src/components/memory/RecordItem/RecordItem.tsx (1)

133-135: roomId 기본값 '1' 강제는 위험합니다(오삭제/오핀 유발 가능)

roomId || '1'로 기본값을 강제하면, URL 파라미터가 비어있는 경우 다른 방(혹은 존재하지 않는 방) 대상으로 삭제/핀하기 요청이 나갈 수 있습니다. 이는 고위험 동작입니다. roomId가 없으면 즉시 사용자에게 알리고 조기 반환하세요.

-    const currentRoomId = roomId || '1';
+    if (!roomId) {
+      openSnackbar({
+        message: '유효하지 않은 방입니다.',
+        variant: 'top',
+        onClose: () => {},
+      });
+      return;
+    }
-    const recordId = parseInt(record.id);
+    const recordId = Number(record.id);
+    if (Number.isNaN(recordId)) {
+      openSnackbar({ message: '유효하지 않은 기록 ID입니다.', variant: 'top', onClose: () => {} });
+      return;
+    }

-      if (type === 'poll') {
-        response = await deleteVote(parseInt(currentRoomId), recordId);
-      } else {
-        response = await deleteRecord(parseInt(currentRoomId), recordId);
-      }
+      if (type === 'poll') {
+        response = await deleteVote(Number(roomId), recordId);
+      } else {
+        response = await deleteRecord(Number(roomId), recordId);
+      }

핀하기 로직에도 동일하게 적용해 주세요:

-    const currentRoomId = roomId || '1';
-    const recordId = parseInt(record.id);
+    if (!roomId) {
+      openSnackbar({ message: '유효하지 않은 방입니다.', variant: 'top', onClose: () => {} });
+      return;
+    }
+    const recordId = Number(record.id);
+    if (Number.isNaN(recordId)) {
+      openSnackbar({ message: '유효하지 않은 기록 ID입니다.', variant: 'top', onClose: () => {} });
+      return;
+    }

-      const response = await pinRecordToFeed(parseInt(currentRoomId), recordId);
+      const response = await pinRecordToFeed(Number(roomId), recordId);
@@
-              roomId: currentRoomId,
+              roomId, // 위에서 roomId 존재 확인 완료

Also applies to: 140-143, 193-195, 197-198, 211-214

🧹 Nitpick comments (25)
src/components/group/CommentSection.tsx (1)

21-21: alt 텍스트 표현 미세 조정

이미지는 직접 클릭 대상이 아니므로 ‘버튼’이라는 용어는 혼선을 줄 수 있습니다. ‘아이콘’으로 바꾸는 것을 제안합니다.

-        <CommentSectionChevron src={rightChevron} alt="한마디 이동 버튼" />
+        <CommentSectionChevron src={rightChevron} alt="한마디 이동 아이콘" />
src/components/group/RecordSection.styled.ts (1)

57-59: progress 값 클램프 제안 — 스타일 파손 예방

width: ${progress}%는 0~100을 벗어나면 레이아웃이 깨질 수 있습니다. 안전하게 클램프하세요.

-export const ProgressBarFill = styled.div<{ progress: number }>`
-  width: ${({ progress }) => progress}%;
+export const ProgressBarFill = styled.div<{ progress: number }>`
+  width: ${({ progress }) => Math.max(0, Math.min(100, progress))}%;
   height: 100%;
   background-color: ${colors.purple.main};
   border-radius: 4px;
   transition: width 0.3s ease;
src/components/group/RecordSection.tsx (1)

26-26: alt 텍스트 용어 정교화

이미지는 내비게이션을 암시하는 장식 아이콘이므로 ‘버튼’ 대신 ‘아이콘’이 더 정확합니다.

-        <RecordSectionChevron src={rightChevron} alt="기록장 이동 버튼" />
+        <RecordSectionChevron src={rightChevron} alt="기록장 이동 아이콘" />
src/components/creategroup/ActivityPeriodSection/ActivityPeriodSection.tsx (3)

103-106: 날짜 비교 시 시각(시간대) 영향을 제거해 경계 버그를 예방하세요

Date 비교 전에 모두 00:00:00.000으로 정규화하지 않으면 자정 경계/타임존에 따라 오탐 보정이 발생할 수 있습니다. 아래와 같이 정규화 적용을 권장합니다.

-    const today = new Date();
-    const selectedDate = new Date(date.year, date.month - 1, date.day);
+    const today = new Date();
+    today.setHours(0, 0, 0, 0);
+    const selectedDate = new Date(date.year, date.month - 1, date.day);
+    selectedDate.setHours(0, 0, 0, 0);

또한 start/endDate를 생성하는 다른 지점(예: Line 53-55, 63-65)에서도 동일한 정규화를 적용하면 일관성이 높아집니다.


135-166: 중복된 “시작일 변경 시 종료일 재검증” 로직을 유틸로 추출하세요

handleStartYear/Month/Day 세 곳에서 동일한 재검증 패턴을 반복합니다. 작은 유틸(ensureValidEndDate 등)로 추출하면 유지보수성과 테스트 용이성이 좋아집니다.


59-60: “최대 3개월” 카피와 90일 하드코딩의 정책 정합성 확인 필요

문구는 “최대 3개월”인데 로직은 90일 고정입니다. 31일이 포함되는 구간에서는 실제 3개월이 92~93일이 될 수 있습니다. 정책이 “달 기준”인지 “일수 기준”인지 확인 부탁드립니다. 필요 시 “달 기준” 계산으로 전환하거나 카피를 “최대 90일”로 맞추는 것이 깔끔합니다.

src/components/creategroup/MemberLimitSection.tsx (1)

20-21: Section 중첩 대신 상위 Section에 showDivider를 주입하는 방식으로 단순화하세요

현재

내부에
를 중첩 사용하고 있어 DOM/여백이 불필요하게 늘어날 수 있고, styled-section이 실제
태그라면 시맨틱 중첩 이슈가 생깁니다. 상위 Section에 showDivider 속성을 주는 쪽이 간결합니다.

-    <Section>
-      <Section showDivider />
+    <Section showDivider>

만약 시각적 구분선만 필요하면 전용 컴포넌트를 쓰거나 CSS pseudo-element로 처리하는 것도 좋습니다.

src/components/creategroup/PrivacySettingSection/PrivacySettingSection.tsx (1)

27-28: Privacy 섹션도 Section 중첩 없이 처리 권장

UI 목적의 구분선이라면 상위 Section에 showDivider를 부여하는 방식이 더 간단하고 예측 가능하며, 시맨틱 중첩 리스크도 피할 수 있습니다.

-    <Section>
-      <Section showDivider />
+    <Section showDivider>

추가로 ToggleSwitch가 div 기반이라면 role="switch" 및 aria-checked 적용을 고려하면 접근성이 향상됩니다. (선택사항)

src/api/rooms/getRoomPlaying.ts (2)

71-74: 메시지 문자열 비교 대신 도메인 에러로 표준화하는 것을 고려하세요

다른 API들(getRoomDetail/getRoomMembers 등)과 동일한 에러 코드(140011 등)를 처리 중이라면, 코드→의미 매핑을 공통화하고 Error 서브클래스(예: PermissionDeniedError, RecruitmentExpiredError)로 던지면 페이지단 라우팅 분기가 메시지 문자열에 의존하지 않아 안정적입니다. i18n 변경에도 견고해집니다.

  • 공통 헬퍼 예시: mapApiErrorCodeToDomainError(axiosError)
  • 페이지단 분기: if (err instanceof PermissionDeniedError) navigate('/group')

52-62: index 기반 id 생성은 리스트 재정렬 시 불안정할 수 있습니다

convertVotesToPolls에서 id를 index로 생성하면 재정렬/필터링 시 키 불안정으로 React 성능 경고가 발생할 수 있습니다. 서버가 고유 식별자를 제공하지 않는다면 content+page 조합 등으로 안정 키를 생성하는 것을 고려해주세요. (선택사항)

src/components/creategroup/GenreSelectionSection.tsx (1)

15-16: 구분선 표현을 위해 Section 중첩 사용 대신 속성으로 처리

여기도 동일하게 상위 Section에 showDivider를 부여하는 방식이 간결합니다. 불필요한 DOM 중첩을 줄이고 시맨틱 이슈를 예방합니다.

-    <Section>
-      <Section showDivider />
+    <Section showDivider>
src/api/rooms/getRoomMembers.ts (1)

50-52: API 레이어에서의 console.error 남발 가능성

API 유틸 내부에서 console.error를 직접 호출하면 상위 호출부에서도 로깅할 때 중복 로그가 발생할 수 있습니다. 호출부에서만 로깅하거나, 공용 로거로 레벨 제어를 권장합니다.

src/components/creategroup/RoomInfoSection.tsx (1)

19-20: 불필요한 DOM 노드 최소화 제안(선택)

<Section showDivider />를 독립 노드로 세 번 추가하기보다, SectiontopDivider / bottomDivider 같은 prop을 두어 하나의 섹션 컴포지션으로 표현하면 DOM이 얇아지고 스타일 계산도 단순해집니다(특히 리스트가 많은 화면에서).

Also applies to: 34-35, 50-51

src/pages/groupDetail/ParticipatedGroupDetail.tsx (1)

77-86: 권한 오류 리다이렉트 로직 공통화 제안

여러 페이지에서 동일한 패턴(권한 오류 → /group 리다이렉트)이 반복됩니다. usePermissionRedirect(err) 같은 훅으로 공통화하면 중복 제거 및 유지보수성이 좋아집니다.

src/pages/groupMembers/GroupMembers.tsx (1)

26-33: 도달 불가능한 분기 제거 제안

const currentRoomId = roomId || localStorage.getItem('currentRoomId') || '1';로 기본값을 '1'로 보장하고 있어, 직후의 if (!currentRoomId)는 절대 진입하지 않습니다. 분기를 제거하거나, 기본값 제공을 상위로 이동해 의도를 명확히 하길 권장합니다.

간단 제거 예시:

-      if (!currentRoomId) {
-        setError('방 ID를 찾을 수 없습니다.');
-        setLoading(false);
-        return;
-      }
src/api/rooms/getRoomDetail.ts (1)

45-56: 문자열 비교 의존도 축소를 위한 에러 매핑 유틸 제안(선택)

UI단에서 err.message === '...' 비교 대신, API 계층에서 code → DomainError로 매핑해 error.name 또는 error.code 기반 분기를 추천합니다. 예: throw new PermissionDeniedError() 또는 throw new ApiError(140011, '...').

src/pages/groupDetail/GroupDetail.tsx (1)

103-115: 문자열 비교 기반 분기(에러 메시지)는 취약 — 에러 코드/커스텀 에러로 분기하도록 리팩터 권장

현재 '모집기간이 만료된 방입니다.', '방 접근 권한이 없습니다.' 같은 하드코딩된 한글 문자열에 의존합니다. 메시지 변경(문구/띄어쓰기/i18n) 시 즉시 오동작합니다. API 래퍼에서 이미 에러 코드를 해석하고 있으니, 아래 중 하나로 개선을 권장합니다.

  • 최소침습: 메시지 상수를 중앙화해 오타/변경에 대비
    • 예: src/constants/roomErrors.ts에 상수 정의 후 사용
  • 권장: API 래퍼에서 커스텀 에러 클래스로 throw하여 코드 기반으로 분기
    • 예: new RoomError('RECRUITMENT_CLOSED') / new RoomError('NO_ACCESS')
    • UI 단에서는 err instanceof RoomError && err.code === 'NO_ACCESS' 식으로 분기
  • 중복 제거: ParticipatedGroupDetail.tsx와 동일 로직이 있으므로, 공통 유틸(handleRoomDomainError)로 추출

참고로 위 100라인의 diff와 함께 최소 수정으로 유지하려면, 아래처럼 message 변수 사용으로 NPE를 방지할 수 있습니다.

-        // 모집기간이 만료된 방인 경우 - 진행중인 방으로 리다이렉트
-        if (error.message === '모집기간이 만료된 방입니다.') {
+        // 모집기간이 만료된 방인 경우 - 진행중인 방으로 리다이렉트
+        if (message === '모집기간이 만료된 방입니다.') {
           navigate(`/group/detail/joined/${roomId}`, { replace: true });
           return;
         }
         
-        // 방 접근 권한이 없는 경우 - 모임 홈으로 리다이렉트
-        if (error.message === '방 접근 권한이 없습니다.') {
+        // 방 접근 권한이 없는 경우 - 모임 홈으로 리다이렉트
+        if (message === '방 접근 권한이 없습니다.') {
           navigate('/group', { replace: true });
           return;
         }

추가로 원천 해결안(별도 파일 예시):

// src/errors/RoomError.ts
export type RoomErrorCode = 'RECRUITMENT_CLOSED' | 'NO_ACCESS';

export class RoomError extends Error {
  constructor(public code: RoomErrorCode, message?: string) {
    super(message ?? code);
    this.name = 'RoomError';
  }
}

// src/api/rooms/getRoomDetail.ts
// throw new RoomError('RECRUITMENT_CLOSED'); // code 100004
// throw new RoomError('NO_ACCESS');          // code 140011

// 사용처
catch (err) {
  if (err instanceof RoomError) {
    if (err.code === 'RECRUITMENT_CLOSED') { ... }
    if (err.code === 'NO_ACCESS') { ... }
  }
}
src/pages/post/UpdatePost.tsx (2)

121-127: 사진 추가/삭제 핸들러가 no-op입니다 — 의도라면 UI 비활성화, 아니라면 구현 필요

편집 화면에서 새 사진 추가가 불가한 정책이라면 PhotoSection의 추가/삭제 UI를 비활성화하거나 힌트를 노출하세요. 반대로 추가 지원이 목적이면 업로드 핸들러를 구현해야 합니다.

가능한 방향:

  • 편집에서 “추가” 비활성화
  • 또는 업로드 훅(예: useUploadImages) 연동 및 UpdateFeedBody에 새 업로드 결과를 병합

116-118: 수정 실패 시 사용자 피드백 없음 — 스낵바로 실패 메시지 노출 권장

네트워크/서버 오류 등 실패 시 즉시 반환만 하고 있어 UX가 불명확합니다. 스낵바로 실패 안내를 추가하세요.

적용 diff:

-    if (!result?.success) {
-      return;
-    }
+    if (!result?.success) {
+      openSnackbar({
+        message: result?.message ?? '피드 수정에 실패했습니다.',
+        variant: 'top',
+        onClose: closePopup,
+      });
+      return;
+    }
src/components/memory/RecordItem/RecordItem.tsx (6)

375-392: 텍스트 기록에만 핀 버튼 노출: 메뉴 정책과 UI 일관성 확보

상단 더보기 메뉴에서 onPin을 텍스트에만 제공하도록 한 것과 UI 버튼 노출 정책이 일치합니다. 사용성 측면에서 혼란을 줄이는 적절한 제한입니다.

추가 제안(접근성): ActionButton이 실제 버튼 역할이라면 aria-label="피드에 핀하기"를 부여하고, div 기반이라면 role="button"tabIndex={0}를 추가해 키보드 접근성을 확보해 주세요.

가능한 미니멀 변경 예:

-          <ActionButton
+          <ActionButton
+            aria-label="피드에 핀하기"
             onClick={

76-83: 좋아요 카운트 동기화 로직 보강(서버 카운트 사용/하한 0 보장)

서버에서 최신 likeCount를 내려주면 그것을 신뢰하는 편이 안전합니다. 없을 경우에도 카운트가 음수로 내려가지 않도록 하한을 두세요.

-        setCurrentLikeCount((prev: number) => (response.data.isLiked ? prev + 1 : prev - 1));
+        setCurrentLikeCount((prev: number) =>
+          typeof response.data.likeCount === 'number'
+            ? response.data.likeCount
+            : Math.max(0, response.data.isLiked ? prev + 1 : prev - 1)
+        );

338-347: 연타(중복 요청) 방지: in-flight 상태로 클릭 잠그기

빠른 연타 시 응답 경합으로 UI/서버 상태 불일치가 생길 수 있습니다. 좋아요 요청 동안 버튼을 잠그는 얇은 플래그를 두세요.

핵심 변경 요약:

  • 상태 추가: const [liking, setLiking] = useState(false);
  • 요청 전/후로 setLiking(true/false)
  • 버튼에서 pointerEvents/aria-busy 조절

예시:

-  const handleLikeClick = async () => {
+  const handleLikeClick = async () => {
+    if (liking) return;
+    setLiking(true);
     try {
       const postId = parseInt(id);
@@
     } catch (error) {
       console.error('좋아요 API 호출 실패:', error);
       openSnackbar({
         message: '네트워크 오류가 발생했습니다. 다시 시도해주세요.',
         variant: 'top',
         onClose: () => {},
       });
     }
+    finally {
+      setLiking(false);
+    }
   };
-          style={{
+          style={{
             cursor: shouldBlur ? 'default' : 'pointer',
-            pointerEvents: shouldBlur ? 'none' : 'auto',
+            pointerEvents: shouldBlur || liking ? 'none' : 'auto',
           }}
+          aria-busy={liking || undefined}

컴포넌트 상단 어딘가에 상태를 추가하세요:

const [liking, setLiking] = useState(false);

153-156: window.location.reload() 대신 상위 상태 갱신으로 교체 권장

전면 새로고침은 UX가 거칠고, 상태 관리 이점을 잃습니다. 상위(목록) 컴포넌트로 콜백을 올려 해당 아이템만 제거하는 방향이 바람직합니다.

가능하면 RecordItemonDeleted?: (recordId: string) => void를 추가하고, 성공 시 onDeleted?.(record.id) 호출로 대체하도록 리팩터 제안 드립니다. 원하시면 관련 변경까지 한번에 패치 제안 드릴게요.


68-74: parseInt 기본 진수 미지정 및 반복 파싱 정리

parseInt는 반드시 10진을 명시하거나 Number(...)를 사용하는 편이 안전합니다. 또한 같은 값을 여러 번 파싱하지 않도록 지역 변수로 묶어 재사용하세요.

예시 변경:

-      const postId = parseInt(id);
+      const postId = Number(id);
+      if (Number.isNaN(postId)) { /* 에러 처리 */ return; }
-      const response = await pinRecordToFeed(parseInt(currentRoomId), recordId);
+      const response = await pinRecordToFeed(Number(roomId), recordId);
-    openCommentBottomSheet(parseInt(id), type === 'poll' ? 'VOTE' : 'RECORD');
+    openCommentBottomSheet(Number(id), type === 'poll' ? 'VOTE' : 'RECORD');
-            postId={parseInt(id)}
+            postId={Number(id)}

Also applies to: 193-198, 265-266, 327-328


1-4: 타입 별칭 권장: 전역 Record<K, T>와의 혼동 방지

import type { Record } ...는 TS 내장 Record<K, V>와 이름 충돌을 유발해 가독성이 떨어질 수 있습니다. MemoryRecord 등 도메인 친화 이름으로 별칭하는 것을 권장합니다.

-import type { Record } from '../../../types/memory';
+import type { Record as MemoryRecord } from '../../../types/memory';
@@
-const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
+const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {

또는 원 타입 정의 자체를 MemoryRecord로 개명하는 것을 검토해 주세요(범위가 넓으므로 별도 PR 권장).

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 f64e904 and ac65401.

📒 Files selected for processing (17)
  • src/api/rooms/getRoomDetail.ts (1 hunks)
  • src/api/rooms/getRoomMembers.ts (1 hunks)
  • src/api/rooms/getRoomPlaying.ts (1 hunks)
  • src/components/creategroup/ActivityPeriodSection/ActivityPeriodSection.tsx (1 hunks)
  • src/components/creategroup/GenreSelectionSection.tsx (1 hunks)
  • src/components/creategroup/MemberLimitSection.tsx (1 hunks)
  • src/components/creategroup/PrivacySettingSection/PrivacySettingSection.tsx (1 hunks)
  • src/components/creategroup/RoomInfoSection.tsx (2 hunks)
  • src/components/group/CommentSection.styled.ts (1 hunks)
  • src/components/group/CommentSection.tsx (1 hunks)
  • src/components/group/RecordSection.styled.ts (1 hunks)
  • src/components/group/RecordSection.tsx (1 hunks)
  • src/components/memory/RecordItem/RecordItem.tsx (3 hunks)
  • src/pages/groupDetail/GroupDetail.tsx (1 hunks)
  • src/pages/groupDetail/ParticipatedGroupDetail.tsx (1 hunks)
  • src/pages/groupMembers/GroupMembers.tsx (1 hunks)
  • src/pages/post/UpdatePost.tsx (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/components/group/CommentSection.tsx (1)
src/components/group/CommentSection.styled.ts (1)
  • CommentSectionHeader (16-20)
src/components/group/RecordSection.tsx (1)
src/components/group/RecordSection.styled.ts (1)
  • RecordSectionHeader (16-20)
🪛 ESLint
src/pages/groupDetail/ParticipatedGroupDetail.tsx

[error] 77-77: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

src/api/rooms/getRoomPlaying.ts

[error] 68-68: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

src/api/rooms/getRoomDetail.ts

[error] 45-45: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

src/pages/groupDetail/GroupDetail.tsx

[error] 100-100: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

src/pages/groupMembers/GroupMembers.tsx

[error] 45-45: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

src/api/rooms/getRoomMembers.ts

[error] 50-50: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

src/components/memory/RecordItem/RecordItem.tsx

[error] 272-272: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🔇 Additional comments (5)
src/components/creategroup/RoomInfoSection.tsx (1)

19-20: 구분선(showDivider) 추가 변경은 명확하고 부작용 없어 보입니다

시각적 분리 강화를 위한 Section showDivider 삽입이 일관되며 다른 섹션들과도 패턴이 맞습니다. 접근성/상호작용에 영향이 없는 수평 레이아웃 변경으로 판단됩니다.

Also applies to: 34-35, 50-51

src/pages/post/UpdatePost.tsx (1)

111-112: remainImageUrls 전송 방식—백엔드 계약과 일치함 확인됨

검토 결과, UpdateFeedBody 인터페이스에

remainImageUrls?: string[];

로 optional 정의되어 있고, API 샘플 코드에서도

  • remainImageUrls: ["..."] → 기존 이미지 일부 유지
  • remainImageUrls: [] → 기존 이미지 전부 삭제
  • 필드 미포함(undefined) → 기존 이미지 유지

로 처리되고 있습니다. (apiClient.patch 기반 PATCH 요청은 필드 미포함 시 해당 속성을 변경하지 않음)

또한 코드 내 모든 remainImageUrls 참조(상태 선언·전송·컴포넌트 prop)는 스펙과 정확히 일치하며 오타도 없습니다.

따라서 “이미지 없을 때 빈 배열 전송 → 삭제 처리” 로직은 백엔드 계약에 부합하므로 추가 변경 없이 그대로 진행하셔도 좋습니다.

src/components/memory/RecordItem/RecordItem.tsx (3)

292-301: deps에 type 추가: 메뉴 옵션 반영을 위한 올바른 수정

handleClick에서 type을 deps에 포함시킨 변경으로, 레코드 타입이 바뀔 때 onPin 포함 여부가 즉시 반영됩니다. 의도에 부합하는 좋은 수정입니다.


311-318: UserAvatar는 div 기반이므로 img 속성(alt, onError)이 적용되지 않습니다.
해당 컴포넌트는 styled.div로 구현돼 있어 alt 텍스트나 onError 핸들러를 받을 수 없습니다. 접근성을 개선하려면 아래 방안을 고려해 주세요:

  • div에 role="img"aria-label을 추가해 스크린리더에 대체 텍스트를 제공
  • CSS background-image 대신 <img> 태그로 전환하고 alt/onError 처리
  • div에 배경 이미지 폴백 CSS를 적용하거나, 이미지 로드 실패 시 다른 스타일을 적용하는 로직 구현

Likely an incorrect or invalid review comment.


272-286: any 타입 제거 및 메뉴 옵션을 MoreMenuProps로 대체

ESLint가 지적한 any를 제거하고, openMoreMenu의 인자 타입인 MoreMenuProps를 직접 재사용해 타입 안전성을 확보합니다. 아래와 같이 수정해 주세요.

--- a/src/components/memory/RecordItem/RecordItem.tsx
+++ b/src/components/memory/RecordItem/RecordItem.tsx
@@ 272,286
-      const menuOptions: any = {
-        onEdit: handleEdit,
-        onDelete: handleDeleteConfirm,
-        onClose: closePopup,
-        type: 'post',
-        isWriter: true,
-      };
-      
-      // 기록(text)일 때만 핀하기 기능 추가
-      if (type === 'text') {
-        menuOptions.onPin = handlePinConfirm;
-      }
-      
-      openMoreMenu(menuOptions);
+      // `MoreMenuProps`를 사용해 any 제거
+      const baseMenuOptions: MoreMenuProps = {
+        onEdit: handleEdit,
+        onDelete: handleDeleteConfirm,
+        onClose: closePopup,
+        type: 'post',
+        isWriter: true,
+      };
+      const menuOptions: MoreMenuProps =
+        type === 'text'
+          ? { ...baseMenuOptions, onPin: handlePinConfirm }
+          : baseMenuOptions;
+      openMoreMenu(menuOptions);

• 파일 상단에 import type { MoreMenuProps } from '@/stores/usePopupStore'; 추가
openMoreMenu는 이미 MoreMenuProps를 파라미터로 받도록 정의되어 있어 별도의 유도 타입 선언 없이 재사용 가능합니다.

Comment thread src/api/rooms/getRoomDetail.ts Outdated
Comment on lines +45 to +56
} catch (error: any) {
console.error('방 상세 정보 조회 API 오류:', error);

// 모집기간이 만료된 방인 경우
if (error.response?.data?.code === 100004) {
throw new Error('모집기간이 만료된 방입니다.');
}

// 방 접근 권한이 없는 경우
if (error.response?.data?.code === 140011) {
throw new Error('방 접근 권한이 없습니다.');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

catch any → Axios 오류 내로잉 + 린트 해결

에러 타입을 unknown으로 바꾸고 Axios 오류일 때만 안전하게 코드 접근하세요. 두 가지 코드(100004/140011) 모두 동일한 패턴으로 처리합니다.

-  } catch (error: any) {
+  } catch (error: unknown) {
     console.error('방 상세 정보 조회 API 오류:', error);
     
     // 모집기간이 만료된 방인 경우
-    if (error.response?.data?.code === 100004) {
+    if (isAxiosError<{ code: number }>(error) && error.response?.data?.code === 100004) {
       throw new Error('모집기간이 만료된 방입니다.');
     }
     
     // 방 접근 권한이 없는 경우
-    if (error.response?.data?.code === 140011) {
+    if (isAxiosError<{ code: number }>(error) && error.response?.data?.code === 140011) {
       throw new Error('방 접근 권한이 없습니다.');
     }

파일 상단 임포트 추가:

import { isAxiosError } from 'axios';
🧰 Tools
🪛 ESLint

[error] 45-45: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
In src/api/rooms/getRoomDetail.ts around lines 45 to 56, change the catch to
accept error as unknown and only treat it as an Axios error after checking with
isAxiosError(error); add the import "isAxiosError" from axios at the file top,
then inside the catch guard with if (isAxiosError(error)) and safely read
error.response?.data?.code to handle codes 100004 and 140011 using the same
pattern; for non-Axios errors rethrow or wrap appropriately to preserve typing
and satisfy the linter.

Comment thread src/api/rooms/getRoomMembers.ts Outdated
Comment on lines +50 to +57
} catch (error: any) {
console.error('독서메이트 조회 API 오류:', error);

// 방 접근 권한이 없는 경우
if (error.response?.data?.code === 140011) {
throw new Error('방 접근 권한이 없습니다.');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

catch 파라미터 any 사용으로 ESLint 위반 → Axios 오류 내로잉으로 해결 제안

현재 catch (error: any)@typescript-eslint/no-explicit-any에 걸립니다. unknown으로 전환하고 Axios 오류일 때만 안전하게 코드 접근하도록 내로잉하면 린트 통과와 타입 안전성을 동시에 확보할 수 있습니다.

다음 변경을 제안합니다(해당 범위 내 변경):

-  } catch (error: any) {
+  } catch (error: unknown) {
     console.error('독서메이트 조회 API 오류:', error);
     
     // 방 접근 권한이 없는 경우
-    if (error.response?.data?.code === 140011) {
+    if (isAxiosError<{ code: number }>(error) && error.response?.data?.code === 140011) {
       throw new Error('방 접근 권한이 없습니다.');
     }
     
     throw error;
   }

추가로, 파일 상단(임포트 섹션)에 다음을 추가해 주세요:

import { isAxiosError } from 'axios';
🧰 Tools
🪛 ESLint

[error] 50-50: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
In src/api/rooms/getRoomMembers.ts around lines 50 to 57, replace the catch
parameter typed as any with unknown and narrow it using Axios's isAxiosError;
add "import { isAxiosError } from 'axios';" to the file imports, then inside the
catch check if isAxiosError(error) before accessing error.response?.data?.code
and handle the specific 140011 code as before, otherwise rethrow or throw a
generic error to preserve type-safety and satisfy ESLint.

Comment thread src/api/rooms/getRoomPlaying.ts Outdated
Comment on lines 68 to 76
} catch (error: any) {
console.error('진행중인 방 상세 정보 조회 API 오류:', error);

// 방 접근 권한이 없는 경우
if (error.response?.data?.code === 140011) {
throw new Error('방 접근 권한이 없습니다.');
}

throw error;
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

catch (error: any)로 인한 ESLint 오류 해결 및 Axios 에러 가드 추가

ESLint(@typescript-eslint/no-explicit-any) 에러가 발생합니다. catch 파라미터의 any 제거와 Axios 에러 가드를 추가해 타입 안전성을 확보하세요. 또한 콘솔 로그는 유지하되, 권한 에러만 조건 분기합니다.

-  } catch (error: any) {
-    console.error('진행중인 방 상세 정보 조회 API 오류:', error);
-    
-    // 방 접근 권한이 없는 경우
-    if (error.response?.data?.code === 140011) {
-      throw new Error('방 접근 권한이 없습니다.');
-    }
-    
-    throw error;
-  }
+  } catch (error) {
+    // Axios 에러인 경우에만 코드 분기
+    if (isAxiosError(error) && error.response?.data?.code === 140011) {
+      throw new Error('방 접근 권한이 없습니다.');
+    }
+    console.error('진행중인 방 상세 정보 조회 API 오류:', error);
+    throw error;
+  }

또한 상단에 아래 유틸 임포트를 추가해주세요:

import { isAxiosError } from 'axios';
🧰 Tools
🪛 ESLint

[error] 68-68: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
In src/api/rooms/getRoomPlaying.ts around lines 68 to 76, replace the
catch(error: any) with a typed catch (error: unknown) and add an Axios error
guard using isAxiosError: keep the console.error line but only check
error.response?.data?.code === 140011 inside an if (isAxiosError(error) &&
error.response?.data) branch and throw the specific '방 접근 권한이 없습니다.' Error
there; otherwise rethrow the original error (preserving the caught value). Also
add the import "import { isAxiosError } from 'axios';" at the top of the file.

Comment on lines +13 to 14
cursor: pointer;
`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

섹션 전체 클릭 전환 OK — 키보드 사용자용 포커스 표시 추가 필요

section을 인터랙티브하게 사용했으므로 키보드 포커스가 명확히 보여야 합니다. 다음처럼 :focus-visible 스타일을 추가해 접근성을 보강해 주세요.

 export const CommentSection = styled.section`
   display: flex;
   flex-direction: column;
   width: calc(100% - 40px);
   gap: 12px;
   background: ${colors.darkgrey.dark};
   margin: 20px 20px 0 20px;
   padding: 16px 12px;
   border-radius: 12px;
-  cursor: pointer;
+  cursor: pointer;
+  &:focus-visible {
+    outline: 2px solid ${colors.purple.main};
+    outline-offset: 2px;
+  }
 `;
📝 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
cursor: pointer;
`;
export const CommentSection = styled.section`
display: flex;
flex-direction: column;
width: calc(100% - 40px);
gap: 12px;
background: ${colors.darkgrey.dark};
margin: 20px 20px 0 20px;
padding: 16px 12px;
border-radius: 12px;
cursor: pointer;
&:focus-visible {
outline: 2px solid ${colors.purple.main};
outline-offset: 2px;
}
`;
🤖 Prompt for AI Agents
In src/components/group/CommentSection.styled.ts around lines 13-14, the
interactive section currently only sets cursor: pointer but lacks keyboard focus
styling; add a :focus-visible rule to the section that provides a clear visible
focus indicator (for example an outline or box-shadow with appropriate
outline-offset and accessible color) and ensure the underlying element is
focusable in the DOM (e.g., the component using this styled section should set
tabindex="0" if it isn't naturally focusable) so keyboard users will see the
focus state.

Comment on lines +18 to +19
<StyledCommentSection onClick={onClick}>
<CommentSectionHeader>
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

비-버튼 요소 클릭: 키보드 접근성과 스크린 리더 지원 추가 필요

section 클릭으로 바뀌면서 탭 포커스/Enter/Space 조작이 불가합니다. role="button", tabIndex, onKeyDown을 추가해 주세요.

-    <StyledCommentSection onClick={onClick}>
-      <CommentSectionHeader>
+    <StyledCommentSection
+      role="button"
+      tabIndex={0}
+      onClick={onClick}
+      onKeyDown={handleKeyDown}
+      aria-label="오늘의 한마디 이동"
+    >
+      <CommentSectionHeader>

컴포넌트 내부에 핸들러를 추가하세요(타입 임포트가 번거로우면 any 생략 가능).

// return 위에 추가
const handleKeyDown = (e: React.KeyboardEvent) => {
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault();
    onClick();
  }
};

필요 시 타입 전용 임포트:

import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
// ...
const handleKeyDown = (e: ReactKeyboardEvent) => { /* 동일 */ };
🤖 Prompt for AI Agents
In src/components/group/CommentSection.tsx around lines 18-19, the section
element lost keyboard accessibility when changed from a button; add
role="button" and tabIndex={0} to make it focusable, implement an internal
handleKeyDown that prevents default and calls onClick when e.key is "Enter" or "
" (Space), and attach it as onKeyDown={handleKeyDown}; keep onClick as-is. If
you prefer stricter typing, add a type-only import for React.KeyboardEvent and
type the handler, otherwise omit types for brevity.

Comment on lines +13 to 14
cursor: pointer;
`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

섹션 전체 클릭 전환 OK — 포커스 가시성 추가 제안

section을 버튼처럼 사용하므로 키보드 포커스 링을 제공해야 합니다.

 export const RecordSection = styled.section`
   display: flex;
   flex-direction: column;
   width: calc(100% - 40px);
   gap: 12px;
   background: ${colors.darkgrey.dark};
   margin: 20px 20px 0 20px;
   padding: 16px 12px;
   border-radius: 12px;
-  cursor: pointer;
+  cursor: pointer;
+  &:focus-visible {
+    outline: 2px solid ${colors.purple.main};
+    outline-offset: 2px;
+  }
 `;
📝 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
cursor: pointer;
`;
export const RecordSection = styled.section`
display: flex;
flex-direction: column;
width: calc(100% - 40px);
gap: 12px;
background: ${colors.darkgrey.dark};
margin: 20px 20px 0 20px;
padding: 16px 12px;
border-radius: 12px;
cursor: pointer;
&:focus-visible {
outline: 2px solid ${colors.purple.main};
outline-offset: 2px;
}
`;
🤖 Prompt for AI Agents
In src/components/group/RecordSection.styled.ts around lines 13-14, the section
is styled as a clickable element but lacks keyboard focus visibility; make the
section keyboard-focusable (e.g., ensure it gets tabindex when used as a button)
and add focus styles using :focus and preferably :focus-visible to show a clear
outline or box-shadow (contrasting color and at least 2px) and preserve existing
hover/click styles so keyboard users see the same interactive affordance.

Comment on lines +23 to +24
<StyledRecordSection onClick={onClick}>
<RecordSectionHeader>
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

컨테이너 onClick 이동은 타당 — 키보드 조작 가능하도록 역할/핸들러 추가 필요

접근성 준수를 위해 role="button", tabIndex, onKeyDown을 추가해 주세요.

-    <StyledRecordSection onClick={onClick}>
-      <RecordSectionHeader>
+    <StyledRecordSection
+      role="button"
+      tabIndex={0}
+      onClick={onClick}
+      onKeyDown={handleKeyDown}
+      aria-label="기록장 이동"
+    >
+      <RecordSectionHeader>

컴포넌트 내부에 핸들러 추가:

// return 위에 추가
const handleKeyDown = (e: React.KeyboardEvent) => {
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault();
    onClick();
  }
};
🤖 Prompt for AI Agents
In src/components/group/RecordSection.tsx around lines 23 to 24, the container
has an onClick but lacks keyboard accessibility attributes; add role="button"
and tabIndex={0} to the StyledRecordSection and implement an onKeyDown handler
that calls onClick when Enter or Space is pressed (preventDefault first). Define
the handleKeyDown function above the return to check for e.key === 'Enter' ||
e.key === ' ' (or 'Spacebar' for legacy) and call onClick(); attach it as
onKeyDown={handleKeyDown}. Ensure onClick is invoked safely (check it exists)
and keep the existing onClick prop.

Comment thread src/pages/groupDetail/GroupDetail.tsx Outdated
Comment thread src/pages/groupDetail/ParticipatedGroupDetail.tsx Outdated
Comment thread src/pages/groupMembers/GroupMembers.tsx Outdated
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: 0

♻️ Duplicate comments (2)
src/pages/groupMembers/GroupMembers.tsx (1)

45-47: catch 파라미터 unknown 전환 + 안전 가드 도입이 잘 반영되었습니다

catch (err: unknown)로 타입을 좁히고 콘솔 로깅으로 상황을 남기는 접근, 이전 리뷰 피드백과 일치합니다. 👍

src/components/group/CommentSection.styled.ts (1)

13-18: section을 버튼처럼 사용할 때 role/tabIndex/키보드 활성화를 보장하세요

section은 기본적으로 포커스/액션 시맨틱이 없습니다. 스타일만으로는 충분치 않으니 TSX에서 다음을 확인해 주세요: tabIndex={0}, role="button", Enter/Space로 트리거되는 onKeyDown. 확장/접힘 UI라면 aria-expanded/aria-controls도 함께 지정하세요.

예시(외부 TSX, 참조용):

<StyledCommentSection
  role="button"
  tabIndex={0}
  aria-expanded={isOpen}           // 적용 UI에 한해
  aria-controls="comment-panel"    // 적용 UI에 한해
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      onClick?.(e);
    }
  }}
  onClick={onClick}
>
  ...
</StyledCommentSection>
🧹 Nitpick comments (5)
src/pages/groupMembers/GroupMembers.tsx (3)

37-37: parseInt 기수(radix) 명시

암묵적 10진 파싱에 의존하지 말고 기수를 명시하는 편이 안전합니다.

-        const response: RoomMembersResponse = await getRoomMembers(parseInt(currentRoomId));
+        const response: RoomMembersResponse = await getRoomMembers(parseInt(currentRoomId, 10));

29-33: 도달 불가한 null 체크 정리

const currentRoomId = roomId || localStorage.getItem('currentRoomId') || '1'; 때문에 currentRoomId는 항상 truthy이므로 아래 if (!currentRoomId) 블록은 실행되지 않습니다. 불필요한 분기를 제거해 가독성을 높일 수 있습니다.

-      if (!currentRoomId) {
-        setError('방 ID를 찾을 수 없습니다.');
-        setLoading(false);
-        return;
-      }

48-52: 문자열 비교 기반 권한 에러 처리 통합 가드 함수 도입 권장

현재 코드베이스 전반에 걸쳐 '방 접근 권한이 없습니다.' 문자열 비교 방식으로 에러를 감지하고 있어, 이후 메시지 변경 시 일일이 수동 수정이 필요합니다. 아래와 같이 공용 유틸(예: src/api/errors.ts)에 접근 거부 가드 함수를 정의하고, 각 컴포넌트에서는 해당 가드만 호출하도록 리팩터링할 것을 권장드립니다.

주요 변경 포인트:

  • src/api/errors.tsisAccessDeniedError 함수 추가
  • UI/페이지 컴포넌트(GroupMembers.tsx, GroupDetail.tsx, TodayWords.tsx, RecordWrite.tsx, PollWrite.tsx 등)에서 직접 메시지 비교 대신 가드 호출
  • API 레이어(getRoomPlaying.ts, getRoomMembers.ts, getRoomDetail.ts)에서는 AxiosError.response.data.code === 140011 기준으로 throw new AccessDeniedError() 등 커스텀 에러 타입 사용 검토

예시 diff (컴포넌트 측):

- // 방 접근 권한이 없는 경우 - 모임 홈으로 리다이렉트
- if (err instanceof Error && err.message === '방 접근 권한이 없습니다.') {
+ // 방 접근 권한이 없는 경우 - 모임 홈으로 리다이렉트
+ if (isAccessDeniedError(err)) {
    navigate('/group', { replace: true });
    return;
  }

가드 함수 예시 (src/api/errors.ts):

export const ACCESS_DENIED_CODE = 140011;

export function isAccessDeniedError(e: unknown): boolean {
  if (!e || typeof e !== 'object') return false;
  const err = e as any;

  // Axios 스타일: response.data.code
  if (Number(err?.response?.data?.code) === ACCESS_DENIED_CODE) return true;

  // 도메인 에러 형태: code 필드
  if (Number(err?.code) === ACCESS_DENIED_CODE) return true;

  // 레거시 메시지 비교 (차후 제거)
  if (e instanceof Error && e.message === '방 접근 권한이 없습니다.') return true;

  return false;
}

추가 리팩터링 대상 파일 (주요 경로):

  • src/pages/groupMembers/GroupMembers.tsx
  • src/pages/groupDetail/{GroupDetail,ParticipatedGroupDetail}.tsx
  • src/pages/today-words/TodayWords.tsx
  • src/pages/recordwrite/RecordWrite.tsx
  • src/pages/pollwrite/PollWrite.tsx
  • src/components/memory/{RecordItem,PollRecord}.tsx
  • src/api/rooms/{getRoomPlaying,getRoomMembers,getRoomDetail}.ts

위 가드를 활용하면 향후 에러 처리 로직 변경 시 API 레이어만 수정하면 되고, UI는 일관된 로직을 재사용할 수 있어 유지보수성이 크게 향상됩니다.

src/components/group/CommentSection.styled.ts (2)

13-18: 자식 요소 포커스까지 하이라이트하려면 :focus-within 추가 제안

내부에 링크/입력 등이 포커스를 받는 경우 컨테이너도 함께 강조되도록 하면 인지성이 좋아집니다.

   &:focus-visible {
     outline: 2px solid ${colors.purple.main};
     outline-offset: 2px;
   }
+  &:focus-within {
+    outline: 2px solid ${colors.purple.main};
+    outline-offset: 2px;
+  }

13-18: 호버 힌트/전환 추가로 상호작용성을 더 명확히

커서 변경만으로는 부족할 수 있습니다. 가벼운 배경 변화와 트랜지션을 권장합니다. 디자인 톤은 동일 계열 색으로 유지했습니다.

   cursor: pointer;
   
   &:focus-visible {
     outline: 2px solid ${colors.purple.main};
     outline-offset: 2px;
   }
+  transition: background-color 120ms ease, outline-color 120ms ease;
+  &:hover {
+    background: ${colors.darkgrey.main};
+  }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 ac65401 and 55ffb9f.

📒 Files selected for processing (7)
  • src/api/rooms/getRoomDetail.ts (2 hunks)
  • src/api/rooms/getRoomMembers.ts (2 hunks)
  • src/api/rooms/getRoomPlaying.ts (2 hunks)
  • src/components/group/CommentSection.styled.ts (1 hunks)
  • src/pages/groupDetail/GroupDetail.tsx (1 hunks)
  • src/pages/groupDetail/ParticipatedGroupDetail.tsx (1 hunks)
  • src/pages/groupMembers/GroupMembers.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/pages/groupDetail/ParticipatedGroupDetail.tsx
  • src/api/rooms/getRoomMembers.ts
  • src/pages/groupDetail/GroupDetail.tsx
  • src/api/rooms/getRoomPlaying.ts
  • src/api/rooms/getRoomDetail.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/components/group/CommentSection.styled.ts (1)
src/styles/global/global.ts (1)
  • colors (4-53)
🔇 Additional comments (1)
src/components/group/CommentSection.styled.ts (1)

13-18: 포커스 표시 추가 굿 — 접근성 보강이 반영되었습니다

cursor: pointer:focus-visible 아웃라인/오프셋 추가가 의도와 일치하며 키보드 사용자에게 명확한 포커스 피드백을 제공합니다. 색상 대비(보라색 #6868FF vs 배경 #282828)도 충분히 눈에 띕니다.

@ljh130334 ljh130334 merged commit 190f87d into main Aug 21, 2025
2 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Aug 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🌏 Deploy 배포 관련

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants