Skip to content

[Fix] WTH-325 : 게시판 리팩토링 및 좋아요 삭제 api 연결#70

Merged
nabbang6 merged 18 commits intodevelopfrom
WTH-325-게시판-리팩토링-및-이미지-오류-수정
Apr 27, 2026

Hidden character warning

The head ref may contain hidden characters: "WTH-325-\uac8c\uc2dc\ud310-\ub9ac\ud329\ud1a0\ub9c1-\ubc0f-\uc774\ubbf8\uc9c0-\uc624\ub958-\uc218\uc815"
Merged

[Fix] WTH-325 : 게시판 리팩토링 및 좋아요 삭제 api 연결#70
nabbang6 merged 18 commits intodevelopfrom
WTH-325-게시판-리팩토링-및-이미지-오류-수정

Conversation

@nabbang6
Copy link
Copy Markdown
Collaborator

@nabbang6 nabbang6 commented Apr 24, 2026

✅ PR 유형

어떤 변경 사항이 있었나요?

  • 새로운 기능 추가
  • 버그 수정
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

📌 관련 이슈번호

  • Closed #325

✅ Key Changes

게시글 상세 디자인 변경

  • 상세 페이지 레이아웃 순서 변경: 제목 → 작성자 → 구분선 → 본문
  • PostCard.Body, PostCard.Title을 독립 컴포넌트로 분리하여 상세/목록에서 재사용

게시판 URL 구조 개선 (/board/[id] → /board/[boardId]/[postId])

  • 기존 /board/[id] 단일 파라미터 구조에서 /board/[boardId]/[postId] 2-depth 구조로 변경
  • boardId가 없는 경우를 위한 fallback 경로 /board/posts/[postId] 추가
  • URL에서 boardId를 읽어 활성 채널을 자동 동기화하도록 BoardNavClient 개선
  • buildPostPath, buildBoardPath 유틸 함수 추가하여 경로 생성 로직 일원화

내비게이션 가드 버그 수정

  • 게시글 작성/수정 성공 후 allowNavigation을 호출하여 가드가 페이지 이동을 차단하지 않도록 수정
  • usePostStore에 _allowNavigation 상태 추가, PostEditorShell에서 가드 해제 함수를 주입
  • popstate 리스너 중복 및 beforeUnload 정리 로직 간소화
  • 홈에서 글쓰기 페이지 이동 시 activeBoardId를 null로 리셋

이미지 업로드 시 원본 비율 유지

  • ImageList의 이미지 클래스를 w-fullw-auto object-contain으로 변경하여
    원본 비율 보존

게시글 좋아요 취소 API 연결 및 캐시 버그 수정

  • 기존 toggleLike (POST만 사용) → addLike (POST) / removeLike (DELETE) 분리
  • 좋아요 상태(wasLiked)를 mutationFn에 전달하여 optimistic update와 실제
    요청의 불일치 해소
  • toggledLike 헬퍼 함수로 좋아요 토글 로직 중복 제거

📸 스크린샷 or 실행영상

2026-04-24.221633.mp4

🎸 기타 사항 or 추가 코멘트

  • RecentPost, UnreadNotice 타입에 boardId 필드 추가
  • useNoticeBoardId 훅 추가 — 홈 대시보드에서 공지사항 게시판 링크 생성 시 사용

현재 최신 게시글 조회 (/clubs/{clubId}/dashboard/recent-posts) 시 boardId를 응답으로 받아올 수 없어 /{clubId}/board/posts/{postId}로 이동되게 해두었습니다! (이외에도 boardId를 받아올 수 없는 경우가 생기면 해당 URL로 이동되게 처리)
요건 배겐드 분들께 요청 드리면 바로 해결될 예정입니당

URL 구조 수정으로 파일 변경사항이 많은데... ㅜ ㅜ
page.tsxerror.tsx는 확인하지 않아주셔도 괜찮습니닷,,,

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 게시물 콘텐츠 안전 렌더링 및 확장(더보기) 지원 추가
    • 좋아요 동작을 명확히 분리(좋아요/취소)하여 반영
  • 개선사항

    • 게시판/게시물 라우팅 및 뒤로가기 내비게이션 개선
    • 게시물 상세·목록 네비게이션 일관성 강화(공유 URL 빌더 사용)
    • 홈/공지 섹션의 링크·첨부 처리 개선
    • 이미지 적응형 표시 방식 개선
  • 스타일

    • 본문 헤딩에 일관된 구분선·간격 스타일 적용

@nabbang6 nabbang6 requested review from JIN921, dalzzy and woneeeee April 24, 2026 13:25
@nabbang6 nabbang6 self-assigned this Apr 24, 2026
@nabbang6 nabbang6 added 🐞 BugFix Something isn't working 📬 API 서버 API 통신 🔨 Refactor 코드 리팩토링 labels Apr 24, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

보드별 라우트·푸터와 게시물 상세 경로를 분리하고, PostCard 렌더링을 Body 컴포넌트로 위임했으며 좋아요 API를 add/remove로 분리하고 경로 빌더·스토어 동기화를 추가했습니다.

Changes

Cohort / File(s) Summary
보드 라우트·푸터
src/app/(private)/[clubId]/(main)/board/(with-nav)/@footer/[boardId]/page.tsx, src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/page.tsx, src/app/(private)/[clubId]/(main)/board/(with-nav)/page.tsx
[boardId] 전용 페이지·푸터 라우트 추가, params 검증(정수 검사), 기본 BoardContent에 boardId={null} 전달
게시물 상세 및 에러/로딩
src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/page.tsx, .../[postId]/PostDetailContent.tsx, .../[postId]/loading.tsx, .../posts/[postId]/page.tsx, .../posts/[postId]/error.tsx, .../posts/[postId]/loading.tsx
route params에 boardId·postId 분리·검증, 삭제·네비게이션 대상에 boardId 반영, 에러·로딩 컴포넌트 추가/정리, PostDetail 레이아웃·간격 조정
네비게이션 동기화·BoardNav
src/app/(private)/[clubId]/(main)/board/(with-nav)/BoardNavClient.tsx, src/app/(private)/[clubId]/(main)/board/(with-nav)/layout.tsx
URL boardId를 읽어 activeBoardId 동기화(유효성/클리어 처리), BoardNav를 Suspense+로더로 분리하여 비동기 로드
BoardContent 및 홈 연동
src/components/board/BoardContent.tsx, src/components/home/HomeBoardContent.tsx
BoardContentboardId prop 사용하도록 변경; 게시물 링크·댓글 네비게이션을 buildPostPath로 통일; 홈 컴포넌트도 경로·첨부 처리 통일
PostCard 분해·렌더링
src/components/board/PostCard/PostCardBody.tsx, src/components/board/PostCard/PostCardDetailContent.tsx, src/components/board/PostCard/PostCardTitle.tsx, src/components/board/PostCard/index.tsx
HTML 정제(DOMPurify), 코드 하이라이트, 라인클램프를 담당하는 PostCardBody 추가; DetailContent가 Body로 위임; Title 레이아웃·아이콘 크기 중앙화; PostCard 네임스페이스에 Title/Body 노출
좋아요 API·뮤테이션 리팩터
src/lib/apis/board.ts, src/hooks/board/useToggleLike.ts
boardApi.toggleLike 제거, addLike/removeLike 추가; useToggleLike가 wasLiked 기반 분기, 낙관 업데이트·롤백·서버 동기화 로직 재구성
경로 헬퍼·스토어·훅
src/lib/board.ts, src/stores/usePostStore.ts, src/hooks/board/useBoardQuery.ts, src/hooks/board/useCreatePost.ts, src/hooks/board/useUpdatePost.ts, src/hooks/home/useWritePost.ts, src/hooks/useNavigationGuard.ts
buildPostPath/buildBoardPath 추가 및 사용으로 경로 중앙화; _allowNavigation 스토어 필드·setter 추가 및 전파; useNoticeBoardId 추가; 네비게이션 가드·포스트 생성/업데이트 로직 조정
홈 공지·Unread 변경
src/components/home/NoticeBoardBox.tsx, src/components/home/UnreadNoticeBox.tsx
공지·미확인 공지 링크를 buildPostPath 사용으로 변경, noticeBoardId 유도 로직 도입
이미지·에디터·헤더·에디터셸 등 마이너
src/components/board/ImageList/ImageList.tsx, src/components/board/Editor/index.tsx, src/components/board/PostEditorShell.tsx, src/components/board/PostDetailHeader.tsx, src/components/layout/header/PostingActions.tsx
멀티이미지 스타일 변경(w-auto/object-contain), 에디터 주석 제거, PostEditorShell에서 allowNavigation 동기화, PostDetailHeader 뒤로가기에 boardId 반영, edit 경로 감지 정규식 완화
스타일·타입·패키지
src/app/globals.css, src/types/board.ts, src/types/home.ts, package.json
ProseMirror 제목/구분선 스타일 통일, PostBase.boardName optional로 변경, UnreadNotice/RecentPostboardId? 추가, dompurify 의존성 추가

