Skip to content

feat: 방 게시물(기록,투표) 좋아요 상태변경 API 연동 구현#147

Merged
ljh130334 merged 3 commits into
developfrom
feat/api-rooms-like
Aug 17, 2025
Merged

feat: 방 게시물(기록,투표) 좋아요 상태변경 API 연동 구현#147
ljh130334 merged 3 commits into
developfrom
feat/api-rooms-like

Conversation

@ljh130334

@ljh130334 ljh130334 commented Aug 17, 2025

Copy link
Copy Markdown
Member

#️⃣ 연관된 이슈

#106

📝 작업 내용

기록장(Memory) 페이지의 좋아요 기능 구현과 관련된 여러 문제를 해결했습니다.

🕸️ 주요 작업 내용

1. 좋아요 API 연동 및 상태 관리 구현

  • postRoomPostLike API를 사용하여 좋아요/좋아요 취소 기능을 구현했습니다. 기록 타입(RECORD/VOTE)에 따라 적절한 roomPostType 파라미터를 전송하도록 처리했습니다.
  • 서버 응답에 따른 좋아요 상태(isLiked)와 좋아요 수(likeCount) 실시간 업데이트를 구현했습니다.
  • API 호출 실패 시 에러 코드별로 구분된 사용자 친화적인 오류 메시지를 표시하도록 했습니다(방 접근 권한 없음, 이미 좋아요한 게시물 등).

2. 터치 이벤트 충돌 문제 해결

  • 기존에 좋아요 버튼과 길게 누르기 기능 간의 터치 이벤트 충돌로 인해 passive event listener 오류가 발생하던 문제를 해결했습니다.
  • 액션 섹션(좋아요, 댓글 버튼 영역)에서는 길게 누르기 기능을 완전히 제외하고, stopPropagation을 통해 이벤트 전파를 차단하여 터치 이벤트가 분리되도록 구현했습니다.
  • 마우스 클릭과 터치 이벤트 모두에서 좋아요 기능이 정상 동작하도록 했습니다.

3. Memory 페이지 타입 시스템 정리

  • RecordPollOption 타입을 types/memory.ts로 통합하여 import 오류를 해결했습니다.
  • SortType 정의를 API 스펙에 맞게 수정했습니다(popular → like, comments → comment).
  • TypeScript 타입 오류들을 모두 해결하여 빌드가 정상적으로 되도록 했습니다.

4. 좋아요 상태 데이터 구조 개선

  • Record 타입에 isLiked 속성을 추가하여 좋아요 상태를 추적할 수 있도록 했습니다.
  • API에서 받은 Post 데이터를 Record로 변환할 때 isLiked 속성이 올바르게 매핑되도록 convertPostToRecord 함수를 수정했습니다.

Summary by CodeRabbit

  • New Features
    • 메모리 게시글 좋아요/취소 지원(투표·텍스트 모두), 상태·카운트 즉시 반영 및 실패 시 스낵바 안내
    • 비소유자 롱프레스 신고, 소유자 클릭 편집/삭제
    • 메모리 경로 /rooms/{roomId}/memory로 일원화, 투표 페이지 이동(filter=poll) 지원
  • Improvements
    • 정렬 키워드 정비(표시 라벨은 기존 유지)
    • 진행도·페이지 범위 필터 연동 및 업로드 진행 표시
    • 특정 조건(80% 진행도 미달) 시 안내 메시지 강화
  • Refactor
    • 메모리/좋아요 관련 타입 분리 및 공용화

@vercel

vercel Bot commented Aug 17, 2025

Copy link
Copy Markdown

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

Project Deployment Preview Comments Updated (UTC)
thip Ready Ready Preview Comment Aug 17, 2025 2:44pm

@coderabbitai

coderabbitai Bot commented Aug 17, 2025

Copy link
Copy Markdown

Walkthrough

방 게시물 좋아요 토글 API와 타입을 추가하고, 메모리(기억) 도메인 타입을 공용 모듈로 이전했습니다. RecordItem에 좋아요 연동 및 터치/롱프레스 제스처 로직을 도입했고, Memory 페이지의 데이터 로딩/정렬/필터 흐름을 단일 API 호출로 재구성했습니다. 일부 정렬 키와 라우팅 경로를 변경했습니다.

Changes

