✨ Feat: 커뮤니티 게시물 반응 기능 구현 - #59
Conversation
- 게시글 좋아요/싫어요 반응 API 연동 - 게시글 상세에서 반응 추가, 변경, 취소 기능 구현 - 반응 후 상세/목록 UI가 즉시 반영되도록 낙관적 업데이트 적용 - 커뮤니티 목록에서는 좋아요 수만 노출되도록 유지 - 404, 409 등 반응 관련 예외 상황 메시지 처리 `DoDo-Project#56`
- 게시글 상세 조회 응답의 좋아요/싫어요 수를 상세 화면에 반영 - 게시글 반응 API 생성/변경/취소 로직 정리 - 내가 누른 반응이 상세 화면에서 active 상태로 유지되도록 개선 - 반대 반응 선택 시 기존 반응 변경 흐름이 정상 동작하도록 수정 - 내 게시물 반응 시 권한 에러 대신 안내 토스트가 노출되도록 변경 - 중복 반응(409) 상황을 에러 문구 대신 토스트로 안내하도록 개선 - 커뮤니티 목록은 기존처럼 좋아요 수만 노출되도록 유지 `DoDo-Project#56`
|
@sooloin is attempting to deploy a commit to the sooloin's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Code Review
This pull request introduces a board reaction feature (like/dislike) to the community module, including API integrations, utility functions for reaction state management, UI updates with interactive reaction buttons, and a custom useBoardReaction hook utilizing React Query for optimistic updates. The reviewer feedback highlights several key improvement opportunities: removing the redundant reactionOverride local state in BoardDetailPage to prevent state synchronization issues and clean up unused code, refactoring updateBoardListResponse with generics to ensure type safety across different response structures, and consistently using the query key factory instead of hardcoded query keys.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const [reactionError, setReactionError] = useState(''); | ||
| const [reactionNotice, setReactionNotice] = useState(''); | ||
| const [reactionOverride, setReactionOverride] = useState<ReactionOverrideState | null>(null); | ||
|
|
||
| const canManage = Boolean(board && nickname && board.nickname.trim() === nickname.trim()); | ||
| const serverReactionType = getBoardReactionType(board); | ||
| const currentReactionType = | ||
| reactionOverride?.boardId === boardId ? reactionOverride.reactionType : serverReactionType; |
There was a problem hiding this comment.
useBoardReaction 훅 내부에서 React Query의 setQueryData 및 setQueriesData를 통해 상세 정보, 목록, 내 게시글 목록에 대해 이미 낙관적 업데이트(Optimistic Update)를 수행하고 있습니다. 따라서 BoardDetailPage에서 별도의 reactionOverride 로컬 상태를 유지할 필요가 없으며, 이는 상태 동기화 문제를 야기할 수 있습니다.
특히 성공 시에 reactionOverride를 null로 초기화하는 로직이 누락되어 있어, 요청이 성공한 이후에도 서버의 최신 상태 대신 로컬 오버라이드 상태가 계속 유지되는 버그가 존재합니다.
로컬 상태를 제거하고 React Query의 캐시 상태(board 객체)를 직접 사용하도록 단순화하는 것을 권장합니다.
| const [reactionError, setReactionError] = useState(''); | |
| const [reactionNotice, setReactionNotice] = useState(''); | |
| const [reactionOverride, setReactionOverride] = useState<ReactionOverrideState | null>(null); | |
| const canManage = Boolean(board && nickname && board.nickname.trim() === nickname.trim()); | |
| const serverReactionType = getBoardReactionType(board); | |
| const currentReactionType = | |
| reactionOverride?.boardId === boardId ? reactionOverride.reactionType : serverReactionType; | |
| const [reactionError, setReactionError] = useState(''); | |
| const [reactionNotice, setReactionNotice] = useState(''); | |
| const canManage = Boolean(board && nickname && board.nickname.trim() === nickname.trim()); | |
| const currentReactionType = getBoardReactionType(board); |
| const previousReactionType = currentReactionType; | ||
| const nextReactionType = getNextReactionType(previousReactionType, reactionType); | ||
|
|
||
| try { | ||
| setReactionError(''); | ||
| setReactionNotice(''); | ||
| setReactionOverride({ boardId, reactionType: nextReactionType }); | ||
| await reactToBoard({ | ||
| boardId, | ||
| nextReactionType: reactionType, | ||
| currentReactionType: previousReactionType, | ||
| }); | ||
| } catch (reactionActionError) { | ||
| setReactionOverride(null); | ||
| const status = getApiErrorStatus(reactionActionError); |
There was a problem hiding this comment.
reactionOverride 로컬 상태를 제거함에 따라, handleBoardReaction 내의 오버라이드 설정 및 에러 발생 시 초기화 코드를 제거할 수 있습니다. 이를 통해 핸들러 로직이 훨씬 단순해지고 가독성이 향상됩니다.
const previousReactionType = currentReactionType;
try {
setReactionError('');
setReactionNotice('');
await reactToBoard({
boardId,
nextReactionType: reactionType,
currentReactionType: previousReactionType,
});
} catch (reactionActionError) {
const status = getApiErrorStatus(reactionActionError);
| function getNextReactionType(currentReactionType: 'LIKE' | 'DISLIKE' | null, clickedReactionType: 'LIKE' | 'DISLIKE') { | ||
| return currentReactionType === clickedReactionType ? null : clickedReactionType; | ||
| } | ||
|
|
||
| interface ReactionOverrideState { | ||
| boardId: number | null; | ||
| reactionType: 'LIKE' | 'DISLIKE' | null; | ||
| } |
| function updateBoardListResponse( | ||
| data: BoardListResponse, | ||
| boardId: number, | ||
| previousReaction: ReactionType | null, | ||
| nextReaction: ReactionType | null, | ||
| ) { | ||
| return { | ||
| ...data, | ||
| boards: data.boards.map((board) => | ||
| board.boardId === boardId ? applyReactionToBoardListItem(board, previousReaction, nextReaction) : board, | ||
| ), | ||
| }; | ||
| } |
There was a problem hiding this comment.
updateBoardListResponse 함수는 현재 BoardListResponse 타입만 인자로 받도록 정의되어 있습니다. 하지만 onMutate에서 MyBoardListResponse 타입의 쿼리 데이터를 업데이트할 때도 이 함수가 사용되고 있습니다.
두 타입이 현재는 구조적으로 동일하지만, 향후 명세가 달라질 경우 타입 에러가 발생하거나 런타임 오류로 이어질 수 있습니다. 제네릭을 사용하여 boards 필드를 가진 모든 객체에 대해 안전하게 동작하도록 개선하는 것이 좋습니다.
| function updateBoardListResponse( | |
| data: BoardListResponse, | |
| boardId: number, | |
| previousReaction: ReactionType | null, | |
| nextReaction: ReactionType | null, | |
| ) { | |
| return { | |
| ...data, | |
| boards: data.boards.map((board) => | |
| board.boardId === boardId ? applyReactionToBoardListItem(board, previousReaction, nextReaction) : board, | |
| ), | |
| }; | |
| } | |
| function updateBoardListResponse<T extends { boards: BoardListItem[] }>( | |
| data: T, | |
| boardId: number, | |
| previousReaction: ReactionType | null, | |
| nextReaction: ReactionType | null, | |
| ): T { | |
| return { | |
| ...data, | |
| boards: data.boards.map((board) => | |
| board.boardId === boardId ? applyReactionToBoardListItem(board, previousReaction, nextReaction) : board, | |
| ), | |
| }; | |
| } |
| await Promise.all([ | ||
| queryClient.cancelQueries({ queryKey: queryKeys.boards.detail(boardId) }), | ||
| queryClient.cancelQueries({ queryKey: queryKeys.boards.listInfinite() }), | ||
| queryClient.cancelQueries({ queryKey: ['boards', 'mine'] }), |
- 게시글 반응 상태를 localStorage 보조값으로 유지하도록 개선 - 상세 API에 반응 타입이 없을 때도 마지막 반응 active 상태가 유지되도록 처리 - 반응 성공 및 취소 시 저장된 반응 상태를 함께 갱신하도록 수정 - 내 게시글 반응 및 중복 반응 상황을 토스트로 안내하도록 유지 - 게시글 반응 관련 mine 쿼리 키를 queryKeys 팩토리로 통일 - 반응 목록 업데이트 헬퍼를 제네릭으로 개선해 재사용성 보완 `DoDo-Project#56`
📄 작업 내용 (Description)
🔗 관련 이슈 (Related Issues)
✅ 체크리스트 (Checklist)
Style)Test)📸 스크린샷 (Screenshots)
💬 기타 사항 (Etc)