Sequence Diagram(s)

(생성 조건에 부합하지 않아 생략)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Possibly related PRs

Suggested reviewers

  • JIN921
  • dalzzy
  • woneeeee

Poem

🐰 보드 길 따라 아이디가 반짝,
카드 속 글은 깔끔히 다듬어,
경로는 한 줄로 모이고,
좋아요는 명확히 오가며,
토끼가 박수 쳐요 — 홧팅! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 PR의 핵심 변경사항인 게시판 리팩토링과 좋아요 삭제 API 연결을 명확하게 요약하고 있습니다.
Description check ✅ Passed 설명이 PR 유형, 관련 이슈번호, Key Changes를 포함하고 있으나 기본 템플릿의 필수 섹션이 일부 누락되어 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch WTH-325-게시판-리팩토링-및-이미지-오류-수정

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

🤖 Claude 테스트 제안

모델: claude-sonnet-4-6 | 토큰: 0 입력 / 0 출력

변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.

src/app/(private)/[clubId]/(main)/board/(with-nav)/@footer/[boardId]/[postId]/page.tsx

오류: Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.


src/app/(private)/[clubId]/(main)/board/(with-nav)/@footer/[boardId]/page.tsx

오류: Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.


src/app/(private)/[clubId]/(main)/board/(with-nav)/BoardNavClient.tsx

오류: Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.


src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/PostDetailContent.tsx

오류: Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.


src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/error.tsx

오류: Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.


이 코멘트는 Claude API를 통해 자동 생성되었습니다. 반드시 검토 후 사용하세요.

@github-actions
Copy link
Copy Markdown

PR 테스트 결과

Jest: 통과

🎉 모든 테스트를 통과했습니다!

@github-actions
Copy link
Copy Markdown

PR 검증 결과

TypeScript: 통과
ESLint: 통과
Prettier: 실패
Build: 통과

⚠️ 일부 검증에 실패했습니다. 확인 후 수정해주세요.

@github-actions
Copy link
Copy Markdown

구현한 기능 Preview: https://weeth-piu217ba2-weethsite-4975s-projects.vercel.app

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: 8

🧹 Nitpick comments (9)
src/app/globals.css (1)

654-685: 하드코딩된 16px 대신 디자인 토큰 사용 권장.

h1::aftermargin-top: 16px 및 하단 결합 선택자의 margin-top: 16px는 이미 정의된 --margin-300(16px)과 동일한 값입니다. 토큰을 사용하면 추후 간격 조정 시 일관성을 유지하기 쉽습니다. .ProseMirror hr의 배경색은 var(--line)을 잘 사용하고 있으니 간격 값도 동일한 방향으로 정리하면 좋습니다.

As per coding guidelines: "Always use design token classes first; no hardcoded values."

♻️ 제안 diff
 .ProseMirror h1::after {
   content: '';
   display: block;
-  margin-top: 16px;
+  margin-top: var(--margin-300);
   height: 1px;
   background-color: var(--line);
 }