Cohort / File(s) Summary
Room post like API & types
src/api/roomPosts/postRoomPostLike.ts, src/types/roomPostLike.ts
좋아요 토글 POST 헬퍼 추가(/room-posts/{postId}/likes), 요청/응답 타입(RoomPostLikeRequest/Response/Data) 신설. 에러 로깅 후 재throw.
Memory 공용 타입 도입
src/types/memory.ts
Record, PollOption 공용 타입 신규 추가(필드에 isLiked 포함).
Memory 타입 소스 변경(컴파일 타임)
src/components/memory/MemoryContent/MemoryContent.tsx, src/components/memory/MemoryContent/RecordList.tsx, src/components/memory/RecordItem/PollRecord.tsx
타입 import 경로를 .../pages/memory/Memory.../types/memory로 변경. 런타임 로직 변화 없음.
RecordItem 기능 확장
src/components/memory/RecordItem/RecordItem.tsx
공용 타입 사용, record.id/isLiked 활용. 좋아요 API 연동(비동기 처리, 성공 시 상태 업데이트, 실패 코드별 스낵바). 터치/롱프레스 제스처 재구성(신고/수정/삭제 분기). usePopupActionsclosePopup 사용 제거.
Memory 페이지 데이터 흐름 재구성
src/pages/memory/Memory.tsx
로컬 타입 제거→공용 타입 사용. 파라미터(GetMemoryPostsParams) 기반 단일 getMemoryPosts 호출로 통합, 응답을 Record로 변환 저장. 40002 에러 특수 처리, 정렬/필터/페이지 범위 상태 및 핸들러 확장.
정렬 키 업데이트
src/components/memory/RecordFilters/FilterButtons.tsx, src/components/memory/SortDropdown.tsx
정렬 식별자 popularlike, commentscomment로 변경(라벨 유지). SortType 유니온 타입 갱신.
라우팅 경로 변경(메모리)
src/pages/groupDetail/ParticipatedGroupDetail.tsx
메모리/폴 경로를 /memory/{roomId}/rooms/{roomId}/memory로 통일. 폴 페이지 이동 핸들러 경로 수정.
MoreMenu 확장 포인트
src/stores/usePopupStore.ts
MoreMenuProps에 선택적 onReport 콜백 추가.

Sequence Diagram(s)

sequenceDiagram
  participant U as User
  participant RI as RecordItem
  participant API as postRoomPostLike
  participant S as Server
  U->>RI: Click Like
  RI->>API: postRoomPostLike(postId, { type: !isLiked, roomPostType })
  API->>S: POST /room-posts/{postId}/likes
  S-->>API: { isSuccess, data: { isLiked } }
  API-->>RI: RoomPostLikeResponse
  alt isSuccess
    RI->>RI: Update isLiked, likeCount
  else failure
    RI->>U: Show snackbar (error message)
  end
Loading
sequenceDiagram
  participant U as User
  participant M as Memory Page
  participant API as getMemoryPosts
  participant S as Server
  U->>M: Open /rooms/{roomId}/memory
  M->>M: Build GetMemoryPostsParams (roomId, tab, sort, filters)
  M->>API: getMemoryPosts(params)
  API->>S: GET /memory-posts
  S-->>API: { postList, isOverviewEnabled, message }
  API-->>M: Response
  M->>M: Map postList → Record[], set state
  M-->>U: Render MemoryContent
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

📬 API, ✨ Feature

Poem

봄밤 별빛, 방에 톡— 좋아요 한 점 찍고 ✨
페이지는 살랑, 추억은 토글로 반짝여요
토끼는 살금, 새 경로로 폴을 찾아 hoppity-hop
정렬 키도 싹— like와 comment로 깔끔하게
캬릉(?) 스낵바 한 입, 오늘도 배포 완료! 🐇💫

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 feat/api-rooms-like

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🔭 Outside diff range comments (2)
src/components/memory/RecordItem/RecordItem.tsx (2)

257-267: blur 상태 전달 누락으로 pointer-events 토글이 동작하지 않음

Container.styled에서 shouldBlur=true일 때 pointer-events를 none으로 바꾸도록 설계되어 있으나, 컴포넌트에 shouldBlur prop이 전달되지 않아 비활성화가 적용되지 않습니다. 또한 inline style로 filter를 중복 적용하고 있습니다.

-    <Container
-      onClick={handleClick}
-      onTouchStart={handleTouchStart}
-      onTouchMove={handleTouchMove}
-      onTouchEnd={handleTouchEnd}
-      style={{
-        filter: shouldBlur ? 'blur(4px)' : 'none',
-        transform: isPressed ? 'scale(0.98)' : 'scale(1)',
-        transition: 'transform 0.1s ease',
-        touchAction: 'manipulation',
-      }}
-    >
+    <Container
+      shouldBlur={shouldBlur}
+      onClick={handleClick}
+      onTouchStart={handleTouchStart}
+      onTouchMove={handleTouchMove}
+      onTouchEnd={handleTouchEnd}
+      style={{
+        transform: isPressed ? 'scale(0.98)' : 'scale(1)',
+        transition: 'transform 0.1s ease',
+        touchAction: 'manipulation',
+      }}
+    >

56-58: 타이머 타입 정의와 언마운트 정리

브라우저 환경에서는 setTimeout의 반환 타입이 number이므로 NodeJS.Timeout은 타입 불일치/경고를 유발할 수 있습니다. 또 언마운트 시 타이머를 정리하지 않아 메모리 누수/경고가 날 수 있습니다.

-  const longPressTimer = useRef<NodeJS.Timeout | null>(null);
+  const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);

컴포넌트 언마운트 시 타이머를 정리해 주세요:

// 추가 코드 (적절한 위치에)
useEffect(() => {
  return () => {
    if (longPressTimer.current) {
      clearTimeout(longPressTimer.current);
      longPressTimer.current = null;
    }
  };
}, []);
♻️ Duplicate comments (1)
src/components/memory/RecordFilters/FilterButtons.tsx (1)

39-50: 레거시 sort 값('popular', 'comments') 참조 잔존 여부 확인

상단 코멘트에서 안내한 레거시 문자열 검색 스크립트로 본 파일 외 사용처도 한 번 더 점검해 주세요.

🧹 Nitpick comments (18)
src/components/memory/SortDropdown.tsx (1)

34-38: 'as SortType' 캐스트 제거하고 옵션 리스트를 타입 안전하게 정의

불필요한 단언을 제거해 가독성과 타입 안전성을 높일 수 있습니다.

적용 제안:

-  const sortOptions = [
-    { value: 'latest' as SortType, label: '최신순' },
-    { value: 'like' as SortType, label: '인기순' },
-    { value: 'comment' as SortType, label: '댓글 많은순' },
-  ];
+  const sortOptions: ReadonlyArray<{ value: SortType; label: string }> = [
+    { value: 'latest', label: '최신순' },
+    { value: 'like', label: '인기순' },
+    { value: 'comment', label: '댓글 많은순' },
+  ];
src/components/memory/MemoryContent/RecordList.tsx (1)

1-1: 타입 이름 'Record'는 TS 내장 유틸리티 타입과 혼동 가능

전역 유틸 타입 Record<Keys, Type>와 이름이 동일해 가독성 저하 소지가 있습니다. 추후 여유 있을 때 MemoryRecord 등으로 리네이밍을 고려해 보세요.

src/components/memory/RecordItem/PollRecord.tsx (1)

2-2: 타입 이동 잘했어요. 경로 별칭 일관화 제안

공용 타입으로의 이동은 👍입니다. 다만 프로젝트 전반에서 @ 경로 별칭을 쓰는 파일(src/api/roomPosts/postRoomPostLike.ts 등)과 상대경로를 쓰는 곳이 섞여 있습니다. 본 파일도 별칭으로 통일하면 유지보수성이 좋아집니다.

적용 diff:

-import type { PollOption } from '../../../types/memory';
+import type { PollOption } from '@/types/memory';

추가 제안(선택): IntersectionObserver는 cleanup 시 disconnect()까지 호출하면 메모리/콜백 누수를 더 안전하게 방지할 수 있습니다. 또한 animate를 의존성에 두면 옵저버가 두 번 만들어질 수 있으니, 최초 1회만 등록하는 패턴을 고려해 보세요.

예시:

useEffect(() => {
  const observer = new IntersectionObserver(entries => {
    entries.forEach(entry => {
      if (entry.isIntersecting && !animate) {
        setTimeout(() => setAnimate(true), 100);
      }
    });
  }, { threshold: 0.3, rootMargin: '0px 0px -50px 0px' });

  const el = pollRef.current;
  if (el) observer.observe(el);

  return () => {
    if (el) observer.unobserve(el);
    observer.disconnect();
  };
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // 최초 1회만 등록
src/components/memory/MemoryContent/MemoryContent.tsx (1)

1-2: 타입 소스 분리 OK. 경로 별칭/타입 위치 정리 제안

  • Record를 공용 모듈로 옮긴 점은 좋습니다. 동일한 컨벤션으로 경로 별칭을 쓰면 더 일관됩니다.
  • RecordType, FilterType이 페이지 전용이 아니라 컴포넌트 여러 곳에서 공용으로 쓰인다면, 이들도 src/types/memory.ts로 승격해 단일 소스에서 가져오길 권장합니다.

경로 별칭 예시:

-import type { Record } from '../../../types/memory';
+import type { Record } from '@/types/memory';
src/api/roomPosts/postRoomPostLike.ts (2)

15-19: 에러 처리: Axios 에러 식별 및 메시지 정규화 권장

현재는 console.error 후 원본 에러를 그대로 rethrow합니다. 호출부에서 사용자 친화적 메시지를 일관되게 처리하려면 Axios 에러를 식별해 표준화된 Error로 변환하거나, 최소한 로그를 간결히 하고 원인 메시지를 보강해 주는 편이 좋습니다.

catch 블록 예시(diff는 본 블록 내부만):

-  } catch (error) {
-    console.error('방 게시물 좋아요 API 오류:', error);
-    throw error;
-  }
+  } catch (error) {
+    // 필요시 axios 에러를 식별해 사용자/로그 메시지를 정규화
+    const message =
+      (typeof error === 'object' && error !== null && 'message' in error && String((error as any).message)) ||
+      '방 게시물 좋아요 API 오류';
+    console.error('[postRoomPostLike] 실패:', message);
+    throw error;
+  }

Axios 전용 처리가 필요하면 다음을 추가해 사용할 수 있습니다:

import { isAxiosError } from 'axios';
// ...
if (isAxiosError(error)) {
  const apiMsg = error.response?.data?.message ?? error.message;
  // apiMsg로 표준화된 Error 생성/재던지기 등
}

21-64: 사용 예시 주석: Axios의 실패 응답 처리 흐름 명확화 요청

예시에서는 result.isSuccess 분기를 사용하고 있는데, 백엔드가 비즈니스 실패를 200으로 응답하지 않고 4xx/5xx로 응답한다면 Axios는 예외를 던져 result에 도달하지 못합니다. 실제 서버 규약이 어떤지에 따라 예시를 명확히 해 주세요.

  • 서버가 200 + isSuccess=false를 반환: 현재 예시처럼 result.isSuccess 분기 사용
  • 서버가 4xx/5xx로 반환: try/catch에서 에러 메시지 처리

권장 예시(두 케이스 모두 커버):

try {
  const result = await postRoomPostLike(postId, { type: !isLiked, roomPostType: 'RECORD' });
  if (result.isSuccess) {
    // result.data.isLiked 사용
  } else {
    // 비즈니스 실패 메시지 표시
  }
} catch (e) {
  // 네트워크/권한 등 HTTP 레벨 실패 처리
}
src/types/memory.ts (2)

59-74: Record.id 타입과 createdAt 변환 보장 여부 확인

  • Post.postIdnumber인데 Record.idstring입니다. 매핑 단계에서 문자열로 바꾸는 특별한 이유가 없다면 동일한 number로 두면 혼동이 줄어듭니다.
  • createdAt: Date를 사용하려면 변환 함수(convertPostToRecord)에서 postDate: string을 실제 Date로 변환하는 것이 보장되어야 합니다. 아니라면 string 유지가 안전합니다.

검토 제안:

-export interface Record {
-  id: string;
+export interface Record {
+  id: number;
   // ...
-  createdAt: Date;
+  createdAt: Date; // ← 변환이 항상 수행되는지 확인 필요

혹시 전역적으로 변경이 부담된다면, 주석으로 변환 보장을 명시하거나 createdAtstring으로 두고 UI에서 포맷팅 시점에 파싱하는 방식을 고려해 주세요.


77-82: PollOption.id의 UI 노출 의도 확인

PollRecord에서 <PollNumber>{option.id}</PollNumber>로 그대로 노출하고 있습니다. id가 내부 식별자라면 사용자에게는 index+1 같은 표시값이 더 자연스러울 수 있습니다. ID가 표시용이라면 label 또는 order 같은 별도 필드로 의도를 분리하는 것을 추천합니다.

간단 대안(표시용 번호는 index 사용):

{pollOptions.map((option, index) => (
  <PollNumber isHighest={option.isHighest}>{index + 1}</PollNumber>
))}
src/types/roomPostLike.ts (1)

13-19: likeCount 동기화가 서버 응답에 있다면 타입에 반영 고려

PR 설명에 “likeCount를 서버 응답에 따라 실시간 업데이트”가 포함되어 있습니다. 백엔드에서 최신 likeCount를 함께 내려준다면, 아래처럼 RoomPostLikeData에 포함시키면 호출부 로직이 단순해집니다. (서버 미지원이면 현 상태 유지)

가능 시 제안:

export interface RoomPostLikeData {
  postId: number; // 게시물 ID
  isLiked: boolean; // 좋아요 상태
+ likeCount: number; // 최신 좋아요 개수
}
src/components/memory/RecordItem/RecordItem.tsx (3)

66-66: parseInt 사용 시 진법 명시 권장

런타임 차단 이슈는 아니지만 일관성을 위해 radix(10) 명시를 권장합니다.

-      const postId = parseInt(id);
+      const postId = parseInt(id, 10);

82-104: 에러 코드 매핑을 상수화/테이블화하여 간결성·유지보수성 향상

if-else 체인 대신 코드→메시지 매핑 객체를 사용하면 가독성과 변경 용이성이 좋아집니다.

-        // 에러 메시지에 따른 사용자 알림
-        let errorMessage = '좋아요 처리 중 오류가 발생했습니다.';
-
-        if (response.code === 140011) {
-          errorMessage = '방 접근 권한이 없습니다.';
-        } else if (response.code === 185001) {
-          errorMessage = '이미 좋아요한 게시물입니다.';
-        } else if (response.code === 185002) {
-          errorMessage = '좋아요하지 않은 게시물은 취소할 수 없습니다.';
-        } else if (response.code === 100009) {
-          errorMessage = '잘못된 게시물 타입입니다.';
-        } else if (response.code === 110009) {
-          errorMessage = '존재하지 않는 게시물입니다.';
-        } else if (response.code === 40500) {
-          errorMessage = '허용되지 않는 HTTP 메소드입니다.';
-        }
+        const errorMessages: Record<number, string> = {
+          140011: '방 접근 권한이 없습니다.',
+          185001: '이미 좋아요한 게시물입니다.',
+          185002: '좋아요하지 않은 게시물은 취소할 수 없습니다.',
+          100009: '잘못된 게시물 타입입니다.',
+          110009: '존재하지 않는 게시물입니다.',
+          40500: '허용되지 않는 HTTP 메소드입니다.',
+        };
+        const errorMessage =
+          (response.code && errorMessages[response.code]) ||
+          '좋아요 처리 중 오류가 발생했습니다.';

69-73: RoomPostLikeRequest 스키마 확인 요청

요청 바디의 필드명이 type: boolean으로 전달되는데, 의미상 isLike 또는 like가 자연스러워 보입니다. 현재 API 스키마가 type을 요구하는 것이 맞는지 확인 부탁드립니다. 스키마가 boolean 이름을 제공한다면 그에 맞춰 변경하는 것이 혼란을 줄입니다.

src/pages/memory/Memory.tsx (6)

31-37: voteItems 접근 안전성 확인 필요

텍스트 포스트의 경우 post.voteItems가 undefined일 가능성이 있다면 .map에서 런타임 오류가 날 수 있습니다. API 보장이 없다면 안전하게 처리하는 방식을 권장합니다.

-    pollOptions: post.voteItems.map((item, index) => ({
+    pollOptions: (post.voteItems ?? []).map((item, index) => ({
       id: item.voteItemId.toString(),
       text: item.itemName,
       percentage: item.percentage,
       isHighest: index === 0,
     })),

71-104: 디버그 로그 정리 권장

console.log가 다수 남아 있습니다. 개발 편의가 끝났다면 제거하거나 환경변수(예: NODE_ENV !== 'production')로 가드하세요. 로그가 민감 데이터/PII를 포함할 소지가 있다면 더더욱 주의가 필요합니다.


118-126: 특정 에러 코드(40002) 분기 처리 LGTM

총평 권한(80% 진행률) 에러를 사용자 메시지와 함께 필터 해제로 처리한 흐름이 좋습니다. 추가로, 자주 쓰이는 에러 코드는 상수/enum으로 중앙집중화하면 유지보수성이 향상됩니다.


162-166: 불필요한 useMemo 정리 또는 실제 정렬 로직 구현 제안

현재 sortedRecordscurrentRecords를 그대로 반환합니다. 정렬이 서버에서만 이뤄진다면 이 useMemo는 제거 가능하고, 클라이언트 보조 정렬이 필요하다면 이 자리에서 구현하는 편이 명확합니다.


236-239: 임시 값(읽기 진행률, 현재 페이지) 하드코딩

readingProgresscurrentUserPage가 임시 하드코딩되어 있어 UX 오해 소지가 있습니다. 표시 자체를 숨기거나, API 연동 완료 전에는 placeholder UI로 대체하는 것을 권장합니다.


79-79: parseInt 진법 명시(사소)

작은 스타일 이슈지만, 일관성을 위해 radix(10) 명시를 권장합니다.

-        roomId: parseInt(roomId),
+        roomId: parseInt(roomId, 10),
📜 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 9929319 and fed5cca.

📒 Files selected for processing (12)
  • src/api/roomPosts/postRoomPostLike.ts (1 hunks)
  • src/components/memory/MemoryContent/MemoryContent.tsx (1 hunks)
  • src/components/memory/MemoryContent/RecordList.tsx (1 hunks)
  • src/components/memory/RecordFilters/FilterButtons.tsx (1 hunks)
  • src/components/memory/RecordItem/PollRecord.tsx (1 hunks)
  • src/components/memory/RecordItem/RecordItem.tsx (6 hunks)
  • src/components/memory/SortDropdown.tsx (2 hunks)
  • src/pages/groupDetail/ParticipatedGroupDetail.tsx (1 hunks)
  • src/pages/memory/Memory.tsx (7 hunks)
  • src/stores/usePopupStore.ts (1 hunks)
  • src/types/memory.ts (1 hunks)
  • src/types/roomPostLike.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/api/roomPosts/postRoomPostLike.ts (2)
src/types/roomPostLike.ts (2)
  • RoomPostLikeRequest (2-5)
  • RoomPostLikeResponse (14-19)
src/api/index.ts (1)
  • apiClient (7-14)
src/pages/memory/Memory.tsx (2)
src/types/memory.ts (2)
  • Record (59-74)
  • GetMemoryPostsParams (29-38)
src/api/memory/getMemoryPosts.ts (1)
  • getMemoryPosts (5-51)
src/components/memory/RecordItem/RecordItem.tsx (3)
src/hooks/usePopupActions.ts (1)
  • usePopupActions (9-35)
src/api/roomPosts/postRoomPostLike.ts (1)
  • postRoomPostLike (5-19)
src/components/memory/RecordItem/RecordItem.styled.ts (1)
  • Container (4-9)
🔇 Additional comments (11)
src/stores/usePopupStore.ts (1)

18-19: MoreMenu에 신고(onReport) 콜백 추가: LGTM

메뉴 액션 확장에 자연스럽게 부합합니다. 컴포넌트 레벨에서 onReport 유무에 따라 항목 표시/비활성화만 일관되게 처리해 주세요.

src/components/memory/RecordFilters/FilterButtons.tsx (1)

43-46: 정렬 라벨 매핑 업데이트: LGTM

새 정렬 키('like', 'comment')에 맞춰 라벨 매핑이 정상 반영되었습니다.

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

134-136: poll 클릭 시 쿼리 파라미터(filter=poll) 허용 범위 확인

메모리 페이지의 필터 파라미터에서 'poll'이 유효한 값인지(타입, 파싱, 초기 렌더 조건 포함) 점검해 주세요. API/페이지 양쪽에서 허용 값 집합이 일치하는지 확인을 권장합니다.


138-140: 댓글 섹션 이동 경로: LGTM

/today-words/:roomId로의 내비게이션이 기존 플로우와 일치합니다.


127-133: 라우트 정의 및 리디렉트 검토 필요
rg 검색 결과 /rooms/:roomId/memory 경로를 찾지 못했습니다.

  • 라우터 설정 파일(예: App.tsx, routes.tsx 등)에서 /rooms/:roomId/memory가 정확히 등록되어 있는지 확인
  • 기존 경로(/memory/:roomId)로 진입 시 /rooms/:roomId/memory로 리디렉트 로직이 필요한지 검토
src/components/memory/MemoryContent/RecordList.tsx (1)

1-1: 공용 타입 모듈로 import 경로 정리: LGTM

메모리 타입 중앙화 방향과 일치합니다.

src/api/roomPosts/postRoomPostLike.ts (1)

5-14: API 래퍼 구성 깔끔합니다

엔드포인트 구성, 제네릭 응답 타입 적용, response.data 반환까지 목적에 맞게 잘 작성되었습니다. UI 쪽에서 토글 상태와 카운트를 반영하기 수월하겠습니다.

src/types/roomPostLike.ts (1)

2-5: 요청 타입 정의 적절합니다

서버 계약(좋아요/취소 boolean + 포스트 타입)을 정확히 캡처하고 있어 호출부에서 혼동이 적겠습니다.

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

3-3: 공용 타입으로의 이전 LGTM

메모리 도메인 Record 타입을 공용 모듈에서 가져오도록 정리한 부분 좋습니다.

src/pages/memory/Memory.tsx (2)

15-31: isLiked 매핑 추가 LGTM

API의 Post → 앱의 Record로 isLiked를 반영한 부분이 정확합니다. 타입 이전과 함께 일관성 있게 처리되었습니다.


79-86: 정렬 파라미터 전달 방식 변경 확인 요청

group 탭일 때만 params.sort = selectedSort를 전달하는 로직은 API 스펙(getMemoryPosts.ts)과 일치합니다. 다만 SortType이 'latest' | 'like' | 'comment'로 갱신된 것이 맞는지, 드롭다운/서버 간 값 호환성을 한번 더 점검해 주세요.

Comment on lines +50 to 52
// 좋아요 상태 관리 - record 객체에서 isLiked 속성 가져오기
const [isLiked, setIsLiked] = useState(record.isLiked || false);
const [currentLikeCount, setCurrentLikeCount] = useState(likeCount);

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

좋아요 토글: 중복 요청 방지와 카운트 음수 방지, 초기 상태 표현 개선 권장

  • 빠른 연타 시 중복 요청이 발생해 비정상 상태가 될 수 있습니다. in-flight 플래그를 두어 재진입을 방지하세요.
  • 언팔 시 likeCount가 음수로 내려갈 수 있습니다. 0으로 클램핑하세요.
  • 초기화 시 || false 대신 ?? false가 의도에 더 정확합니다(명시적 false가 그대로 유지됨).

아래와 같이 보완을 제안합니다.

-  const [isLiked, setIsLiked] = useState(record.isLiked || false);
+  const [isLiked, setIsLiked] = useState(record.isLiked ?? false);
+  const [liking, setLiking] = useState(false);
   const handleLikeClick = async () => {
+    if (liking) return; // 중복 요청 방지
+    setLiking(true);
     try {
       const postId = parseInt(id);
       const roomPostType = type === 'poll' ? 'VOTE' : 'RECORD';

       const response = await postRoomPostLike(postId, {
         type: !isLiked, // 현재 상태 반대로 전송
         roomPostType,
       });

       if (response.isSuccess) {
         // 서버 응답으로 상태 업데이트
-        setIsLiked(response.data.isLiked);
-        setCurrentLikeCount((prev: number) => (response.data.isLiked ? prev + 1 : prev - 1));
+        const nextIsLiked = response.data.isLiked;
+        setIsLiked(nextIsLiked);
+        setCurrentLikeCount((prev: number) =>
+          nextIsLiked ? prev + 1 : Math.max(0, prev - 1),
+        );
         console.log('좋아요 상태 변경 성공:', response.data.isLiked);
       } else {
         console.error('좋아요 상태 변경 실패:', response.message);
         // ...
       }
     } catch (error) {
       console.error('좋아요 API 호출 실패:', error);
       openSnackbar({
         message: '네트워크 오류가 발생했습니다. 다시 시도해주세요.',
         variant: 'top',
         onClose: () => {},
       });
-    }
+    } finally {
+      setLiking(false);
+    }
   };

추가로, 서버가 likeCount를 응답해준다면 그 값을 신뢰하는 것이 가장 안전합니다. 응답 스키마에 따라 setCurrentLikeCount(response.data.likeCount) 사용을 고려해 주세요.

Also applies to: 63-81

🤖 Prompt for AI Agents
In src/components/memory/RecordItem/RecordItem.tsx around lines 50-52 (and also
apply the same changes to lines 63-81), the like state logic needs protection
against rapid duplicate requests, negative counts, and incorrect initial falsy
handling: replace the initial state expression to use the nullish coalescing
operator (record.isLiked ?? false), add an in-flight (isLiking) flag to prevent
re-entry while an async like/unlike request is pending, clamp the like count
when decrementing so it never goes below 0, and on success set the count from
the server response (e.g., setCurrentLikeCount(response.data.likeCount)) rather
than relying solely on local increments/decrements. Ensure the in-flight flag is
cleared in both success and error paths so subsequent toggles work.

Comment on lines +194 to 216
// 길게 누르기 이벤트 핸들러
const handleTouchStart = useCallback(
(e: React.TouchEvent) => {
if (isMyRecord) return;

pressStartPos.current = { x: clientX, y: clientY };
hasTriggeredLongPress.current = false;
setIsPressed(true);
hasTriggeredLongPress.current = false;
pressStartPos.current = {
x: e.touches[0].clientX,
y: e.touches[0].clientY,
};

longPressTimer.current = setTimeout(() => {
if (!hasTriggeredLongPress.current) {
hasTriggeredLongPress.current = true;
handleLongPress();
}
}, 500);
hasTriggeredLongPress.current = true;
setIsPressed(false);

openMoreMenu({
onReport: handleReport,
});
}, 800);
},
[handleLongPress],
[isMyRecord, openMoreMenu, handleReport],
);

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

⚠️ Potential issue

액션 영역에서 롱프레스/컨테이너 클릭과 충돌: 이벤트 전파 차단 필요

PR 목적에 명시된 “액션 영역 제외 + stopPropagation”이 현재 반영되어 있지 않아, 좋아요/댓글 버튼 터치가 롱프레스(신고)나 소유자 메뉴(onClick)로 번질 수 있습니다. 실제로 지금은 액션 버튼을 누르면 컨테이너 onClick이 함께 실행될 수 있습니다.

아래와 같이 액션 영역에서 이벤트 전파를 차단하세요.

-      <ActionSection>
-        <ActionButton onClick={handleLikeClick}>
+      <ActionSection
+        onClick={e => e.stopPropagation()}
+        onTouchStart={e => e.stopPropagation()}
+      >
+        <ActionButton
+          onClick={e => {
+            e.stopPropagation();
+            handleLikeClick();
+          }}
+          onTouchStart={e => e.stopPropagation()}
+          aria-pressed={isLiked}
+          aria-label={isLiked ? '좋아요 취소' : '좋아요'}
+        >
           <img
             src={isLiked ? heartFilledIcon : heartIcon}
             alt={isLiked ? '좋아요 취소' : '좋아요'}
           />
           <span>{currentLikeCount}</span>
         </ActionButton>
-        <ActionButton>
+        <ActionButton
+          onClick={e => e.stopPropagation()}
+          onTouchStart={e => e.stopPropagation()}
+          aria-label="댓글 보기"
+        >
           <img src={commentIcon} alt="댓글" />
           <span>{commentCount}</span>
         </ActionButton>
       </ActionSection>

이렇게 하면 액션 영역에서의 터치/클릭이 컨테이너의 롱프레스/클릭과 분리됩니다.

Also applies to: 218-232, 241-254, 285-296

🤖 Prompt for AI Agents
In src/components/memory/RecordItem/RecordItem.tsx around lines 194-216 (and
similarly for 218-232, 241-254, 285-296), touch/click events from the action
area are currently propagating to the container causing long-press/report and
owner onClick to fire; update the action-area event handlers to call
e.stopPropagation() (and where appropriate e.preventDefault()) at the start of
the handler so action button touches/clicks do not bubble to the parent,
ensuring the action area’s touchstart/click handlers return early after stopping
propagation.

Comment on lines +4 to 5
export type SortType = 'latest' | 'like' | 'comment';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

SortType 변경(‘popular’→‘like’, ‘comments’→‘comment’)에 따른 레거시 사용처 점검 필요

정렬 키가 바뀐 만큼, 기존에 'popular'/'comments'를 참조하는 호출부가 남아 있지 않은지 확인해 주세요. URL 쿼리, 로컬스토리지, API 파라미터 변환부까지 포함해 점검을 권장합니다.

다음 스크립트로 레거시 문자열 리터럴 사용처를 탐색할 수 있습니다:


🏁 Script executed:

#!/bin/bash
# 'popular' 리터럴 사용처
rg -nP -C2 "(?<=[\"'])popular(?=[\"'])" --glob '!**/node_modules/**'

# 'comments' 리터럴 사용처(문자열 리터럴만)
rg -nP -C2 "(?<=[\"'])comments(?=[\"'])" --glob '!**/node_modules/**'

Length of output: 800


SortType 변경에 따른 ‘popular’ → ‘like’ 호출부 업데이트 필요

다음 파일에서 여전히 'popular' 리터럴이 사용되고 있어, 모두 'like'로 변경해 주세요. ‘comments’ 리터럴은 검색되지 않았습니다.

  • src/pages/group/Group.tsx
    • Line 19:
      - listType: 'deadline' | 'popular',
      + listType: 'deadline' | 'like',
    • Lines 51–53:
      - const popularGroups = response.data.popularRoomList.map(room =>
      -   convertRoomItemToGroup(room, category, 'popular'),
      - );
      + const popularGroups = response.data.popularRoomList.map(room =>
      +   convertRoomItemToGroup(room, category, 'like'),
      + );

추가로 URL 쿼리, 로컬스토리지, API 요청·응답 매핑 등에서 legacy 키가 남아 있지 않은지 다시 한번 검토해 주세요.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/memory/SortDropdown.tsx around lines 4–5 and in
src/pages/group/Group.tsx at line 19 and lines 51–53, update any occurrences of
the legacy 'popular' sort literal to 'like' so they match the new SortType
definition; adjust any switch/case, props, or function calls accordingly, and
then scan URL query params, localStorage keys, and API request/response mappings
related to sorting to remove or convert any remaining legacy 'popular' keys to
'like' (update serialization/deserialization and defaults where needed).

@ljh130334 ljh130334 merged commit 9de997c into develop Aug 17, 2025
3 checks passed
@ljh130334 ljh130334 deleted the feat/api-rooms-like branch August 19, 2025 01:58
@coderabbitai coderabbitai Bot mentioned this pull request Aug 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant