Skip to content

✨ Feat: 커뮤니티 게시물 반응 기능 구현 - #59

Merged
sooloin merged 3 commits into
DoDo-Project:developfrom
sooloin:feature/56
Jun 14, 2026
Merged

✨ Feat: 커뮤니티 게시물 반응 기능 구현#59
sooloin merged 3 commits into
DoDo-Project:developfrom
sooloin:feature/56

Conversation

@sooloin

@sooloin sooloin commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

📄 작업 내용 (Description)

이번 PR에서 변경되거나 추가된 주요 작업을 간단히 설명해주세요.

  • 커뮤니티 게시글 좋아요/싫어요 반응 기능 구현
  • 반응 추가, 변경, 취소 API 연동
  • 게시글 상세 화면에 좋아요/싫어요 수 및 반응 버튼 반영
  • 커뮤니티 목록에서는 좋아요 수만 보이도록 유지
  • 내가 누른 반응이 active 상태로 유지되도록 처리
  • 반대 반응 선택 시 기존 반응이 정상적으로 변경되도록 수정
  • 내 게시물 반응 시 안내 토스트가 노출되도록 UX 개선
  • 중복 반응(409) 상황을 토스트로 안내하도록 처리

🔗 관련 이슈 (Related Issues)

작업한 이슈 번호를 아래 형식으로 PULL REQUEST BODY에 작성해주세요.
(PR 머지 시 해당 이슈가 자동으로 종료됩니다.)


✅ 체크리스트 (Checklist)

PR을 보내기 전 아래 항목들을 모두 확인해주세요.

  • PR 제목은 커밋 컨벤션을 따랐습니다.
  • 관련 이슈를 연결했습니다.
  • 스스로 코드를 검토하고 불필요한 코드를 제거했습니다.
  • 코드 스타일이 프로젝트 규칙과 일치합니다. (Style)
  • 새로운 기능에 대한 테스트 코드를 추가했거나, 기존 테스트가 모두 통과했습니다. (Test)

📸 스크린샷 (Screenshots)

작업 내용과 관련된 스크린샷이 있다면 첨부해주세요. (UI 변경이 있는 경우)

image

💬 기타 사항 (Etc)

리뷰어에게 전달하고 싶은 추가 정보가 있다면 자유롭게 작성해주세요.

sooloin added 2 commits June 14, 2026 01:14
- 게시글 좋아요/싫어요 반응 API 연동
- 게시글 상세에서 반응 추가, 변경, 취소 기능 구현
- 반응 후 상세/목록 UI가 즉시 반영되도록 낙관적 업데이트 적용
- 커뮤니티 목록에서는 좋아요 수만 노출되도록 유지
- 404, 409 등 반응 관련 예외 상황 메시지 처리

`DoDo-Project#56`
- 게시글 상세 조회 응답의 좋아요/싫어요 수를 상세 화면에 반영
- 게시글 반응 API 생성/변경/취소 로직 정리
- 내가 누른 반응이 상세 화면에서 active 상태로 유지되도록 개선
- 반대 반응 선택 시 기존 반응 변경 흐름이 정상 동작하도록 수정
- 내 게시물 반응 시 권한 에러 대신 안내 토스트가 노출되도록 변경
- 중복 반응(409) 상황을 에러 문구 대신 토스트로 안내하도록 개선
- 커뮤니티 목록은 기존처럼 좋아요 수만 노출되도록 유지

`DoDo-Project#56`
@sooloin
sooloin requested review from WhiteBin-bin and limhb708 June 14, 2026 13:53
@sooloin sooloin self-assigned this Jun 14, 2026
@sooloin sooloin added ✨ Feature 새 기능 혹은 요구 사항 😸 조수빈 조수빈 파트 labels Jun 14, 2026
@vercel

vercel Bot commented Jun 14, 2026

Copy link
Copy Markdown

@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.

@vercel

vercel Bot commented Jun 14, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
dodo-frontend Ready Ready Preview, Comment Jun 14, 2026 2:07pm

@gemini-code-assist gemini-code-assist 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.

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.

Comment thread src/pages/community/BoardDetailPage.tsx Outdated
Comment on lines +75 to +82
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