@@
 .ProseMirror hr {
   margin: 0;
   border: none;
   height: 1px;
   background-color: var(--line);
 }

 /* heading·구분선 전후 16px 간격, p+p는 0 */
 .ProseMirror > * + :is(h1, h2, h3, hr),
 .ProseMirror > :is(h1, h2, h3, hr) + * {
-  margin-top: 16px;
+  margin-top: var(--margin-300);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/globals.css` around lines 654 - 685, Replace the hardcoded 16px
spacing with the design token --margin-300: update the rules targeting
.ProseMirror h1::after (margin-top: 16px) and the grouping selector .ProseMirror
> * + :is(h1, h2, h3, hr), .ProseMirror > :is(h1, h2, h3, hr) + * (margin-top:
16px) to use var(--margin-300) instead; keep .ProseMirror hr's background-color
as-is and ensure no other hardcoded 16px remains in those selectors so spacing
follows the design token.
src/components/board/PostEditorShell.tsx (1)

35-42: allowNavigation 콜백 참조 최적화 검토

useNavigationGuard에서 allowNavigation이 매 렌더마다 새로 생성되고 있습니다. React Compiler가 활성화되어 있어 자동 최적화가 도움이 되지만, PostEditorShell.tsx의 useEffect에서 allowNavigation을 명시적으로 의존성 배열에 포함했을 때는 여전히 함수 참조 변화로 인해 effect 정리→재실행 사이클이 발생하며, zustand 스토어에 null → 콜백 → null을 반복 기록합니다.

useNavigationGuardallowNavigationuseCallback으로 감싸면 불필요한 effect 순환과 스토어 쓰기를 줄일 수 있습니다.

추가로 컴포넌트 단 콜백을 전역 스토어로 우회 전달하는 패턴의 가독성 개선도 검토하면 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/board/PostEditorShell.tsx` around lines 35 - 42, The
allowNavigation callback from useNavigationGuard is being recreated each render
causing PostEditorShell's useEffect to repeatedly unset and reset the zustand
value; update useNavigationGuard to memoize and return allowNavigation wrapped
in React.useCallback (keeping its dependencies minimal and explicit) so the
reference is stable across renders, then confirm PostEditorShell's useEffect can
safely depend on allowNavigation without triggering unnecessary cleanup cycles
(also consider whether passing a stable wrapper or a small adapter function to
usePostStore.setAllowNavigation improves readability).
src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/PostDetailContent.tsx (1)

104-108: Number(boardIdParam)의 NaN 방어

라우팅 매칭 특성상 일반적으로는 숫자만 들어오지만, boardIdParam이 유효 숫자가 아닐 때 Number(boardIdParam)NaN이 되며 buildBoardPathboardId != null 분기(NaN != null은 true)를 타 /board/NaN 경로로 리다이렉트됩니다. 방어적으로 정수 검증을 추가해 주세요.

🛠️ 제안 diff
-              onDeleted={() =>
-                router.push(
-                  buildBoardPath(clubIdParam, boardIdParam ? Number(boardIdParam) : undefined),
-                )
-              }
+              onDeleted={() => {
+                const parsed = boardIdParam ? Number(boardIdParam) : NaN;
+                router.push(
+                  buildBoardPath(clubIdParam, Number.isInteger(parsed) ? parsed : undefined),
+                );
+              }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/app/`(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/PostDetailContent.tsx
around lines 104 - 108, The onDeleted handler uses Number(boardIdParam) which
can produce NaN and cause buildBoardPath to treat the value as present (NaN !=
null) and generate /board/NaN; validate boardIdParam before converting: parse it
as an integer and only pass a numeric boardId to buildBoardPath if
Number.isInteger(parsed) (or use isFinite and >0 check as appropriate),
otherwise pass undefined; update the onDeleted -> router.push call to compute a
safeBoardId from boardIdParam and pass that to buildBoardPath so
invalid/non-numeric params don't redirect to /board/NaN.
src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/page.tsx (1)

10-22: URL의 boardId와 실제 게시글 boardId 불일치 처리 검토

params.boardId는 타입에만 있고 검증/사용되지 않습니다. 사용자가 수동으로 /clubId/board/{잘못된boardId}/{postId}를 입력해도 페이지는 정상 렌더링되며, 이 경우 BoardNavClient는 URL의 boardId(잘못된 값)로, PostDetailContentuseEffect는 실제 currentPost.boardId로 각각 activeBoardId를 설정해 사이드바 하이라이트 깜빡임/오류가 발생할 수 있습니다. 서버 페이지에서 postIdNum 검증처럼 boardId도 정수 검증 후 response.data.boardId와 일치하지 않으면 정상 경로로 redirect 처리하는 것을 고려해 주세요.

🛠️ 제안 diff
-import { notFound } from 'next/navigation';
+import { notFound, redirect } from 'next/navigation';
 ...
 export default async function PostDetailPage({ params }: PostDetailPageProps) {
-  const { clubId, postId } = await params;
+  const { clubId, boardId, postId } = await params;
   const postIdNum = Number(postId);
-  if (!Number.isInteger(postIdNum)) notFound();
+  const boardIdNum = Number(boardId);
+  if (!Number.isInteger(postIdNum) || !Number.isInteger(boardIdNum)) notFound();

   const response = await boardServerApi.getPostById(clubId, postIdNum).catch((error) => {
     if (error?.response?.status === 404) return null;
     throw error;
   });
   if (!response?.data) notFound();
+  if (response.data.boardId !== boardIdNum) {
+    redirect(`/${clubId}/board/${response.data.boardId}/${postIdNum}`);
+  }

   return <PostDetailContent initialData={response.data} />;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/app/`(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/page.tsx
around lines 10 - 22, PostDetailPage currently ignores params.boardId causing UI
mismatch; parse and validate params.boardId (e.g., const boardIdNum =
Number(params.boardId)), check Number.isInteger(boardIdNum), then after fetching
via boardServerApi.getPostById compare boardIdNum to response.data.boardId; if
they differ, perform a server-side redirect to the canonical URL using
response.data.boardId (or call notFound() if invalid) so BoardNavClient and
PostDetailContent receive the same active boardId. Ensure you update
PostDetailPage to import and use the proper server redirect (or notFound) and
keep error handling around getPostById as-is.
src/app/(private)/[clubId]/(main)/board/(with-nav)/BoardNavClient.tsx (1)

33-34: 정규식이 clubId가 숫자일 때 오탐 가능성

/\/board\/\d+\/\d+$/는 경로 끝의 두 숫자 세그먼트만 보므로 {clubId}가 숫자일 경우에도 .../board/{boardId}/{postId}$가 아닌 .../board/{clubId}/{boardId} 같은 구조로도 오탐할 여지가 있습니다(현재 라우팅 구조에서는 안전하지만 URL 스킴이 바뀌면 취약). 라우트 파라미터 기반(boardIdParam, postIdParam) 판정이 더 견고합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(private)/[clubId]/(main)/board/(with-nav)/BoardNavClient.tsx around
lines 33 - 34, The regex checks isDetailPage and isPostsRoute can false-positive
when clubId is numeric; replace those regex-based guards with route-parameter
based checks using the parsed params (e.g., boardIdParam and postIdParam) and
the existing pathname only for context if needed: detect a detail page by
verifying both boardIdParam and postIdParam are present and valid, and detect
the posts route by verifying boardIdParam exists and postIdParam is absent (or
matches the posts route param), updating the logic in the component where
isDetailPage and isPostsRoute are defined to rely on these route params instead
of the fragile /\/board\/\d+\/\d+$/ and /\/board\/posts\/\d+$/ regexes.
src/hooks/useNavigationGuard.ts (1)

77-106: history.pushState/replaceState 전역 패치 — 동시성 주의

이 구현은 effect 내에서 history.pushState/replaceState를 원본으로 교체하고 cleanup에서 복구합니다. 만약 동일 컴포넌트 트리 내에서 여러 가드 인스턴스가 활성화되거나, 외부 라이브러리가 사이에 patch를 추가할 경우, cleanup 순서에 따라 원본이 아닌 패치된 버전이 복구되어 호출 체인이 손상될 수 있습니다.

현재 app에서는 각 useNavigationGuard 인스턴스가 서로 다른 페이지(post detail, post editor, profile edit, admin attendance)에서 활성화되므로 실제 발생 가능성이 매우 낮습니다. 다만 향후 아키텍처 변경으로 단일 페이지에 여러 가드가 공존하게 된다면, 참조 카운팅이나 Next.js useRouter.events 같은 이벤트 기반 접근으로 refactoring 검토를 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useNavigationGuard.ts` around lines 77 - 106, The global patching
of history.pushState/history.replaceState in useNavigationGuard can clobber
other patches when multiple guards or third-party patches exist; change the
cleanup to only restore the originals if the current global functions still
match the wrapped functions installed by this hook (i.e., save references to the
wrapped functions created by interceptNavigation and on unmount check if
history.pushState === thisWrappedPushState before assigning originalPushState,
likewise for replaceState), or implement a simple reference-counting/registry so
multiple instances compose instead of blindly restoring the saved original in
the cleanup; update symbols: history.pushState, history.replaceState,
interceptNavigation, originalPushState, originalReplaceState, and the hook’s
cleanup logic in useNavigationGuard.
src/hooks/board/useToggleLike.ts (2)

38-68: onMutate에서 반환한 wasLiked가 사용되지 않습니다.

mutationFnmutate(isLiked)로 전달된 인자를 사용하고, onErrorpreviousDetail/previousHomePosts만 참조하므로 컨텍스트에 담긴 wasLiked는 어디에서도 소비되지 않습니다. 제거하여 혼란을 줄이거나, 반대로 mutationFncontext.wasLiked를 쓰도록 일원화하는 것을 고려해 보세요 (mutationFn 호출 시점에는 컨텍스트 접근이 불가하므로 현재 구조를 유지한다면 단순 제거가 맞습니다).

♻️ 제안 diff
-      return { previousDetail, previousHomePosts, wasLiked };
+      return { previousDetail, previousHomePosts };

또한 wasLiked 로컬 변수 자체도 더 이상 필요하지 않다면 함께 제거할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/board/useToggleLike.ts` around lines 38 - 68, In useToggleLike.ts,
remove the unused wasLiked value from the onMutate block: delete the local
wasLiked assignment and stop returning it in the onMutate context (return only
previousDetail and previousHomePosts), since mutationFn receives mutate(isLiked)
directly and onError only uses previousDetail/previousHomePosts; leave
mutationFn and onError unchanged to preserve behavior.

16-21: likeCount 하한 방어 검토.

캐시가 서버와 어긋나 isLiked=true && likeCount=0 상태에서 removeLike 낙관적 업데이트가 실행되면 likeCount-1로 표시됩니다. 가드 추가를 권장합니다.

🛡️ 제안 diff
 function toggledLike(like: PostLike): PostLike {
   return {
     isLiked: !like.isLiked,
-    likeCount: like.isLiked ? like.likeCount - 1 : like.likeCount + 1,
+    likeCount: like.isLiked ? Math.max(0, like.likeCount - 1) : like.likeCount + 1,
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/board/useToggleLike.ts` around lines 16 - 21, The toggledLike
function can produce a negative likeCount when isLiked is true and likeCount is
0; update toggledLike(PostLike) to clamp likeCount to a minimum of 0 when
decrementing (e.g., compute newCount for the isLiked → false branch and use
Math.max(0, newCount) or equivalent) so toggling never yields negative values
while preserving the existing increment logic for the false → true branch.
src/components/board/PostCard/PostCardBody.tsx (1)

17-31: 두 개의 ref를 콜백으로 병합하는 부분 단순화 여지.

useCodeHighlightRefObject<HTMLElement | null>을 받고, useLineClamp는 내부에서 자체 ref를 반환합니다. 현재는 같은 DOM에 contentRef.currentref.current를 수동으로 동기화하고 있는데, expandable=false일 때 useLineClamp는 어차피 early-return 하므로 항상 ref를 JSX에 연결하고 useCodeHighlight에도 같은 ref를 넘기는 편이 더 단순하고 의도가 명확합니다.

♻️ 제안 diff
-  const contentRef = useRef<HTMLDivElement>(null);
-  const { ref, isClamped, isExpanded, setIsExpanded } = useLineClamp<HTMLDivElement>(
-    expandable,
-    content,
-  );
-
-  useCodeHighlight(contentRef, content);
+  const { ref, isClamped, isExpanded, setIsExpanded } = useLineClamp<HTMLDivElement>(
+    expandable,
+    content,
+  );
+
+  useCodeHighlight(ref, content);
@@
-      <div
-        ref={(el) => {
-          contentRef.current = el;
-          if (expandable) ref.current = el;
-        }}
+      <div
+        ref={ref}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/board/PostCard/PostCardBody.tsx` around lines 17 - 31, Replace
the manual syncing of two refs by using the single ref returned from
useLineClamp for both the DOM element and the code-highlighting hook: stop using
the separate contentRef and instead pass the useLineClamp ref to
useCodeHighlight and attach that ref directly to the div; update any typings so
the ref passed to useCodeHighlight matches RefObject<HTMLElement | null> and
remove the callback that sets contentRef.current and ref.current in the JSX.
This simplifies the logic around useCodeHighlight, useLineClamp, ref, isClamped,
isExpanded and setIsExpanded in PostCardBody.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`(private)/[clubId]/(main)/board/(with-nav)/[boardId]/page.tsx:
- Around line 7-12: BoardByIdPage currently converts params.boardId to Number
and passes null to BoardContent on invalid input, causing inconsistent behavior;
update BoardByIdPage to validate boardId strictly (reject non-numeric, empty
string, NaN, and non-integer values) and call notFound() when validation fails
(matching posts/[postId]/page.tsx behavior) instead of passing null to
BoardContent; locate BoardByIdPage and the BoardContent usage to implement the
guard and ensure boardId is a proper integer before rendering.

In `@src/app/`(private)/[clubId]/(main)/board/(with-nav)/BoardNavClient.tsx:
- Around line 49-63: The current useEffect handling boardIdParam can leave
activeBoardId stale when boardIdParam exists but fails Number.isInteger; update
the block inside the useEffect (referencing useEffect, boardIdParam,
activeBoardId, setActiveBoardId, Number.isInteger, isDetailPage, isPostsRoute)
so that if boardIdParam is present but not a valid integer you explicitly call
setActiveBoardId(null) to clear the active board; preserve the existing behavior
that when boardIdParam is absent you only reset to null when not isDetailPage
and not isPostsRoute.

In `@src/components/board/BoardContent.tsx`:
- Line 99: The JSX prop line for onComment is exceeding Prettier's max line
length; extract the computed path into a nearby constant (e.g., commentPath)
using buildPostPath(clubId, post.id, post.boardId) + '#comments' or a template
literal, then replace the long inline arrow with onComment={() =>
router.push(commentPath)} to satisfy Prettier and keep the JSX line short;
reference the onComment prop, router.push call, buildPostPath, clubId, post.id
and post.boardId when making the change.

In `@src/components/board/PostCard/PostCardBody.tsx`:
- Around line 27-38: The PostCardBody component is directly injecting untrusted
HTML via dangerouslySetInnerHTML (see the div using dangerouslySetInnerHTML and
the content variable), which risks XSS; sanitize the content before rendering by
calling a sanitizer (e.g., DOMPurify.sanitize(content)) and use the sanitized
result in dangerouslySetInnerHTML, or if you opt not to sanitize here, add a
clear comment and API contract asserting the server always returns sanitized
HTML; update references to content in the ref callback and className usage to
use the sanitized variable (e.g., sanitizedContent) so only cleaned HTML is
injected.

In `@src/components/board/PostCard/PostCardTitle.tsx`:
- Line 19: The PostCardTitle component uses arbitrary gap utilities
('gap-[10px]' and 'gap-[5px]') which are not present in the design tokens;
update the className expression in PostCardTitle to use the closest existing
spacing tokens (for example replace 'gap-[10px]' with 'gap-300' or 'gap-200' and
'gap-[5px]' with 'gap-200' or 'gap-100' as appropriate) or, if 5px/10px are
required, coordinate with design to add new spacing tokens and then use those
new token classes; adjust the conditional in the div's className (the cn call)
so it references the chosen token names instead of arbitrary pixel values.

In `@src/components/home/HomeBoardContent.tsx`:
- Line 115: The onComment handler line in HomeBoardContent.tsx is too long for
Prettier; break the expression so it fits the print width — e.g., compute the
URL separately or split the router.push call across multiple lines using the
template string parts; target the onComment prop where
router.push(`${buildPostPath(clubId, post.id, post.boardId)}#comments`) is used
and refactor to a shorter/line-broken form referencing buildPostPath, clubId,
post.id, and post.boardId so Prettier passes.

In `@src/components/home/NoticeBoardBox.tsx`:
- Around line 34-39: The onClick handler currently guards navigation by checking
notices[0], which prevents navigation to the notice board when the board exists
but has zero notices; update the logic in the onClick (and any rendering
condition for the "전체보기" button with aria-label="공지 전체보기") to instead check
presence of noticeBoardId (or hide the button when noticeBoardId is absent) and
then call router.push(buildBoardPath(clubId, noticeBoardId)); ensure you
reference the notices array only for rendering notice items, not for enabling
the board navigation button.

In `@src/hooks/board/useToggleLike.ts`:
- Around line 38-43: onMutate cancels queries for detailKey, listKey and
homePostsKey but does not perform an optimistic update for listKey, causing UI
inconsistency; modify the onMutate handler to read and store the current list
state (e.g., previousList via queryClient.getQueryData(listKey)), perform an
optimistic update to the list data (toggling the like for the affected item in
the cached list), and return a context containing previousList so the mutation's
onError can restore it; use the same pattern already applied for
detailKey/homePostsKey and reference onMutate, listKey, detailKey, homePostsKey,
previousList and queryClient when making these changes.

---

Nitpick comments:
In
`@src/app/`(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/page.tsx:
- Around line 10-22: PostDetailPage currently ignores params.boardId causing UI
mismatch; parse and validate params.boardId (e.g., const boardIdNum =
Number(params.boardId)), check Number.isInteger(boardIdNum), then after fetching
via boardServerApi.getPostById compare boardIdNum to response.data.boardId; if
they differ, perform a server-side redirect to the canonical URL using
response.data.boardId (or call notFound() if invalid) so BoardNavClient and
PostDetailContent receive the same active boardId. Ensure you update
PostDetailPage to import and use the proper server redirect (or notFound) and
keep error handling around getPostById as-is.

In
`@src/app/`(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/PostDetailContent.tsx:
- Around line 104-108: The onDeleted handler uses Number(boardIdParam) which can
produce NaN and cause buildBoardPath to treat the value as present (NaN != null)
and generate /board/NaN; validate boardIdParam before converting: parse it as an
integer and only pass a numeric boardId to buildBoardPath if
Number.isInteger(parsed) (or use isFinite and >0 check as appropriate),
otherwise pass undefined; update the onDeleted -> router.push call to compute a
safeBoardId from boardIdParam and pass that to buildBoardPath so
invalid/non-numeric params don't redirect to /board/NaN.

In `@src/app/`(private)/[clubId]/(main)/board/(with-nav)/BoardNavClient.tsx:
- Around line 33-34: The regex checks isDetailPage and isPostsRoute can
false-positive when clubId is numeric; replace those regex-based guards with
route-parameter based checks using the parsed params (e.g., boardIdParam and
postIdParam) and the existing pathname only for context if needed: detect a
detail page by verifying both boardIdParam and postIdParam are present and
valid, and detect the posts route by verifying boardIdParam exists and
postIdParam is absent (or matches the posts route param), updating the logic in
the component where isDetailPage and isPostsRoute are defined to rely on these
route params instead of the fragile /\/board\/\d+\/\d+$/ and
/\/board\/posts\/\d+$/ regexes.

In `@src/app/globals.css`:
- Around line 654-685: Replace the hardcoded 16px spacing with the design token
--margin-300: update the rules targeting .ProseMirror h1::after (margin-top:
16px) and the grouping selector .ProseMirror > * + :is(h1, h2, h3, hr),
.ProseMirror > :is(h1, h2, h3, hr) + * (margin-top: 16px) to use
var(--margin-300) instead; keep .ProseMirror hr's background-color as-is and
ensure no other hardcoded 16px remains in those selectors so spacing follows the
design token.

In `@src/components/board/PostCard/PostCardBody.tsx`:
- Around line 17-31: Replace the manual syncing of two refs by using the single
ref returned from useLineClamp for both the DOM element and the
code-highlighting hook: stop using the separate contentRef and instead pass the
useLineClamp ref to useCodeHighlight and attach that ref directly to the div;
update any typings so the ref passed to useCodeHighlight matches
RefObject<HTMLElement | null> and remove the callback that sets
contentRef.current and ref.current in the JSX. This simplifies the logic around
useCodeHighlight, useLineClamp, ref, isClamped, isExpanded and setIsExpanded in
PostCardBody.

In `@src/components/board/PostEditorShell.tsx`:
- Around line 35-42: The allowNavigation callback from useNavigationGuard is
being recreated each render causing PostEditorShell's useEffect to repeatedly
unset and reset the zustand value; update useNavigationGuard to memoize and
return allowNavigation wrapped in React.useCallback (keeping its dependencies
minimal and explicit) so the reference is stable across renders, then confirm
PostEditorShell's useEffect can safely depend on allowNavigation without
triggering unnecessary cleanup cycles (also consider whether passing a stable
wrapper or a small adapter function to usePostStore.setAllowNavigation improves
readability).

In `@src/hooks/board/useToggleLike.ts`:
- Around line 38-68: In useToggleLike.ts, remove the unused wasLiked value from
the onMutate block: delete the local wasLiked assignment and stop returning it
in the onMutate context (return only previousDetail and previousHomePosts),
since mutationFn receives mutate(isLiked) directly and onError only uses
previousDetail/previousHomePosts; leave mutationFn and onError unchanged to
preserve behavior.
- Around line 16-21: The toggledLike function can produce a negative likeCount
when isLiked is true and likeCount is 0; update toggledLike(PostLike) to clamp
likeCount to a minimum of 0 when decrementing (e.g., compute newCount for the
isLiked → false branch and use Math.max(0, newCount) or equivalent) so toggling
never yields negative values while preserving the existing increment logic for
the false → true branch.

In `@src/hooks/useNavigationGuard.ts`:
- Around line 77-106: The global patching of
history.pushState/history.replaceState in useNavigationGuard can clobber other
patches when multiple guards or third-party patches exist; change the cleanup to
only restore the originals if the current global functions still match the
wrapped functions installed by this hook (i.e., save references to the wrapped
functions created by interceptNavigation and on unmount check if
history.pushState === thisWrappedPushState before assigning originalPushState,
likewise for replaceState), or implement a simple reference-counting/registry so
multiple instances compose instead of blindly restoring the saved original in
the cleanup; update symbols: history.pushState, history.replaceState,
interceptNavigation, originalPushState, originalReplaceState, and the hook’s
cleanup logic in useNavigationGuard.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1f91241b-11d4-478e-a92d-02e848a764d7

📥 Commits

Reviewing files that changed from the base of the PR and between ebf6da0 and f179d2d.

📒 Files selected for processing (35)
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/@footer/[boardId]/[postId]/page.tsx
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/@footer/[boardId]/page.tsx
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/BoardNavClient.tsx
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/PostDetailContent.tsx
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/error.tsx
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/page.tsx
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/page.tsx
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/page.tsx
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/posts/[postId]/error.tsx
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/posts/[postId]/page.tsx
  • src/app/globals.css
  • src/components/board/BoardContent.tsx
  • src/components/board/Editor/index.tsx
  • src/components/board/ImageList/ImageList.tsx
  • src/components/board/PostCard/PostCardBody.tsx
  • src/components/board/PostCard/PostCardDetailContent.tsx
  • src/components/board/PostCard/PostCardTitle.tsx
  • src/components/board/PostCard/index.tsx
  • src/components/board/PostDetailHeader.tsx
  • src/components/board/PostEditorShell.tsx
  • src/components/home/HomeBoardContent.tsx
  • src/components/home/NoticeBoardBox.tsx
  • src/components/home/UnreadNoticeBox.tsx
  • src/components/layout/header/PostingActions.tsx
  • src/hooks/board/useBoardQuery.ts
  • src/hooks/board/useCreatePost.ts
  • src/hooks/board/useToggleLike.ts
  • src/hooks/board/useUpdatePost.ts
  • src/hooks/home/useWritePost.ts
  • src/hooks/useNavigationGuard.ts
  • src/lib/apis/board.ts
  • src/lib/board.ts
  • src/stores/usePostStore.ts
  • src/types/board.ts
  • src/types/home.ts
💤 Files with no reviewable changes (1)
  • src/components/board/Editor/index.tsx

Comment thread src/components/board/BoardContent.tsx Outdated
commentCount={post.commentCount}
isLiked={post.like.isLiked}
onComment={() => router.push(`/${clubId}/board/${post.id}#comments`)}
onComment={() => router.push(`${buildPostPath(clubId, post.id, post.boardId)}#comments`)}
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 | 🟡 Minor

Prettier 포맷 오류 해결 필요

CI에서 Prettier --check 실패가 보고되었습니다. 이 라인이 길이 제한을 초과해 포맷이 깨진 것으로 보입니다. 포맷을 적용해 주세요.

🛠️ 제안 diff
-            onComment={() => router.push(`${buildPostPath(clubId, post.id, post.boardId)}#comments`)}
+            onComment={() =>
+              router.push(`${buildPostPath(clubId, post.id, post.boardId)}#comments`)
+            }
📝 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
onComment={() => router.push(`${buildPostPath(clubId, post.id, post.boardId)}#comments`)}
onComment={() =>
router.push(`${buildPostPath(clubId, post.id, post.boardId)}#comments`)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/board/BoardContent.tsx` at line 99, The JSX prop line for
onComment is exceeding Prettier's max line length; extract the computed path
into a nearby constant (e.g., commentPath) using buildPostPath(clubId, post.id,
post.boardId) + '#comments' or a template literal, then replace the long inline
arrow with onComment={() => router.push(commentPath)} to satisfy Prettier and
keep the JSX line short; reference the onComment prop, router.push call,
buildPostPath, clubId, post.id and post.boardId when making the change.

Comment thread src/components/board/PostCard/PostCardBody.tsx
function PostCardTitle({ title, isNew, size }: PostCardTitleProps) {
return (
<div className="flex items-center gap-[5px]">
<div className={cn('flex items-center', size === 'detail' ? 'gap-[10px]' : 'gap-[5px]')}>
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 프로젝트 CSS 토큰 레지스트리에 5px/10px에 매핑되는 spacing 토큰이 있는지 확인
fd -e css -e ts -e js | xargs rg -nP '(--spacing|gap-\d{3}|--gap-|spacing:)' -C1 2>/dev/null | head -80

Repository: Team-Weeth/weeth-client

Length of output: 3505


🏁 Script executed:

# Tailwind 설정에서 gap 유틸리티가 spacing 토큰을 사용하는지 확인
fd -e config.ts -e tailwind.config.ts -e tailwind.config.js | xargs cat 2>/dev/null | head -200

Repository: Team-Weeth/weeth-client

Length of output: 1303


🏁 Script executed:

# PostCardTitle.tsx 전체 파일 검토
cat -n src/components/board/PostCard/PostCardTitle.tsx

Repository: Team-Weeth/weeth-client

Length of output: 1145


임의의 gap 값 대신 디자인 토큰 사용 필요 — gap-[10px], gap-[5px]

현재 설정된 spacing 토큰 중 정확히 일치하는 값이 없습니다:

  • --spacing-100 (4px), --spacing-200 (8px), --spacing-300 (12px), --spacing-400 (16px) 등이 정의되어 있으나, 5px와 10px는 토큰에 없습니다.

가장 가까운 토큰으로 변경하거나 (예: gap-200 8px 사용), 필요시 디자이너와 협의하여 새 토큰 추가를 요청하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/board/PostCard/PostCardTitle.tsx` at line 19, The
PostCardTitle component uses arbitrary gap utilities ('gap-[10px]' and
'gap-[5px]') which are not present in the design tokens; update the className
expression in PostCardTitle to use the closest existing spacing tokens (for
example replace 'gap-[10px]' with 'gap-300' or 'gap-200' and 'gap-[5px]' with
'gap-200' or 'gap-100' as appropriate) or, if 5px/10px are required, coordinate
with design to add new spacing tokens and then use those new token classes;
adjust the conditional in the div's className (the cn call) so it references the
chosen token names instead of arbitrary pixel values.

Comment thread src/components/home/HomeBoardContent.tsx Outdated
Comment on lines +34 to +39
onClick={() => {
const firstNotice = notices[0];
if (firstNotice) {
router.push(buildBoardPath(clubId, noticeBoardId));
}
}}
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 | 🟡 Minor

'전체보기' 버튼의 동작 조건 재검토 권장.

현재 로직은 notices[0] 존재 여부로 네비게이션을 가드하므로, 공지 게시글이 0개지만 공지 게시판은 존재하는 경우 버튼이 렌더되어도 클릭 시 아무 동작을 하지 않습니다(aria-label="공지 전체보기"와 UX 불일치). 의도상 공지 게시판으로 이동하는 버튼이라면 noticeBoardId 기준으로 판단하거나, 공지 자체가 없을 때는 버튼을 렌더하지 않는 편이 자연스럽습니다.

💡 제안
-          onClick={() => {
-            const firstNotice = notices[0];
-            if (firstNotice) {
-              router.push(buildBoardPath(clubId, noticeBoardId));
-            }
-          }}
+          onClick={() => {
+            if (noticeBoardId != null) {
+              router.push(buildBoardPath(clubId, noticeBoardId));
+            }
+          }}
📝 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
onClick={() => {
const firstNotice = notices[0];
if (firstNotice) {
router.push(buildBoardPath(clubId, noticeBoardId));
}
}}
onClick={() => {
if (noticeBoardId != null) {
router.push(buildBoardPath(clubId, noticeBoardId));
}
}}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/home/NoticeBoardBox.tsx` around lines 34 - 39, The onClick
handler currently guards navigation by checking notices[0], which prevents
navigation to the notice board when the board exists but has zero notices;
update the logic in the onClick (and any rendering condition for the "전체보기"
button with aria-label="공지 전체보기") to instead check presence of noticeBoardId (or
hide the button when noticeBoardId is absent) and then call
router.push(buildBoardPath(clubId, noticeBoardId)); ensure you reference the
notices array only for rendering notice items, not for enabling the board
navigation button.

Comment thread src/hooks/board/useToggleLike.ts
@github-actions
Copy link
Copy Markdown

🤖 Claude 테스트 제안

모델: claude-sonnet-4-6 | 토큰: 0 입력 / 0 출력

변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.

src/app/(private)/[clubId]/(main)/board/(with-nav)/@footer/[boardId]/[postId]/page.tsx

오류: Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.


src/app/(private)/[clubId]/(main)/board/(with-nav)/@footer/[boardId]/page.tsx

오류: Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.


src/app/(private)/[clubId]/(main)/board/(with-nav)/BoardNavClient.tsx

오류: Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.


src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/PostDetailContent.tsx

오류: Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.


src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/error.tsx

오류: Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.


이 코멘트는 Claude API를 통해 자동 생성되었습니다. 반드시 검토 후 사용하세요.

@github-actions
Copy link
Copy Markdown

PR 테스트 결과

Jest: 통과

🎉 모든 테스트를 통과했습니다!

@github-actions
Copy link
Copy Markdown

PR 검증 결과

TypeScript: 통과
ESLint: 통과
Prettier: 통과
Build: 통과

🎉 모든 검증을 통과했습니다!

@github-actions
Copy link
Copy Markdown

구현한 기능 Preview: https://weeth-7263jcil4-weethsite-4975s-projects.vercel.app

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.

Caution

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

⚠️ Outside diff range comments (1)
src/app/(private)/[clubId]/(main)/board/(with-nav)/BoardNavClient.tsx (1)

91-99: ⚠️ Potential issue | 🟡 Minor

commentDirty 가드가 /board/posts/[postId] 경로에서 동작하지 않습니다.

handleItemSelectisDetailPage(즉, /board/\d+/\d+$)일 때만 commentDirty 확인을 수행합니다. 그러나 PostDetailContent는 두 경로에서 모두 렌더링됩니다:

  • src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/page.tsx
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/posts/[postId]/page.tsx

따라서 /board/posts/[postId]에서 댓글을 작성 중이더라도 사이드바에서 게시판을 선택하면 경고 다이얼로그 없이 바로 이동합니다. isDetailPage || isPostsRoute 조건으로 통합해야 합니다.

제안 수정
  const handleItemSelect = (id: number | null) => {
-   if (isDetailPage && commentDirty) {
+   if ((isDetailPage || isPostsRoute) && commentDirty) {
      pendingSelect.current = id;
      setGuardOpen(true);
      return;
    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(private)/[clubId]/(main)/board/(with-nav)/BoardNavClient.tsx around
lines 91 - 99, handleItemSelect currently checks commentDirty only when
isDetailPage is true, which misses the /board/posts/[postId] route; update the
guard to check commentDirty when either isDetailPage or isPostsRoute is true.
Specifically, in the handleItemSelect function (and related logic using
pendingSelect, setGuardOpen, and executeSelect), replace the isDetailPage
condition with (isDetailPage || isPostsRoute) so pendingSelect.current = id and
setGuardOpen(true) run for both routes before returning.
♻️ Duplicate comments (1)
src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/page.tsx (1)

11-12: ⚠️ Potential issue | 🟡 Minor

0 / 음수 boardId 방어 추가 권장.

현재 가드는 빈 문자열과 비정수만 차단하므로 /board/0 또는 /board/-5 같은 URL이 BoardContent까지 통과해 API 404를 유발할 수 있습니다. 이전 리뷰에서 지적했던 부분 중 정수 검증은 반영되었으나 양수 검증은 누락되어 있어 보완을 권장합니다.

🛠️ 제안 diff
-  const boardIdNum = Number(boardId);
-  if (!boardId || !Number.isInteger(boardIdNum)) notFound();
+  const boardIdNum = Number(boardId);
+  if (!boardId || !Number.isInteger(boardIdNum) || boardIdNum <= 0) notFound();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(private)/[clubId]/(main)/board/(with-nav)/[boardId]/page.tsx around
lines 11 - 12, The guard currently converts boardId to boardIdNum and only
checks for NaN/integer but misses non-positive values, allowing /board/0 or
/board/-5 to proceed; update the validation where boardIdNum and boardId are
used (the block with boardIdNum, boardId, and notFound()) to reject non-positive
integers as well by ensuring boardIdNum > 0 before rendering BoardContent or
calling notFound().
🧹 Nitpick comments (1)
src/components/board/BoardContent.tsx (1)

84-101: 중복 경로 계산 추출 제안 (nit).

동일한 포스트에 대해 buildPostPath(clubId, post.id, post.boardId)hrefonComment 두 곳에서 중복 계산됩니다. 가독성 측면에서 렌더 내부에서 한 번만 계산해 재사용하는 편이 깔끔합니다. React Compiler가 활성화되어 있으므로 성능 목적은 아닙니다.

♻️ 제안 diff
-        <PostCard.Root key={post.id} className="relative">
+        {(() => {
+          const postPath = buildPostPath(clubId, post.id, post.boardId);
+          return (
+        <PostCard.Root key={post.id} className="relative">
           <PostCard.Header>
             ...
           </PostCard.Header>
           <Link
-            href={buildPostPath(clubId, post.id, post.boardId)}
+            href={postPath}
             className="after:absolute after:inset-0 after:content-['']"
           >
             ...
           </Link>
           ...
             <PostCard.Actions
               ...
-              onComment={() =>
-                router.push(`${buildPostPath(clubId, post.id, post.boardId)}#comments`)
-              }
+              onComment={() => router.push(`${postPath}#comments`)}
             />
           </div>
         </PostCard.Root>
+          );
+        })()}

또는 매핑 콜백 최상단에서 const postPath = buildPostPath(...)를 선언하는 형태가 더 간단합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/board/BoardContent.tsx` around lines 84 - 101, Extract the
duplicate path computation by calling buildPostPath(clubId, post.id,
post.boardId) once and reuse its result for both Link href and the onComment
handler; e.g., compute const postPath = buildPostPath(...) at the top of the
render/map callback where post is available, replace href={buildPostPath(...)}
with href={postPath}, and change router.push(`${buildPostPath(...)}#comments`)
to router.push(`${postPath}#comments`) so the path is only computed once.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/app/`(private)/[clubId]/(main)/board/(with-nav)/BoardNavClient.tsx:
- Around line 91-99: handleItemSelect currently checks commentDirty only when
isDetailPage is true, which misses the /board/posts/[postId] route; update the
guard to check commentDirty when either isDetailPage or isPostsRoute is true.
Specifically, in the handleItemSelect function (and related logic using
pendingSelect, setGuardOpen, and executeSelect), replace the isDetailPage
condition with (isDetailPage || isPostsRoute) so pendingSelect.current = id and
setGuardOpen(true) run for both routes before returning.

---

Duplicate comments:
In `@src/app/`(private)/[clubId]/(main)/board/(with-nav)/[boardId]/page.tsx:
- Around line 11-12: The guard currently converts boardId to boardIdNum and only
checks for NaN/integer but misses non-positive values, allowing /board/0 or
/board/-5 to proceed; update the validation where boardIdNum and boardId are
used (the block with boardIdNum, boardId, and notFound()) to reject non-positive
integers as well by ensuring boardIdNum > 0 before rendering BoardContent or
calling notFound().

---

Nitpick comments:
In `@src/components/board/BoardContent.tsx`:
- Around line 84-101: Extract the duplicate path computation by calling
buildPostPath(clubId, post.id, post.boardId) once and reuse its result for both
Link href and the onComment handler; e.g., compute const postPath =
buildPostPath(...) at the top of the render/map callback where post is
available, replace href={buildPostPath(...)} with href={postPath}, and change
router.push(`${buildPostPath(...)}#comments`) to
router.push(`${postPath}#comments`) so the path is only computed once.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 35265d29-614a-410d-8f8e-909d798d1e12

📥 Commits

Reviewing files that changed from the base of the PR and between f179d2d and aa450b0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • package.json
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/BoardNavClient.tsx
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/page.tsx
  • src/components/board/BoardContent.tsx
  • src/components/board/PostCard/PostCardBody.tsx
  • src/components/home/HomeBoardContent.tsx
  • src/hooks/board/useToggleLike.ts
✅ Files skipped from review due to trivial changes (1)
  • package.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/home/HomeBoardContent.tsx
  • src/hooks/board/useToggleLike.ts

Copy link
Copy Markdown
Collaborator

@JIN921 JIN921 left a comment

Choose a reason for hiding this comment

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

크게 수정이 필요한 부분은 없어 보입니다 수고하셧어용!

Comment thread src/components/home/NoticeBoardBox.tsx Outdated
onClick={() => router.push(`/${clubId}/board`)}
onClick={() => {
const firstNotice = notices[0];
if (firstNotice) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

요거 firstNotice가 여기에만 쓰인다면 변수 따로 만들지 말고 notices.length로 비교하면 어떨까용?

Comment thread src/hooks/board/useToggleLike.ts Outdated
updateHomePages(old, postId, replace),
);
},
onError: (_error, _variables, context) => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

스웨거 보면
image

이 케이스가 명시되어 잇는데 여기에는 해당 에러 처리가 안 되어 잇는 거 같아용 토스트로 띄워주시면 좋을 거 같습니다!

Copy link
Copy Markdown
Member

@woneeeee woneeeee left a comment

Choose a reason for hiding this comment

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

확인했습니다!! 수고하셨어요!!!🦦🦦🦦

_allowNavigation?.();
usePostStore.getState().reset();
router.push(`/${clubIdParam}/board/${result.id}`);
router.push(buildPostPath(clubIdParam, result.id, selectedBoard ?? undefined));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

요거를 토스트 전에 두는건 어떨까용?? 지금 글 다 작성 후에 토스트가 뜨고 나서 화면이 이동되는게 조금 시간이 걸려서 사용자 입장에선 헷갈릴ㄹ 수도 있을 것 같아용 아니면 그 사이에 로딩창이 있거나 하면 좀 더 사용자 입장에선 이해하기가 쉬울 것 같슴니당!!

Copy link
Copy Markdown
Member

@dalzzy dalzzy left a comment

Choose a reason for hiding this comment

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

수고하셨습니당~~!!!

import { notFound } from 'next/navigation';

import { boardServerApi } from '@/lib/apis/board.server';
import { PostDetailContent } from '../../[boardId]/[postId]/PostDetailContent';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

별건아닌데 이것도 절대경로로 수정하면 좋을 것 같습니다ㅎㅎ

@github-actions
Copy link
Copy Markdown

PR 테스트 결과

Jest: 통과

🎉 모든 테스트를 통과했습니다!

@github-actions
Copy link
Copy Markdown

🤖 Claude 테스트 제안

모델: claude-sonnet-4-6 | 토큰: 0 입력 / 0 출력

변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.

src/app/(private)/[clubId]/(main)/board/(with-nav)/@footer/[boardId]/[postId]/page.tsx

오류: Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.


src/app/(private)/[clubId]/(main)/board/(with-nav)/@footer/[boardId]/page.tsx

오류: Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.


src/app/(private)/[clubId]/(main)/board/(with-nav)/BoardNavClient.tsx

오류: Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.


src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/PostDetailContent.tsx

오류: Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.


src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/error.tsx

오류: Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.


이 코멘트는 Claude API를 통해 자동 생성되었습니다. 반드시 검토 후 사용하세요.

@github-actions
Copy link
Copy Markdown

PR 검증 결과

TypeScript: 통과
ESLint: 통과
Prettier: 통과
Build: 통과

🎉 모든 검증을 통과했습니다!

@github-actions
Copy link
Copy Markdown

구현한 기능 Preview: https://weeth-jy4dz14ib-weethsite-4975s-projects.vercel.app

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.

🧹 Nitpick comments (1)
src/app/(private)/[clubId]/(main)/board/(with-nav)/layout.tsx (1)

24-32: 선택: params 해석도 Suspense 안으로 넣으면 streaming 이점을 더 살릴 수 있습니다.

현재 BoardLayout이 최상위에서 await params를 수행한 뒤 트리 전체를 반환하므로, params가 resolve되기 전까지는 fallback이 나타나지 않고 레이아웃 셸 자체가 블로킹됩니다. 보드 nav 영역에 한해 더 빠른 first paint가 필요하다면 clubId 해석을 BoardNavLoader 내부로 옮기는 식으로 개선할 수 있습니다. 다만 children/footer도 동일 params에 의존할 가능성이 높아 의미 있는 차이가 없을 수 있으니 필요할 때만 고려하시면 됩니다.

♻️ 참고 변형 예시
-async function BoardNavLoader({ clubId }: { clubId: string }) {
+async function BoardNavLoader({ params }: { params: Promise<{ clubId: string }> }) {
+  const { clubId } = await params;
   const response = await boardServerApi.getBoards(clubId).catch(() => null);
   ...
 }

-export default async function BoardLayout({ children, footer, params }: BoardLayoutProps) {
-  const { clubId } = await params;
-
+export default function BoardLayout({ children, footer, params }: BoardLayoutProps) {
   return (
     ...
-        <Suspense fallback={<BoardNavSkeleton />}>
-          <BoardNavLoader clubId={clubId} />
-        </Suspense>
+        <Suspense fallback={<BoardNavSkeleton />}>
+          <BoardNavLoader params={params} />
+        </Suspense>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(private)/[clubId]/(main)/board/(with-nav)/layout.tsx around lines
24 - 32, BoardLayout currently awaits params at the top which blocks the whole
layout; remove the top-level await of params in BoardLayout and instead resolve
clubId inside the streaming subtree (for example inside BoardNavLoader or within
the Suspense fallback boundary) so the layout shell can stream immediately.
Specifically, stop using "const { clubId } = await params" in BoardLayout and
adjust BoardNavLoader (or a new loader component used inside the Suspense) to
accept the raw params and perform the await/resolve there, keeping
children/footer rendering behavior unchanged unless they also require params.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app/`(private)/[clubId]/(main)/board/(with-nav)/layout.tsx:
- Around line 24-32: BoardLayout currently awaits params at the top which blocks
the whole layout; remove the top-level await of params in BoardLayout and
instead resolve clubId inside the streaming subtree (for example inside
BoardNavLoader or within the Suspense fallback boundary) so the layout shell can
stream immediately. Specifically, stop using "const { clubId } = await params"
in BoardLayout and adjust BoardNavLoader (or a new loader component used inside
the Suspense) to accept the raw params and perform the await/resolve there,
keeping children/footer rendering behavior unchanged unless they also require
params.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f25b0c5b-419c-4900-959d-133337ebdf8b

📥 Commits

Reviewing files that changed from the base of the PR and between aa450b0 and 3c51b98.

📒 Files selected for processing (9)
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/loading.tsx
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/layout.tsx
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/loading.tsx
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/posts/[postId]/loading.tsx
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/posts/[postId]/page.tsx
  • src/components/home/HomeBoardContent.tsx
  • src/components/home/NoticeBoardBox.tsx
  • src/hooks/board/useCreatePost.ts
  • src/hooks/board/useToggleLike.ts
✅ Files skipped from review due to trivial changes (3)
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/loading.tsx
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/posts/[postId]/loading.tsx
  • src/components/home/NoticeBoardBox.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/app/(private)/[clubId]/(main)/board/(with-nav)/posts/[postId]/page.tsx
  • src/hooks/board/useCreatePost.ts
  • src/hooks/board/useToggleLike.ts

@nabbang6 nabbang6 merged commit f83755b into develop Apr 27, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📬 API 서버 API 통신 🐞 BugFix Something isn't working 🔨 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants