[Fix] WTH-325 : 게시판 리팩토링 및 좋아요 삭제 api 연결#70
Conversation
📝 WalkthroughWalkthrough보드별 라우트·푸터와 게시물 상세 경로를 분리하고, PostCard 렌더링을 Body 컴포넌트로 위임했으며 좋아요 API를 add/remove로 분리하고 경로 빌더·스토어 동기화를 추가했습니다. Changes
Sequence Diagram(s)(생성 조건에 부합하지 않아 생략) Estimated code review effort🎯 3 (Moderate) | ⏱️ ~35 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
🤖 Claude 테스트 제안
변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.
|
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
PR 검증 결과✅ TypeScript: 통과 |
|
구현한 기능 Preview: https://weeth-piu217ba2-weethsite-4975s-projects.vercel.app |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (9)
src/app/globals.css (1)
654-685: 하드코딩된16px대신 디자인 토큰 사용 권장.
h1::after의margin-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을 반복 기록합니다.
useNavigationGuard내allowNavigation을useCallback으로 감싸면 불필요한 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이 되며buildBoardPath의boardId != 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(잘못된 값)로,PostDetailContent의useEffect는 실제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.jsuseRouter.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가 사용되지 않습니다.
mutationFn은mutate(isLiked)로 전달된 인자를 사용하고,onError는previousDetail/previousHomePosts만 참조하므로 컨텍스트에 담긴wasLiked는 어디에서도 소비되지 않습니다. 제거하여 혼란을 줄이거나, 반대로mutationFn이context.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를 콜백으로 병합하는 부분 단순화 여지.
useCodeHighlight는RefObject<HTMLElement | null>을 받고,useLineClamp는 내부에서 자체 ref를 반환합니다. 현재는 같은 DOM에contentRef.current와ref.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
📒 Files selected for processing (35)
src/app/(private)/[clubId]/(main)/board/(with-nav)/@footer/[boardId]/[postId]/page.tsxsrc/app/(private)/[clubId]/(main)/board/(with-nav)/@footer/[boardId]/page.tsxsrc/app/(private)/[clubId]/(main)/board/(with-nav)/BoardNavClient.tsxsrc/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/PostDetailContent.tsxsrc/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/error.tsxsrc/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/page.tsxsrc/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/page.tsxsrc/app/(private)/[clubId]/(main)/board/(with-nav)/page.tsxsrc/app/(private)/[clubId]/(main)/board/(with-nav)/posts/[postId]/error.tsxsrc/app/(private)/[clubId]/(main)/board/(with-nav)/posts/[postId]/page.tsxsrc/app/globals.csssrc/components/board/BoardContent.tsxsrc/components/board/Editor/index.tsxsrc/components/board/ImageList/ImageList.tsxsrc/components/board/PostCard/PostCardBody.tsxsrc/components/board/PostCard/PostCardDetailContent.tsxsrc/components/board/PostCard/PostCardTitle.tsxsrc/components/board/PostCard/index.tsxsrc/components/board/PostDetailHeader.tsxsrc/components/board/PostEditorShell.tsxsrc/components/home/HomeBoardContent.tsxsrc/components/home/NoticeBoardBox.tsxsrc/components/home/UnreadNoticeBox.tsxsrc/components/layout/header/PostingActions.tsxsrc/hooks/board/useBoardQuery.tssrc/hooks/board/useCreatePost.tssrc/hooks/board/useToggleLike.tssrc/hooks/board/useUpdatePost.tssrc/hooks/home/useWritePost.tssrc/hooks/useNavigationGuard.tssrc/lib/apis/board.tssrc/lib/board.tssrc/stores/usePostStore.tssrc/types/board.tssrc/types/home.ts
💤 Files with no reviewable changes (1)
- src/components/board/Editor/index.tsx
| 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`)} |
There was a problem hiding this comment.
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.
| 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.
| 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]')}> |
There was a problem hiding this comment.
🧩 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 -80Repository: 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 -200Repository: Team-Weeth/weeth-client
Length of output: 1303
🏁 Script executed:
# PostCardTitle.tsx 전체 파일 검토
cat -n src/components/board/PostCard/PostCardTitle.tsxRepository: 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.
| onClick={() => { | ||
| const firstNotice = notices[0]; | ||
| if (firstNotice) { | ||
| router.push(buildBoardPath(clubId, noticeBoardId)); | ||
| } | ||
| }} |
There was a problem hiding this comment.
'전체보기' 버튼의 동작 조건 재검토 권장.
현재 로직은 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.
| 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.
🤖 Claude 테스트 제안
변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.
|
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
PR 검증 결과✅ TypeScript: 통과 🎉 모든 검증을 통과했습니다! |
|
구현한 기능 Preview: https://weeth-7263jcil4-weethsite-4975s-projects.vercel.app |
There was a problem hiding this comment.
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]경로에서 동작하지 않습니다.
handleItemSelect는isDetailPage(즉,/board/\d+/\d+$)일 때만commentDirty확인을 수행합니다. 그러나PostDetailContent는 두 경로에서 모두 렌더링됩니다:
src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/page.tsxsrc/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 | 🟡 Minor0 / 음수
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)가href와onComment두 곳에서 중복 계산됩니다. 가독성 측면에서 렌더 내부에서 한 번만 계산해 재사용하는 편이 깔끔합니다. 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (7)
package.jsonsrc/app/(private)/[clubId]/(main)/board/(with-nav)/BoardNavClient.tsxsrc/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/page.tsxsrc/components/board/BoardContent.tsxsrc/components/board/PostCard/PostCardBody.tsxsrc/components/home/HomeBoardContent.tsxsrc/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
| onClick={() => router.push(`/${clubId}/board`)} | ||
| onClick={() => { | ||
| const firstNotice = notices[0]; | ||
| if (firstNotice) { |
There was a problem hiding this comment.
요거 firstNotice가 여기에만 쓰인다면 변수 따로 만들지 말고 notices.length로 비교하면 어떨까용?
| updateHomePages(old, postId, replace), | ||
| ); | ||
| }, | ||
| onError: (_error, _variables, context) => { |
| _allowNavigation?.(); | ||
| usePostStore.getState().reset(); | ||
| router.push(`/${clubIdParam}/board/${result.id}`); | ||
| router.push(buildPostPath(clubIdParam, result.id, selectedBoard ?? undefined)); |
There was a problem hiding this comment.
요거를 토스트 전에 두는건 어떨까용?? 지금 글 다 작성 후에 토스트가 뜨고 나서 화면이 이동되는게 조금 시간이 걸려서 사용자 입장에선 헷갈릴ㄹ 수도 있을 것 같아용 아니면 그 사이에 로딩창이 있거나 하면 좀 더 사용자 입장에선 이해하기가 쉬울 것 같슴니당!!
| import { notFound } from 'next/navigation'; | ||
|
|
||
| import { boardServerApi } from '@/lib/apis/board.server'; | ||
| import { PostDetailContent } from '../../[boardId]/[postId]/PostDetailContent'; |
There was a problem hiding this comment.
별건아닌데 이것도 절대경로로 수정하면 좋을 것 같습니다ㅎㅎ
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
🤖 Claude 테스트 제안
변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.
|
PR 검증 결과✅ TypeScript: 통과 🎉 모든 검증을 통과했습니다! |
|
구현한 기능 Preview: https://weeth-jy4dz14ib-weethsite-4975s-projects.vercel.app |
There was a problem hiding this comment.
🧹 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
📒 Files selected for processing (9)
src/app/(private)/[clubId]/(main)/board/(with-nav)/[boardId]/[postId]/loading.tsxsrc/app/(private)/[clubId]/(main)/board/(with-nav)/layout.tsxsrc/app/(private)/[clubId]/(main)/board/(with-nav)/loading.tsxsrc/app/(private)/[clubId]/(main)/board/(with-nav)/posts/[postId]/loading.tsxsrc/app/(private)/[clubId]/(main)/board/(with-nav)/posts/[postId]/page.tsxsrc/components/home/HomeBoardContent.tsxsrc/components/home/NoticeBoardBox.tsxsrc/hooks/board/useCreatePost.tssrc/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

✅ PR 유형
어떤 변경 사항이 있었나요?
📌 관련 이슈번호
✅ Key Changes
게시글 상세 디자인 변경
게시판 URL 구조 개선 (/board/[id] → /board/[boardId]/[postId])
/board/[id]단일 파라미터 구조에서/board/[boardId]/[postId]2-depth 구조로 변경/board/posts/[postId]추가boardId를 읽어 활성 채널을 자동 동기화하도록 BoardNavClient 개선내비게이션 가드 버그 수정
allowNavigation을 호출하여 가드가 페이지 이동을 차단하지 않도록 수정_allowNavigation상태 추가,PostEditorShell에서 가드 해제 함수를 주입activeBoardId를 null로 리셋이미지 업로드 시 원본 비율 유지
ImageList의 이미지 클래스를w-full→w-auto object-contain으로 변경하여원본 비율 보존
게시글 좋아요 취소 API 연결 및 캐시 버그 수정
요청의 불일치 해소
📸 스크린샷 or 실행영상
2026-04-24.221633.mp4
🎸 기타 사항 or 추가 코멘트
현재 최신 게시글 조회 (
/clubs/{clubId}/dashboard/recent-posts) 시boardId를 응답으로 받아올 수 없어/{clubId}/board/posts/{postId}로 이동되게 해두었습니다! (이외에도boardId를 받아올 수 없는 경우가 생기면 해당 URL로 이동되게 처리)요건 배겐드 분들께 요청 드리면 바로 해결될 예정입니당
URL 구조 수정으로 파일 변경사항이 많은데... ㅜ ㅜ
각
page.tsx나error.tsx는 확인하지 않아주셔도 괜찮습니닷,,,Summary by CodeRabbit
릴리스 노트
새로운 기능
개선사항
스타일