useBoardReaction 훅 내부에서 React Query의 setQueryDatasetQueriesData를 통해 상세 정보, 목록, 내 게시글 목록에 대해 이미 낙관적 업데이트(Optimistic Update)를 수행하고 있습니다. 따라서 BoardDetailPage에서 별도의 reactionOverride 로컬 상태를 유지할 필요가 없으며, 이는 상태 동기화 문제를 야기할 수 있습니다.

특히 성공 시에 reactionOverridenull로 초기화하는 로직이 누락되어 있어, 요청이 성공한 이후에도 서버의 최신 상태 대신 로컬 오버라이드 상태가 계속 유지되는 버그가 존재합니다.

로컬 상태를 제거하고 React Query의 캐시 상태(board 객체)를 직접 사용하도록 단순화하는 것을 권장합니다.

Suggested change
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);

Comment thread src/pages/community/BoardDetailPage.tsx Outdated
Comment on lines +207 to +221
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

reactionOverride 로컬 상태를 제거함에 따라, handleBoardReaction 내의 오버라이드 설정 및 에러 발생 시 초기화 코드를 제거할 수 있습니다. 이를 통해 핸들러 로직이 훨씬 단순해지고 가독성이 향상됩니다.

    const previousReactionType = currentReactionType;

    try {
      setReactionError('');
      setReactionNotice('');
      await reactToBoard({
        boardId,
        nextReactionType: reactionType,
        currentReactionType: previousReactionType,
      });
    } catch (reactionActionError) {
      const status = getApiErrorStatus(reactionActionError);

Comment thread src/pages/community/BoardDetailPage.tsx Outdated
Comment on lines +40 to +47
function getNextReactionType(currentReactionType: 'LIKE' | 'DISLIKE' | null, clickedReactionType: 'LIKE' | 'DISLIKE') {
return currentReactionType === clickedReactionType ? null : clickedReactionType;
}

interface ReactionOverrideState {
boardId: number | null;
reactionType: 'LIKE' | 'DISLIKE' | null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

reactionOverride 로컬 상태를 제거함에 따라 더 이상 사용되지 않는 getNextReactionType 함수와 ReactionOverrideState 인터페이스입니다. 코드 정리 및 미사용 코드 제거를 위해 삭제하는 것을 권장합니다.

Comment on lines +20 to +32
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,
),
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

updateBoardListResponse 함수는 현재 BoardListResponse 타입만 인자로 받도록 정의되어 있습니다. 하지만 onMutate에서 MyBoardListResponse 타입의 쿼리 데이터를 업데이트할 때도 이 함수가 사용되고 있습니다.

두 타입이 현재는 구조적으로 동일하지만, 향후 명세가 달라질 경우 타입 에러가 발생하거나 런타임 오류로 이어질 수 있습니다. 제네릭을 사용하여 boards 필드를 가진 모든 객체에 대해 안전하게 동작하도록 개선하는 것이 좋습니다.

Suggested change
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'] }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

queryKeys.boards.detail(boardId)queryKeys.boards.listInfinite()와 같이 쿼리 키 팩토리를 일관되게 사용하고 있는 반면, 내 게시글 목록의 경우 ['boards', 'mine']으로 하드코딩되어 있습니다.

쿼리 키의 일관된 관리와 유지보수성을 위해 queryKeys 객체에 정의된 함수(예: queryKeys.boards.mine())가 있다면 이를 사용하도록 변경하는 것을 권장합니다.

- 게시글 반응 상태를 localStorage 보조값으로 유지하도록 개선
- 상세 API에 반응 타입이 없을 때도 마지막 반응 active 상태가 유지되도록 처리
- 반응 성공 및 취소 시 저장된 반응 상태를 함께 갱신하도록 수정
- 내 게시글 반응 및 중복 반응 상황을 토스트로 안내하도록 유지
- 게시글 반응 관련 mine 쿼리 키를 queryKeys 팩토리로 통일
- 반응 목록 업데이트 헬퍼를 제네릭으로 개선해 재사용성 보완

`DoDo-Project#56`
@sooloin
sooloin merged commit 492df0c into DoDo-Project:develop Jun 14, 2026
3 checks passed
@sooloin sooloin changed the title ✨ Feat: 커뮤니티 게시글 반응 기능 구현 ✨ Feat: 커뮤니티 게시물 반응 기능 구현 Jun 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새 기능 혹은 요구 사항 😸 조수빈 조수빈 파트

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 커뮤니티 기능 구현 [Feat] 커뮤니티 게시물 좋아요/싫어요 반응 기능 구현

2 participants