Skip to content

[Feat] WTH-241 게시글 작성/수정/삭제 API 연결#39

Open
nabbang6 wants to merge 16 commits intodevelopfrom
WTH-241-게시글-작성-수정-삭제-API-연결

Hidden character warning

The head ref may contain hidden characters: "WTH-241-\uac8c\uc2dc\uae00-\uc791\uc131-\uc218\uc815-\uc0ad\uc81c-API-\uc5f0\uacb0"
Open

[Feat] WTH-241 게시글 작성/수정/삭제 API 연결#39
nabbang6 wants to merge 16 commits intodevelopfrom
WTH-241-게시글-작성-수정-삭제-API-연결

Conversation

@nabbang6
Copy link
Copy Markdown
Collaborator

@nabbang6 nabbang6 commented Apr 6, 2026

✅ PR 유형

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

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

📌 관련 이슈번호

  • Closed #241

✅ Key Changes

게시글 작성/수정/삭제 API 연결

  • lib/actions/board.ts에 createPost, updatePost, deletePost Server Action 추가
  • lib/apis/board.server.ts에 대응하는 서버 API 함수 추가
  • useCreatePost, useUpdatePost, useDeletePost 훅으로 유효성 검사 + API 호출 + 토스트 + 라우팅 일괄 처리
  • types/board.ts에 CreatePostBody, UpdatePostBody, CreatePostData 등 타입 추가

게시글 수정 페이지

  • board/edit/[id]/page.tsx RSC 페이지 및 EditClientEditor 클라이언트 컴포넌트 신규 생성
  • usePostStore에 initFromDetail() 액션 추가 — 기존 게시글 데이터를 스토어로 복원
  • UploadFileItem에 isExisting, fileSize, contentType 필드 추가

게시글 작성/수정 공통 Shell

  • PostEditorShell 컴포넌트로 작성/수정 페이지의 레이아웃·TitleInput·Editor를 통합
  • ClientEditor(작성)와 EditClientEditor(수정) 모두 PostEditorShell을 사용하도록 리팩토링
  • useNavigationGuard 훅으로 브라우저 뒤로가기/탭 닫기 시 이탈 방지 다이얼로그 제공

게시글 삭제 다이얼로그

  • PostDeleteDialog — 삭제 확인 AlertDialog + API 호출을 캡슐화
  • PostActionMenu를 ActionMenu(범용) + PostDeleteDialog 조합으로 분리
  • 댓글/답글의 수정·삭제는 범용 ActionMenu를 직접 사용하도록 변경

네비게이션 개선

  • 상세 페이지 뒤로가기 버튼: router.back() → router.push('/board')로 변경
  • 상세 페이지 진입 시 boardId 기반으로 사이드 탭 활성 상태 동기화
  • 상세 페이지에서 다른 게시판 탭 선택 시 /board 목록으로 이동

📸 스크린샷 or 실행영상

KakaoTalk_20260406_162330705.mp4

/board/write 와 /board/edit/[id[로 확인 가능합니다!


🎸 기타 사항 or 추가 코멘트

  • EditClientEditor에서 boardServerApi.getPostById의 clubId는 추후 수정 예정
  • CategorySelector에서 type: 'ALL' 항목을 필터링하여 작성/수정 시 "전체" 게시판 선택을 방지

공지 읽음 처리 + 좋아요 토글 + 댓글 작성/수정/삭제 관련은 다음 이슈에서 빠르게 구현해보겠습니다 . . . ㅠ.ㅠ

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 게시물 작성·수정 기능 추가 (공유 편집 UI 포함)
    • 게시물 삭제 기능 추가(삭제 확인 대화상자)
    • 작성 중 페이지 이탈 시 저장 경고 표시
  • Improvements

    • 에디터 레이아웃·사용성 개선 및 초기 콘텐츠 지원
    • 목록/상세 콘텐츠 렌더링 분리로 가독성 향상
    • 카테고리 선택 및 게시판 이동 동기화 강화
    • 파일 첨부 메타데이터 개선 및 업로드 안정성 향상
  • UI

    • 게시물 액션 메뉴와 헤더 게시 버튼 동작 개선

@nabbang6 nabbang6 requested review from JIN921, dalzzy and woneeeee April 6, 2026 07:29
@nabbang6 nabbang6 self-assigned this Apr 6, 2026
@nabbang6 nabbang6 added 📬 API 서버 API 통신 ✨ Feature 기능 개발 labels Apr 6, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

보드 게시물의 작성·수정·삭제 워크플로우를 추가하고 에디터 초기화/레이아웃을 통합한 PostEditorShellActionMenu를 도입했습니다. PostCard 콘텐츠는 리스트/상세로 분리되었고, 관련 훅, 서버 액션, 스토어/타입 및 네비게이션 가드가 함께 추가·수정되었습니다.

Changes

Cohort / File(s) Summary
게시물 CRUD 훅·서버 액션
src/hooks/board/useCreatePost.ts, src/hooks/board/useUpdatePost.ts, src/hooks/board/useDeletePost.ts, src/lib/actions/board.ts, src/lib/apis/board.server.ts
create/update/delete 서버 액션과 이를 사용하는 클라이언트 훅 추가. 유효성 검사, isPending, 토스트, 쿼리 무효화 포함.
에디터 셸·에디터 초기화
src/components/board/PostEditorShell.tsx, src/components/board/Editor/index.tsx, src/components/board/Editor/usePostEditor.ts
PostEditorShell 추가, initialContent 지원, 에디터 레이아웃 재구성 및 네비게이션 가드 통합.
편집 페이지·클라이언트 에디터
src/app/.../board/edit/[id]/page.tsx, src/app/.../board/edit/[id]/EditClientEditor.tsx
게시물 편집 라우트/클라이언트 컴포넌트 추가; 서버에서 포스트 로드 후 에디터 초기화(스토어 동기화).
ActionMenu 및 PostActionMenu 리팩토링
src/components/board/ActionMenu.tsx, src/components/board/PostActionMenu.tsx, src/components/board/PostDeleteDialog.tsx
공통 작업 메뉴 ActionMenu 추가, PostActionMenu 재구성 및 삭제 확인 다이얼로그 추가.
PostCard 콘텐츠 분리
src/components/board/PostCard/PostCardContent.tsx, src/components/board/PostCard/index.tsx
콘텐츠를 ListContent/DetailContent로 분리. 리스트는 plain text, 상세는 HTML 렌더링으로 구분.
스토어·타입·파일 메타데이터 변경
src/stores/usePostStore.ts, src/types/board.ts, src/utils/shared/file.ts, src/hooks/board/useFileAttach.ts, src/hooks/useFileUpload.ts
UploadFileItemfileSize, contentType 추가, initFromDetail 추가, 페이로드 파일 형식 변경 및 유틸 변환 업데이트.
네비게이션·헤더·글쓰기 흐름 통합
src/components/layout/header/Header.tsx, src/app/(private)/(main)/board/(with-nav)/BoardNavClient.tsx, src/app/(private)/(main)/board/(with-nav)/[id]/PostDetailContent.tsx
헤더에서 작성/수정 분기 통합 및 submit 핸들러 연결, 보드 네비 동기화 및 상세→목록 네비게이션으로 변경.
댓글/리스트 컴포넌트 변경
src/components/board/BoardContent.tsx, src/components/home/HomeBoardContent.tsx, src/components/board/Comment/*
PostCard.ListContent 사용으로 변경 및 댓글/답글에 ActionMenu 적용.
글쓰기 클라이언트 초기화 변경
src/app/(private)/(main)/board/write/ClientEditor.tsx
스토어 리셋 방식 변경 및 PostEditorShell 사용으로 카테고리 선택 초기화 로직 조정.
전역 스타일 조정
src/app/globals.css
.ProseMirror 레이아웃·간격 규칙 및 인라인 코드 단락 패딩 조정.
바렐·내보내기 업데이트
src/components/board/index.ts, src/hooks/index.ts
ActionMenu, PostEditorShell, 새 훅들(useCreatePost, useUpdatePost, useDeletePost, useNavigationGuard) 재수출 추가.

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자
    participant Client as 클라이언트 (Header/Editor)
    participant Store as PostStore
    participant Hook as useCreatePost
    participant Action as createPost (Server Action)
    participant API as 백엔드 API
    participant Router as 라우터

    User->>Client: 게시하기 클릭
    Client->>Store: getPayload()
    Store-->>Client: {title, content, files, board}
    Client->>Hook: submit()
    Hook->>Hook: 유효성 검사 (title/content/board/files)
    alt 실패
        Hook->>Client: 오류 토스트
    else 성공
        Hook->>Action: createPost(clubId, boardId, payload)
        Action->>API: POST /clubs/{clubId}/boards/{boardId}/posts
        API-->>Action: {id}
        Action->>Action: revalidatePath('/board')
        Action-->>Hook: result
        Hook->>Store: reset()
        Hook->>Router: push('/board/{id}')
        Hook->>Client: 성공 토스트
    end
Loading
sequenceDiagram
    participant User as 사용자
    participant UI as PostActionMenu / Dialog
    participant Hook as useDeletePost
    participant Action as deletePost (Server Action)
    participant API as 백엔드 API
    participant Query as QueryClient

    User->>UI: 삭제 선택
    UI->>User: 삭제 확인 다이얼로그 표시
    User->>UI: 확인
    UI->>Hook: submit(postId, onDeleted)
    Hook->>Action: deletePost(clubId, postId)
    Action->>API: DELETE /clubs/{clubId}/boards/posts/{postId}
    API-->>Action: 성공
    Action->>Query: invalidateQueries(['posts'], ['home','recent-posts'])
    Hook-->>UI: onDeleted 콜백 / 닫기
    Hook->>User: 성공 토스트
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • JIN921
  • dalzzy
  • woneeeee

"나는 토끼, 코드 밭을 뛰어다녀요 🐇
에디터와 메뉴를 한데 묶고
파일 메타를 챙기며 깡충깡충,
삭제는 다이얼로그로 묻고,
게시되면 라우터 따라 폴짝! 🎉"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 메인 변경사항인 게시글 작성/수정/삭제 API 연결을 명확하게 반영하고 있으며, 이슈 번호를 포함하고 있습니다.
Description check ✅ Passed PR 설명은 템플릿의 주요 섹션(PR 유형, 이슈번호, Key Changes)을 완벽하게 작성했으며, 상세한 변경사항과 스크린샷을 포함하고 있습니다.

✏️ 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-241-게시글-작성-수정-삭제-API-연결

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

github-actions bot commented Apr 6, 2026

🤖 Claude 테스트 제안

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

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

src/app/(private)/(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)/(main)/board/(with-nav)/[id]/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)/(main)/board/edit/[id]/EditClientEditor.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)/(main)/board/edit/[id]/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)/(main)/board/write/ClientEditor.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

github-actions bot commented Apr 6, 2026

PR 테스트 결과

Jest: 통과

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

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 6, 2026

PR 검증 결과

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

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

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 6, 2026

🤖 Claude 테스트 제안

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

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

src/app/(private)/(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)/(main)/board/(with-nav)/[id]/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)/(main)/board/edit/[id]/EditClientEditor.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)/(main)/board/edit/[id]/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)/(main)/board/write/ClientEditor.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

github-actions bot commented Apr 6, 2026

PR 테스트 결과

Jest: 통과

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

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 6, 2026

PR 검증 결과

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

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

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 6, 2026

🤖 Claude 테스트 제안

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

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

src/app/(private)/(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)/(main)/board/(with-nav)/[id]/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)/(main)/board/edit/[id]/EditClientEditor.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)/(main)/board/edit/[id]/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)/(main)/board/write/ClientEditor.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

github-actions bot commented Apr 6, 2026

PR 테스트 결과

Jest: 통과

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

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 6, 2026

PR 검증 결과

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

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

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 6, 2026

구현한 기능 Preview: https://weeth-8k4l2vp27-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: 4

🧹 Nitpick comments (13)
src/components/board/PostCard/PostCardContent.tsx (1)

30-58: useEffect 의존성 배열에 스프레드 사용

ESLint 경고대로 ...deps 스프레드는 정적 분석을 방해합니다. 현재 사용처에서는 content 문자열 하나만 전달하므로, 명시적인 단일 의존성으로 리팩토링하면 더 안전합니다.

♻️ 의존성 명시화 제안
-function useLineClamp<T extends HTMLElement>(enabled: boolean, deps: unknown[]) {
+function useLineClamp<T extends HTMLElement>(enabled: boolean, contentKey: string) {
   const ref = useRef<T>(null);
   const [isClamped, setIsClamped] = useState(false);
   const [isExpanded, setIsExpanded] = useState(false);

   useEffect(() => {
     // ...
-  }, [enabled, ...deps]);
+  }, [enabled, contentKey]);

   return { ref, isClamped, isExpanded, setIsExpanded };
 }

사용처:

-  const { ref, isClamped, isExpanded, setIsExpanded } = useLineClamp<HTMLParagraphElement>(
-    expandable,
-    [content],
-  );
+  const { ref, isClamped, isExpanded, setIsExpanded } = useLineClamp<HTMLParagraphElement>(
+    expandable,
+    content,
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/board/PostCard/PostCardContent.tsx` around lines 30 - 58, The
useLineClamp hook currently spreads deps in the useEffect dependency array which
ESLint flags; update the hook to accept an explicit single dependency (e.g.,
rename parameter from deps to contentDep or similar) and use that single value
in the dependency array alongside enabled, then update call sites to pass the
specific content string instead of an array; modify symbols: function
useLineClamp<T extends HTMLElement>(enabled: boolean, deps: unknown[]) ->
useLineClamp<T>(enabled: boolean, contentDep: unknown) and change useEffect(...,
[enabled, contentDep]) so the effect has a static, explicit dependency.
src/app/globals.css (1)

628-630: 인라인 코드 선택자 안정성 확인 필요

:has() 선택자와 중첩 부정 선택자 조합(p:has(> code:not(pre code)))이 의도대로 동작하는지 확인해주세요. code:not(pre code)pre 내부의 code가 아닌 모든 code를 선택하려는 의도로 보이지만, pre code는 자손 선택자이므로 직접 자식이 아닌 경우에도 매칭될 수 있습니다.

의도가 "코드 블록(<pre><code>) 외부의 인라인 코드를 포함한 단락"이라면 현재 선택자는 정상 동작합니다. 다만 브라우저 호환성 측면에서 :has() 지원 여부(Safari 15.4+, Chrome 105+)를 확인해주세요.

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

In `@src/app/globals.css` around lines 628 - 630, The selector ".ProseMirror
p:has(> code:not(pre code))" may not reliably express "paragraphs containing
inline code but not code inside <pre>" across all browsers and the nested
:not(pre code) can be confusing; confirm the intent is to target inline <code>
that are direct children of <p> (excluding any <pre><code>) and either (A) keep
the current selector but add a comment noting browser compatibility requirements
for :has() (Safari 15.4+, Chrome 105+) or (B) if wider compatibility is needed,
replace the selector logic with a more explicit approach (e.g., target
direct-child code elements within .ProseMirror p and exclude paragraphs that
contain <pre>) by updating the CSS rules that reference the selector and adding
a brief inline comment referencing ".ProseMirror p:has(> code:not(pre code))" so
future readers know the intent.
src/components/board/Comment/ReplyItem.tsx (1)

46-51: onDeleteSelectonDelete 간의 타입 시그니처 불일치

ActionMenuonDeleteSelect는 Radix UI의 DropdownMenuItem.onSelect에서 Event 파라미터를 받습니다 (src/components/board/ActionMenu.tsx:63-65 참조). 그러나 ReplyItemProps.onDelete() => void로 정의되어 있어 타입 계약이 일치하지 않습니다.

런타임에서는 JavaScript가 추가 인자를 무시하므로 정상 동작하지만, 타입 안전성을 위해 onDelete의 시그니처를 (event?: Event) => void로 변경하거나, 래퍼 함수를 사용하는 것을 권장합니다.

🔧 래퍼 함수를 사용한 수정 제안
         <ActionMenu
           triggerVariant="secondary"
           triggerClassName="absolute top-400 right-400 size-6"
           onEdit={onEdit}
-          onDeleteSelect={onDelete}
+          onDeleteSelect={() => onDelete?.()}
         />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/board/Comment/ReplyItem.tsx` around lines 46 - 51, The
onDeleteSelect prop passed to <ActionMenu> expects a handler that accepts an
Event (per DropdownMenuItem.onSelect), but ReplyItemProps.onDelete is typed as
() => void, causing a signature mismatch; update the ReplyItem implementation to
either change the prop type to onDelete: (event?: Event) => void or wrap the
existing onDelete with a small adapter (e.g., onDeleteSelect={(e) => onDelete()}
or onDeleteSelect={(e) => onDelete?.(e)}) so the handler you pass from ReplyItem
matches ActionMenu's expected signature; reference ActionMenu, onDeleteSelect,
and ReplyItemProps.onDelete when making the change.
src/components/board/Comment/CommentItem.tsx (1)

72-77: onDeleteSelectonDelete 간의 타입 시그니처 불일치

ReplyItem과 동일한 이슈입니다. ActionMenuonDeleteSelect(event: Event) => void를 기대하지만, CommentItemProps.onDelete() => void로 정의되어 있습니다.

🔧 래퍼 함수를 사용한 수정 제안
           <ActionMenu
             triggerVariant="secondary"
             triggerClassName="size-6"
             onEdit={onEdit}
-            onDeleteSelect={onDelete}
+            onDeleteSelect={() => onDelete?.()}
           />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/board/Comment/CommentItem.tsx` around lines 72 - 77,
ActionMenu's prop onDeleteSelect expects a handler with signature (event: Event)
=> void while CommentItemProps.onDelete is defined as () => void; fix by either
updating CommentItemProps.onDelete to accept an Event parameter or, without
changing types, pass a wrapper function to ActionMenu that accepts the event and
calls the existing onDelete (e.g. create an inline handler in CommentItem that
takes event: Event and invokes onDelete()); refer to ActionMenu,
CommentItemProps.onDelete and ReplyItem for the analogous fix.
src/components/board/PostDeleteDialog.tsx (1)

19-23: 삭제 실패 시 다이얼로그 닫힘 동작 확인 필요

handleConfirm에서 await submit() 후 항상 onOpenChange(false)를 호출합니다. useDeletePostsubmit은 에러를 catch하여 toast를 표시하고 다시 throw하지 않으므로, 삭제 실패 시에도 다이얼로그가 닫힙니다.

사용자가 삭제 실패 후 재시도할 수 있도록 다이얼로그를 열어두는 것이 더 나은 UX일 수 있습니다.

♻️ 실패 시 다이얼로그 유지 제안
  const handleConfirm = async (event: React.MouseEvent) => {
    event.preventDefault();
-   await submit(postId, onDeleted);
-   onOpenChange(false);
+   await submit(postId, () => {
+     onDeleted?.();
+     onOpenChange(false);
+   });
  };

이렇게 하면 성공 시에만 다이얼로그가 닫히고, 실패 시에는 열린 상태로 유지됩니다.

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

In `@src/components/board/PostDeleteDialog.tsx` around lines 19 - 23,
handleConfirm currently calls await submit(...) and then always calls
onOpenChange(false), which closes the dialog even when deletion failed; change
handleConfirm to only close the dialog on successful deletion by wrapping the
submit call in try/catch: call await submit(postId, onDeleted) inside try and
call onOpenChange(false) and onDeleted() only on success, and in catch leave the
dialog open (optionally rethrow or let useDeletePost's toast handle error).
Reference function names: handleConfirm, submit, onOpenChange, onDeleted (and
useDeletePost behavior) when making the change.
src/hooks/board/useDeletePost.ts (1)

28-31: 쿼리 키 패턴 명확성 개선 제안

TanStack Query v5에서 invalidateQueries는 기본적으로 prefix 매칭을 사용하므로 ['posts']['posts', clubId]['posts', clubId, activeBoardId]를 모두 일치시키며, ['home', 'recent-posts']['home', 'recent-posts', clubId]를 일치시킵니다. 코드는 정상적으로 동작하지만, 유지보수성과 가독성을 위해 명시적으로 동일한 키 패턴을 사용하는 것이 좋습니다.

♻️ 쿼리 키 일관성 개선 제안
      await deletePost(clubId, postId);
      await Promise.all([
-       queryClient.invalidateQueries({ queryKey: ['posts'] }),
-       queryClient.invalidateQueries({ queryKey: ['home', 'recent-posts'] }),
+       queryClient.invalidateQueries({ queryKey: ['posts', clubId] }),
+       queryClient.invalidateQueries({ queryKey: ['home', 'recent-posts', clubId] }),
      ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/board/useDeletePost.ts` around lines 28 - 31, The invalidateQueries
calls in useDeletePost.ts use broad prefix matching (queryKey ['posts'] and
['home','recent-posts']), so make the intent explicit by either supplying the
full query key shape used elsewhere (e.g., include clubId/activeBoardId
segments) or pass the exact matching option to avoid accidental matches; update
the two calls to queryClient.invalidateQueries({ queryKey: ['posts', /*...same
identifiers used in queries*/], exact: true }) and
queryClient.invalidateQueries({ queryKey: ['home','recent-posts', /*...same
identifiers*/], exact: true }) (or fill in the concrete identifiers used by your
queries) so the keys match precisely.
src/components/layout/header/Header.tsx (2)

38-44: 중복 조건 검사 간소화 가능

isEditPagetrue이면 editPostId는 이미 null이 아님이 보장됩니다 (라인 33 정의). 중복 검사를 제거할 수 있습니다.

♻️ 제안된 수정
   const handleSubmit = () => {
-    if (isEditPage && editPostId !== null) {
-      submitUpdate(editPostId);
+    if (isEditPage) {
+      submitUpdate(editPostId!);
     } else {
       submitCreate();
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/header/Header.tsx` around lines 38 - 44, The
handleSubmit function currently checks both isEditPage and editPostId !== null
but editPostId is guaranteed non-null whenever isEditPage is true; simplify
handleSubmit by removing the redundant null check so that when isEditPage is
true you directly call submitUpdate(editPostId) and otherwise call
submitCreate(); update any TypeScript signatures or assertions if needed to
satisfy the compiler (e.g., narrow editPostId or use a non-null assertion) and
reference handleSubmit, isEditPage, editPostId, submitUpdate, and submitCreate
when making the change.

116-122: 중첩 삼항 연산자 가독성 개선 제안

버튼 텍스트의 중첩 삼항 연산자가 다소 읽기 어렵습니다. 가독성을 위해 헬퍼 함수나 변수로 분리하는 것을 고려해 보세요.

♻️ 제안된 수정
+  const getSubmitButtonText = () => {
+    if (isEditPage) return isPending ? '수정 중...' : '수정 완료';
+    return isPending ? '게시 중...' : '게시하기';
+  };
+
   // ... in JSX:
-                  {isEditPage
-                    ? isPending
-                      ? '수정 중...'
-                      : '수정 완료'
-                    : isPending
-                      ? '게시 중...'
-                      : '게시하기'}
+                  {getSubmitButtonText()}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/header/Header.tsx` around lines 116 - 122, The nested
ternary for the button label (using isEditPage and isPending) is hard to read;
extract the logic into a small helper or computed variable (e.g., a function
getButtonText or a const buttonText) inside the Header component so it returns
one of '수정 중...', '수정 완료', '게시 중...', or '게시하기' based on isEditPage and
isPending, then replace the inline nested ternary with that helper/variable
reference; ensure the helper uses clear if/else or switch for readability and
keeps existing semantics.
src/hooks/useNavigationGuard.ts (1)

40-42: beforeunload 이벤트 핸들러에 returnValue 설정 필요

일부 브라우저(특히 구형 브라우저)에서는 e.preventDefault()만으로는 충분하지 않습니다. 크로스 브라우저 호환성을 위해 returnValue를 설정해야 합니다.

♻️ 제안된 수정
     const handleBeforeUnload = (e: BeforeUnloadEvent) => {
       e.preventDefault();
+      e.returnValue = '';
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useNavigationGuard.ts` around lines 40 - 42, The beforeunload
handler handleBeforeUnload currently only calls e.preventDefault(), which is not
sufficient for some browsers; update handleBeforeUnload to also set
e.returnValue = '' (or a descriptive string) to ensure cross-browser prompts are
shown, and keep the preventDefault() call for modern behavior.
src/hooks/board/useUpdatePost.ts (2)

49-50: catch 블록에서 에러 로깅 추가 권장

useCreatePost와 마찬가지로 에러 객체를 로깅하는 것이 디버깅에 도움됩니다.

♻️ 제안된 수정
-    } catch {
+    } catch (error) {
+      console.error('게시글 수정 실패:', error);
       toast({ title: '게시글 수정에 실패했습니다.', variant: 'error' });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/board/useUpdatePost.ts` around lines 49 - 50, The catch block in
useUpdatePost currently swallows errors; update it to accept the error parameter
(e.g., catch (error)) and log the error object before showing the toast so
debugging is easier—use the same logging approach used in useCreatePost
(console.error or the project logger) and include a clear message like "Failed
to update post" along with the error when calling the logger, then keep the
existing toast({ title: '게시글 수정에 실패했습니다.', variant: 'error' }).

15-57: useCreatePost와 중복 로직 존재

useCreatePost와 검증 로직(title, content, files 체크)이 거의 동일합니다. 향후 공통 검증 유틸리티로 추출하는 것을 고려해 볼 수 있습니다. 현재 구조도 작동하지만, 검증 규칙이 변경될 때 두 파일을 모두 수정해야 합니다.

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

In `@src/hooks/board/useUpdatePost.ts` around lines 15 - 57, The submit validation
logic in useUpdatePost (function submit) duplicates checks from useCreatePost
(title.trim, content.trim, files upload state and toast messages); extract these
validations into a shared utility (e.g., validatePostForSubmit or
validatePostPayload) that accepts the post state (usePostStore.getState() or
{title, content, files}) and returns success/failure (or throws/errors) so both
useCreatePost and useUpdatePost call the same validator before proceeding;
update useUpdatePost.submit to call that validator, keep existing behavior
(toasts, early returns) and preserve updatePost/reset/router.push flows so
validation logic is centralized and consistent.
src/hooks/board/useCreatePost.ts (1)

55-56: catch 블록에서 에러 로깅 추가 권장

에러 객체를 무시하고 있어 디버깅이 어려울 수 있습니다. 개발 환경에서라도 에러를 로깅하는 것이 좋습니다.

♻️ 제안된 수정
-    } catch {
+    } catch (error) {
+      console.error('게시글 작성 실패:', error);
       toast({ title: '게시글 작성에 실패했습니다.', variant: 'error' });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/board/useCreatePost.ts` around lines 55 - 56, useCreatePost의 catch
블록이 에러 객체를 무시하고 있어 디버깅이 어렵습니다; catch문을 catch (err) 형태로 수정하고, 에러를 console.error
또는 프로젝트의 로거(예: logger.error)로 기록한 다음 기존 toast({ title: '게시글 작성에 실패했습니다.',
variant: 'error' })를 유지하여 사용자 알림과 개발용 로그를 모두 남기도록 변경하세요; 참조 심볼: useCreatePost,
toast.
src/stores/usePostStore.ts (1)

91-116: initFromDetail 구현 LGTM - 방어적 코드 추가 권장

기존 파일의 blob URL 정리와 PostDetail 데이터 매핑이 올바르게 구현되었습니다. 다만 post.fileUrls가 undefined일 경우를 대비한 방어적 코드를 추가하면 더 안전합니다.

🛡️ 방어적 코드 제안
             files: post.fileUrls.map((f) => ({
+            files: (post.fileUrls ?? []).map((f) => ({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/usePostStore.ts` around lines 91 - 116, The initFromDetail handler
should guard against post.fileUrls being undefined before mapping; update
initFromDetail to treat post.fileUrls safely (e.g. use a fallback empty array or
optional chaining) when constructing the files array so the map call never
throws, while keeping the existing blob cleanup loop that uses get().files and
URL.revokeObjectURL and the call to set(..., false, 'initFromDetail') intact.
🤖 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)/(main)/board/edit/[id]/page.tsx:
- Around line 8-17: The page lacks error handling around the server call to
boardServerApi.getPostById in PostEditPage — wrap the call in a try/catch (or
check the response status) and handle missing/invalid posts by returning a safe
fallback (e.g., call next/navigation's notFound() or render an error/fallback UI
instead of passing undefined to EditClientEditor). Specifically, update
PostEditPage to catch errors from boardServerApi.getPostById, handle 404 by
invoking notFound() or rendering a user-friendly message, and handle other
errors by logging and returning an error UI so EditClientEditor never receives
an invalid response.data.

In `@src/app/`(private)/(main)/board/write/ClientEditor.tsx:
- Around line 20-26: The useEffect in ClientEditor.tsx intentionally runs only
on mount but ESLint warns about missing dependencies (activeBoardId, reset,
setBoard); add an ESLint disable comment to make the intent explicit by placing
// eslint-disable-next-line react-hooks/exhaustive-deps immediately above the
useEffect and include a brief inline rationale (e.g., "run on mount only; reset
and setBoard are stable/intentional") so the hook using reset() and
setBoard(activeBoardId) is left with an empty dependency array intentionally.

In `@src/components/board/PostEditorShell.tsx`:
- Around line 38-43: hasChanges currently flags true on edit pages because
title/content/files are set from initFromDetail; fix by comparing current values
against an initial snapshot stored in the post store instead of just checking
lengths. When loading detail in initFromDetail, save an initialSnapshot (e.g., {
title, content, filesCount }) to usePostStore; compute hasChanges by comparing
usePostStore state.title, state.content and state.files.length to
initialSnapshot.title/content/filesCount (fallback to the existing length checks
when initialSnapshot is absent for create mode). Use a memoized selector or
useMemo for hasChanges and keep feeding its boolean into useNavigationGuard as
before.

In `@src/hooks/board/useFileAttach.ts`:
- Around line 39-40: useFileAttach.ts sets contentType to selected.type which
can be an empty string when the browser can't determine the MIME type; update
the assignment in the useFileAttach hook (where selected is used to build the
file metadata) to use the same fallback as useFileUpload.ts, e.g. set
contentType to selected.type || 'application/octet-stream' so empty types are
normalized.

---

Nitpick comments:
In `@src/app/globals.css`:
- Around line 628-630: The selector ".ProseMirror p:has(> code:not(pre code))"
may not reliably express "paragraphs containing inline code but not code inside
<pre>" across all browsers and the nested :not(pre code) can be confusing;
confirm the intent is to target inline <code> that are direct children of <p>
(excluding any <pre><code>) and either (A) keep the current selector but add a
comment noting browser compatibility requirements for :has() (Safari 15.4+,
Chrome 105+) or (B) if wider compatibility is needed, replace the selector logic
with a more explicit approach (e.g., target direct-child code elements within
.ProseMirror p and exclude paragraphs that contain <pre>) by updating the CSS
rules that reference the selector and adding a brief inline comment referencing
".ProseMirror p:has(> code:not(pre code))" so future readers know the intent.

In `@src/components/board/Comment/CommentItem.tsx`:
- Around line 72-77: ActionMenu's prop onDeleteSelect expects a handler with
signature (event: Event) => void while CommentItemProps.onDelete is defined as
() => void; fix by either updating CommentItemProps.onDelete to accept an Event
parameter or, without changing types, pass a wrapper function to ActionMenu that
accepts the event and calls the existing onDelete (e.g. create an inline handler
in CommentItem that takes event: Event and invokes onDelete()); refer to
ActionMenu, CommentItemProps.onDelete and ReplyItem for the analogous fix.

In `@src/components/board/Comment/ReplyItem.tsx`:
- Around line 46-51: The onDeleteSelect prop passed to <ActionMenu> expects a
handler that accepts an Event (per DropdownMenuItem.onSelect), but
ReplyItemProps.onDelete is typed as () => void, causing a signature mismatch;
update the ReplyItem implementation to either change the prop type to onDelete:
(event?: Event) => void or wrap the existing onDelete with a small adapter
(e.g., onDeleteSelect={(e) => onDelete()} or onDeleteSelect={(e) =>
onDelete?.(e)}) so the handler you pass from ReplyItem matches ActionMenu's
expected signature; reference ActionMenu, onDeleteSelect, and
ReplyItemProps.onDelete when making the change.

In `@src/components/board/PostCard/PostCardContent.tsx`:
- Around line 30-58: The useLineClamp hook currently spreads deps in the
useEffect dependency array which ESLint flags; update the hook to accept an
explicit single dependency (e.g., rename parameter from deps to contentDep or
similar) and use that single value in the dependency array alongside enabled,
then update call sites to pass the specific content string instead of an array;
modify symbols: function useLineClamp<T extends HTMLElement>(enabled: boolean,
deps: unknown[]) -> useLineClamp<T>(enabled: boolean, contentDep: unknown) and
change useEffect(..., [enabled, contentDep]) so the effect has a static,
explicit dependency.

In `@src/components/board/PostDeleteDialog.tsx`:
- Around line 19-23: handleConfirm currently calls await submit(...) and then
always calls onOpenChange(false), which closes the dialog even when deletion
failed; change handleConfirm to only close the dialog on successful deletion by
wrapping the submit call in try/catch: call await submit(postId, onDeleted)
inside try and call onOpenChange(false) and onDeleted() only on success, and in
catch leave the dialog open (optionally rethrow or let useDeletePost's toast
handle error). Reference function names: handleConfirm, submit, onOpenChange,
onDeleted (and useDeletePost behavior) when making the change.

In `@src/components/layout/header/Header.tsx`:
- Around line 38-44: The handleSubmit function currently checks both isEditPage
and editPostId !== null but editPostId is guaranteed non-null whenever
isEditPage is true; simplify handleSubmit by removing the redundant null check
so that when isEditPage is true you directly call submitUpdate(editPostId) and
otherwise call submitCreate(); update any TypeScript signatures or assertions if
needed to satisfy the compiler (e.g., narrow editPostId or use a non-null
assertion) and reference handleSubmit, isEditPage, editPostId, submitUpdate, and
submitCreate when making the change.
- Around line 116-122: The nested ternary for the button label (using isEditPage
and isPending) is hard to read; extract the logic into a small helper or
computed variable (e.g., a function getButtonText or a const buttonText) inside
the Header component so it returns one of '수정 중...', '수정 완료', '게시 중...', or
'게시하기' based on isEditPage and isPending, then replace the inline nested ternary
with that helper/variable reference; ensure the helper uses clear if/else or
switch for readability and keeps existing semantics.

In `@src/hooks/board/useCreatePost.ts`:
- Around line 55-56: useCreatePost의 catch 블록이 에러 객체를 무시하고 있어 디버깅이 어렵습니다; catch문을
catch (err) 형태로 수정하고, 에러를 console.error 또는 프로젝트의 로거(예: logger.error)로 기록한 다음 기존
toast({ title: '게시글 작성에 실패했습니다.', variant: 'error' })를 유지하여 사용자 알림과 개발용 로그를 모두
남기도록 변경하세요; 참조 심볼: useCreatePost, toast.

In `@src/hooks/board/useDeletePost.ts`:
- Around line 28-31: The invalidateQueries calls in useDeletePost.ts use broad
prefix matching (queryKey ['posts'] and ['home','recent-posts']), so make the
intent explicit by either supplying the full query key shape used elsewhere
(e.g., include clubId/activeBoardId segments) or pass the exact matching option
to avoid accidental matches; update the two calls to
queryClient.invalidateQueries({ queryKey: ['posts', /*...same identifiers used
in queries*/], exact: true }) and queryClient.invalidateQueries({ queryKey:
['home','recent-posts', /*...same identifiers*/], exact: true }) (or fill in the
concrete identifiers used by your queries) so the keys match precisely.

In `@src/hooks/board/useUpdatePost.ts`:
- Around line 49-50: The catch block in useUpdatePost currently swallows errors;
update it to accept the error parameter (e.g., catch (error)) and log the error
object before showing the toast so debugging is easier—use the same logging
approach used in useCreatePost (console.error or the project logger) and include
a clear message like "Failed to update post" along with the error when calling
the logger, then keep the existing toast({ title: '게시글 수정에 실패했습니다.', variant:
'error' }).
- Around line 15-57: The submit validation logic in useUpdatePost (function
submit) duplicates checks from useCreatePost (title.trim, content.trim, files
upload state and toast messages); extract these validations into a shared
utility (e.g., validatePostForSubmit or validatePostPayload) that accepts the
post state (usePostStore.getState() or {title, content, files}) and returns
success/failure (or throws/errors) so both useCreatePost and useUpdatePost call
the same validator before proceeding; update useUpdatePost.submit to call that
validator, keep existing behavior (toasts, early returns) and preserve
updatePost/reset/router.push flows so validation logic is centralized and
consistent.

In `@src/hooks/useNavigationGuard.ts`:
- Around line 40-42: The beforeunload handler handleBeforeUnload currently only
calls e.preventDefault(), which is not sufficient for some browsers; update
handleBeforeUnload to also set e.returnValue = '' (or a descriptive string) to
ensure cross-browser prompts are shown, and keep the preventDefault() call for
modern behavior.

In `@src/stores/usePostStore.ts`:
- Around line 91-116: The initFromDetail handler should guard against
post.fileUrls being undefined before mapping; update initFromDetail to treat
post.fileUrls safely (e.g. use a fallback empty array or optional chaining) when
constructing the files array so the map call never throws, while keeping the
existing blob cleanup loop that uses get().files and URL.revokeObjectURL and the
call to set(..., false, 'initFromDetail') intact.
🪄 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: 5998b373-b5c1-49e9-b583-a5a4584768a5

📥 Commits

Reviewing files that changed from the base of the PR and between 1c80dc2 and 9e95ac5.

📒 Files selected for processing (33)
  • src/app/(private)/(main)/board/(with-nav)/BoardNavClient.tsx
  • src/app/(private)/(main)/board/(with-nav)/[id]/PostDetailContent.tsx
  • src/app/(private)/(main)/board/edit/[id]/EditClientEditor.tsx
  • src/app/(private)/(main)/board/edit/[id]/page.tsx
  • src/app/(private)/(main)/board/write/ClientEditor.tsx
  • src/app/globals.css
  • src/components/board/ActionMenu.tsx
  • src/components/board/BoardContent.tsx
  • src/components/board/CategorySelector.tsx
  • src/components/board/Comment/CommentItem.tsx
  • src/components/board/Comment/ReplyItem.tsx
  • src/components/board/Editor/index.tsx
  • src/components/board/Editor/usePostEditor.ts
  • src/components/board/PostActionMenu.tsx
  • src/components/board/PostCard/PostCardContent.tsx
  • src/components/board/PostCard/index.tsx
  • src/components/board/PostDeleteDialog.tsx
  • src/components/board/PostDetailHeader.tsx
  • src/components/board/PostEditorShell.tsx
  • src/components/board/index.ts
  • src/components/home/HomeBoardContent.tsx
  • src/components/layout/header/Header.tsx
  • src/hooks/board/useCreatePost.ts
  • src/hooks/board/useDeletePost.ts
  • src/hooks/board/useFileAttach.ts
  • src/hooks/board/useUpdatePost.ts
  • src/hooks/index.ts
  • src/hooks/useFileUpload.ts
  • src/hooks/useNavigationGuard.ts
  • src/lib/actions/board.ts
  • src/lib/apis/board.server.ts
  • src/stores/usePostStore.ts
  • src/types/board.ts

Comment on lines +8 to +17
export default async function PostEditPage({ params }: PostEditPageProps) {
const { id } = await params;
// TODO: 추후 하드코딩된 clubId 제거 예정
const response = await boardServerApi.getPostById('YUNJcjFKMO', Number(id));

return (
<main className="w-full">
<EditClientEditor post={response.data} />
</main>
);
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

존재하지 않는 게시글에 대한 에러 처리 부재

게시글 ID가 유효하지 않거나 게시글이 존재하지 않을 때의 에러 처리가 없습니다. API 호출 실패 시 런타임 에러가 발생할 수 있습니다.

🛡️ 에러 처리 추가 제안
+import { notFound } from 'next/navigation';
 import { boardServerApi } from '@/lib/apis/board.server';
 import { EditClientEditor } from './EditClientEditor';

 interface PostEditPageProps {
   params: Promise<{ id: string }>;
 }

 export default async function PostEditPage({ params }: PostEditPageProps) {
   const { id } = await params;
+  const postId = Number(id);
+  if (Number.isNaN(postId)) {
+    notFound();
+  }
+
   // TODO: 추후 하드코딩된 clubId 제거 예정
-  const response = await boardServerApi.getPostById('YUNJcjFKMO', Number(id));
+  const response = await boardServerApi.getPostById('YUNJcjFKMO', postId);

   return (
     <main className="w-full">
       <EditClientEditor post={response.data} />
     </main>
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(private)/(main)/board/edit/[id]/page.tsx around lines 8 - 17, The
page lacks error handling around the server call to boardServerApi.getPostById
in PostEditPage — wrap the call in a try/catch (or check the response status)
and handle missing/invalid posts by returning a safe fallback (e.g., call
next/navigation's notFound() or render an error/fallback UI instead of passing
undefined to EditClientEditor). Specifically, update PostEditPage to catch
errors from boardServerApi.getPostById, handle 404 by invoking notFound() or
rendering a user-friendly message, and handle other errors by logging and
returning an error UI so EditClientEditor never receives an invalid
response.data.

Comment on lines +20 to +26
// 글쓰기 페이지 진입 시 store 초기화 후 현재 게시판으로 설정
useEffect(() => {
if (board === null && defaultId !== null) {
setBoard(defaultId);
reset();
if (activeBoardId !== null) {
setBoard(activeBoardId);
}
}, [board, defaultId, setBoard]);
}, []);
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

의도적인 마운트 전용 effect의 ESLint 경고 처리

effect가 마운트 시에만 실행되도록 의도적으로 빈 의존성 배열을 사용한 것으로 보입니다. 그러나 ESLint가 누락된 의존성(activeBoardId, reset, setBoard)에 대해 경고하고 있습니다.

의도적인 동작이라면 ESLint 비활성화 주석을 추가하여 의도를 명확히 하세요.

🔧 ESLint 경고 처리 제안
  // 글쓰기 페이지 진입 시 store 초기화 후 현재 게시판으로 설정
  useEffect(() => {
    reset();
    if (activeBoardId !== null) {
      setBoard(activeBoardId);
    }
+   // eslint-disable-next-line react-hooks/exhaustive-deps -- 마운트 시 1회만 실행
  }, []);
🧰 Tools
🪛 GitHub Check: Lint & Build

[warning] 26-26:
React Hook useEffect has missing dependencies: 'activeBoardId', 'reset', and 'setBoard'. Either include them or remove the dependency array

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

In `@src/app/`(private)/(main)/board/write/ClientEditor.tsx around lines 20 - 26,
The useEffect in ClientEditor.tsx intentionally runs only on mount but ESLint
warns about missing dependencies (activeBoardId, reset, setBoard); add an ESLint
disable comment to make the intent explicit by placing //
eslint-disable-next-line react-hooks/exhaustive-deps immediately above the
useEffect and include a brief inline rationale (e.g., "run on mount only; reset
and setBoard are stable/intentional") so the hook using reset() and
setBoard(activeBoardId) is left with an empty dependency array intentionally.

Comment on lines +38 to +43
const title = usePostStore((s) => s.title);
const content = usePostStore((s) => s.content);
const files = usePostStore((s) => s.files);

const hasChanges = title.length > 0 || content.length > 0 || files.length > 0;
const { open, onConfirm, onCancel } = useNavigationGuard({ enabled: hasChanges });
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

수정 모드에서 hasChanges 오탐 가능성

현재 로직은 title.length > 0 || content.length > 0 || files.length > 0으로 변경 여부를 판단합니다. 수정 페이지에서는 initFromDetail로 기존 데이터가 로드되어 처음부터 hasChangestrue가 됩니다.

이로 인해 사용자가 아무 변경 없이 뒤로가기해도 경고 다이얼로그가 표시될 수 있습니다. 실제 변경 여부를 감지하려면 초기 상태와 비교하는 로직이 필요할 수 있습니다.

💡 해결 방안 예시
// Store에 initialSnapshot을 저장하고 비교하는 방식
const hasChanges = useMemo(() => {
  const initial = usePostStore.getState().initialSnapshot;
  if (!initial) return title.length > 0 || content.length > 0 || files.length > 0;
  return title !== initial.title || content !== initial.content || files.length !== initial.filesCount;
}, [title, content, files]);
🤖 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 38 - 43, hasChanges
currently flags true on edit pages because title/content/files are set from
initFromDetail; fix by comparing current values against an initial snapshot
stored in the post store instead of just checking lengths. When loading detail
in initFromDetail, save an initialSnapshot (e.g., { title, content, filesCount
}) to usePostStore; compute hasChanges by comparing usePostStore state.title,
state.content and state.files.length to initialSnapshot.title/content/filesCount
(fallback to the existing length checks when initialSnapshot is absent for
create mode). Use a memoized selector or useMemo for hasChanges and keep feeding
its boolean into useNavigationGuard as before.

Comment on lines +39 to +40
fileSize: selected.size,
contentType: selected.type,
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

contentType 빈 문자열 처리 누락

브라우저가 MIME 타입을 판별하지 못하면 selected.type이 빈 문자열을 반환합니다. useFileUpload.ts에서는 file.type || 'application/octet-stream' 폴백을 사용하고 있으므로, 일관성을 위해 동일한 처리를 적용하세요.

🔧 폴백 추가
     setFile({
       id: crypto.randomUUID(),
       file: selected,
       fileName: selected.name,
       fileUrl: URL.createObjectURL(selected),
       fileSize: selected.size,
-      contentType: selected.type,
+      contentType: selected.type || 'application/octet-stream',
       storageKey: '',
       uploaded: false,
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/board/useFileAttach.ts` around lines 39 - 40, useFileAttach.ts sets
contentType to selected.type which can be an empty string when the browser can't
determine the MIME type; update the assignment in the useFileAttach hook (where
selected is used to build the file metadata) to use the same fallback as
useFileUpload.ts, e.g. set contentType to selected.type ||
'application/octet-stream' so empty types are normalized.

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

🧹 Nitpick comments (1)
src/hooks/board/useUpdatePost.ts (1)

50-52: 수정 저장 후에는 push보다 replace가 더 자연스럽습니다.

Line 52는 /board/:id -> /board/edit/:id -> /board/:id 히스토리를 남겨서, 저장 직후 뒤로가기가 다시 수정 화면으로 돌아갑니다. 수정 완료 후 상세로 복귀하는 흐름이면 router.replace()가 UX를 덜 놀라게 합니다.

간단한 변경안
-      router.push(`/board/${postId}`);
+      router.replace(`/board/${postId}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/board/useUpdatePost.ts` around lines 50 - 52, Replace the
router.push call with router.replace in the success path of the useUpdatePost
hook so the edit page isn't left in browser history; specifically, in the block
where toast(...), reset(), and router.push(`/board/${postId}`) are called,
change the navigation from router.push to router.replace so the flow returns to
`/board/${postId}` without keeping the edit route in the back stack.
🤖 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/components/layout/header/Header.tsx`:
- Around line 45-52: The header currently binds the write button directly to
useWritePost.handleWriteClick while profileStatus can be undefined during query
loading, causing the first click to incorrectly evaluate
!profileStatus?.cardinalAssigned and open the "cardinal input" modal; either
disable the button in Header.tsx until profileStatus is loaded or update
useWritePost (and its handleWriteClick) to explicitly handle undefined
profileStatus (e.g., treat undefined as "loading" and no-op or queue action) so
clicks during loading won't open the wrong modal — locate useWritePost,
handleWriteClick, and profileStatus to implement the chosen fix and ensure
cardinalModalOpen/profileModalOpen behavior remains correct.

In `@src/hooks/board/useUpdatePost.ts`:
- Around line 35-38: 현재의 content.trim() 검증은 Tiptap이 반환하는 "<p></p>" 같은 HTML을 빈
문서로 감지하지 못합니다; useUpdatePost 훅에서 제출 전 검증을 editor 인스턴스 기반으로 바꿔 editor?.isEmpty()
또는 editor?.getText().trim().length === 0 를 사용해 빈 문서를 판별하고, editor가 없을 경우에만 기존
content(혹은 editor.getHTML())를 폴백으로 검사하도록 변경하세요; 실패 시 기존 toast 호출(‘내용을 입력해주세요.’)과
return 흐름을 유지하도록 수정하면 됩니다.

---

Nitpick comments:
In `@src/hooks/board/useUpdatePost.ts`:
- Around line 50-52: Replace the router.push call with router.replace in the
success path of the useUpdatePost hook so the edit page isn't left in browser
history; specifically, in the block where toast(...), reset(), and
router.push(`/board/${postId}`) are called, change the navigation from
router.push to router.replace so the flow returns to `/board/${postId}` without
keeping the edit route in the back stack.
🪄 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: 08f583c3-a0e2-4e90-aae7-207a76a53f8d

📥 Commits

Reviewing files that changed from the base of the PR and between 9e95ac5 and 8beeab9.

📒 Files selected for processing (4)
  • src/components/layout/header/Header.tsx
  • src/hooks/board/useCreatePost.ts
  • src/hooks/board/useUpdatePost.ts
  • src/utils/shared/file.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/hooks/board/useCreatePost.ts

Comment on lines +45 to +52
const {
handleWriteClick,
handleSkipProfile,
cardinalModalOpen,
setCardinalModalOpen,
profileModalOpen,
setProfileModalOpen,
} = useWritePost();
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 | 🟠 Major

프로필 상태 로딩 중 첫 클릭이 잘못된 모달로 갈 수 있습니다.

useWritePosthandleWriteClickprofileStatus가 아직 없으면 !profileStatus?.cardinalAssigned를 true로 평가합니다 (src/hooks/home/useWritePost.ts:7-39). 지금처럼 Line 148에서 버튼을 바로 연결하면, 쿼리 로딩 중 첫 클릭이 실제 상태와 무관하게 "기수 입력" 모달로 빠질 수 있습니다. 로딩이 끝날 때까지 버튼을 막거나, hook에서 undefined를 별도 처리해 주세요.

Also applies to: 145-149

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

In `@src/components/layout/header/Header.tsx` around lines 45 - 52, The header
currently binds the write button directly to useWritePost.handleWriteClick while
profileStatus can be undefined during query loading, causing the first click to
incorrectly evaluate !profileStatus?.cardinalAssigned and open the "cardinal
input" modal; either disable the button in Header.tsx until profileStatus is
loaded or update useWritePost (and its handleWriteClick) to explicitly handle
undefined profileStatus (e.g., treat undefined as "loading" and no-op or queue
action) so clicks during loading won't open the wrong modal — locate
useWritePost, handleWriteClick, and profileStatus to implement the chosen fix
and ensure cardinalModalOpen/profileModalOpen behavior remains correct.

Comment on lines +35 to +38
if (!content.trim()) {
toast({ title: '내용을 입력해주세요.', variant: 'error' });
return;
}
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 에디터 본문이 어떤 형식으로 store에 저장되는지 확인
fd -i '^ClientEditor\.tsx$' src | xargs -r -I{} sed -n '1,220p' "{}"
fd -i '^EditClientEditor\.tsx$' src | xargs -r -I{} sed -n '1,220p' "{}"
fd -i '^usePostStore\.ts$' src | xargs -r -I{} sed -n '1,220p' "{}"

# HTML/텍스트 직렬화 지점을 탐색
rg -nP --type=ts --type=tsx 'getHTML\(|getText\(|setContent\(|content\s*:' src

Repository: Team-Weeth/weeth-client

Length of output: 6410


🏁 Script executed:

cat -n src/hooks/board/useUpdatePost.ts | head -60

Repository: Team-Weeth/weeth-client

Length of output: 2230


🏁 Script executed:

fd -type f -name 'PostEditorShell*' src

Repository: Team-Weeth/weeth-client

Length of output: 237


🏁 Script executed:

rg -n 'getHTML|getText|setContent|content.*=' src/components/board/PostEditorShell.tsx -A 3 -B 1

Repository: Team-Weeth/weeth-client

Length of output: 305


🏁 Script executed:

rg -n 'interface PostDetail|type PostDetail' src/types

Repository: Team-Weeth/weeth-client

Length of output: 135


🏁 Script executed:

cat -n src/components/board/PostEditorShell.tsx

Repository: Team-Weeth/weeth-client

Length of output: 2903


🏁 Script executed:

cat -n src/types/board.ts | grep -A 30 'interface PostBase\|type PostBase'

Repository: Team-Weeth/weeth-client

Length of output: 853


🏁 Script executed:

rg -n 'setContent.*getHTML|editor\.getHTML' src --type ts --type tsx

Repository: Team-Weeth/weeth-client

Length of output: 94


🏁 Script executed:

find src -name 'Editor.tsx' -o -name 'editor.tsx' | head -5

Repository: Team-Weeth/weeth-client

Length of output: 49


🏁 Script executed:

cat -n src/components/board/Editor.tsx 2>/dev/null || cat -n src/components/board/editor.tsx 2>/dev/null || echo "File not found in standard locations"

Repository: Team-Weeth/weeth-client

Length of output: 103


🏁 Script executed:

rg -n 'setContent|useEditor|getHTML' src/components/board/ -A 2 -B 1

Repository: Team-Weeth/weeth-client

Length of output: 2616


🏁 Script executed:

rg -n 'isEmpty|getText|getJSON' src/components/board/Editor/ -B 1 -A 1

Repository: Team-Weeth/weeth-client

Length of output: 49


🏁 Script executed:

rg -n 'content.*trim|content.*length' src/hooks/ -B 2 -A 1

Repository: Team-Weeth/weeth-client

Length of output: 539


Tiptap 편집기의 빈 문서를 trim() 검증으로 감지할 수 없습니다.

editor.getHTML()로 저장된 콘텐츠는 빈 에디터에서 <p></p> 같은 HTML을 반환합니다. Line 35의 content.trim()은 이를 감지하지 못하고, 사용자가 아무 내용도 입력하지 않은 상태로 제출할 수 있게 됩니다.

에디터의 실제 텍스트 콘텐츠 존재 여부(getText() 또는 editor.isEmpty())를 기반으로 검증하거나, plain text 길이를 체크하는 방식으로 개선해주세요.

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

In `@src/hooks/board/useUpdatePost.ts` around lines 35 - 38, 현재의 content.trim()
검증은 Tiptap이 반환하는 "<p></p>" 같은 HTML을 빈 문서로 감지하지 못합니다; useUpdatePost 훅에서 제출 전 검증을
editor 인스턴스 기반으로 바꿔 editor?.isEmpty() 또는 editor?.getText().trim().length === 0 를
사용해 빈 문서를 판별하고, editor가 없을 경우에만 기존 content(혹은 editor.getHTML())를 폴백으로 검사하도록
변경하세요; 실패 시 기존 toast 호출(‘내용을 입력해주세요.’)과 return 흐름을 유지하도록 수정하면 됩니다.

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.

나영님은 정말 부지런쟁이.... 작업 속도 짱짱...

const submit = async (postId: number) => {
const { title, content, files, getPayload, reset } = usePostStore.getState();

if (!clubId) {
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.

요기 검증 로직 useCreatePost랑 중복되니까 유틸로 빼면 좋을 거 같애용

import dynamic from 'next/dynamic';
import { ActionMenu, type ActionMenuProps } from './ActionMenu';

const PostDeleteDialog = dynamic(() =>
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.

요거 컴포넌트가 몇줄 안 되는데 코드 스플리팅,, 굳이 없어도 되지 않을까!! 싶습니당

variant = 'list',
}: PostCardContentProps) {
const contentRef = useRef<HTMLParagraphElement>(null);
function PostCardTitle({ title, isNew, size }: PostCardTitleProps) {
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.

안에 컴포넌트 function이랑 훅이 혼재하는데 유지 보수 편하게 분리하믄 어떨까요,,

ro.observe(el);
return () => ro.disconnect();
}, [content, expandable]);
}, [enabled, ...deps]);
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.

요거 spread된 배열을 넣기 보다는 단일 값을 넘겨주는 게 좋을 거 같습니당

export function useLineClamp<T extends HTMLElement>(enabled: boolean, content: string) {
  useEffect(() => {
    // 생략
  }, [enabled, content]);
}

요런 식으루요

호출부는 이렇게
const elementRef = useLineClamp<HTMLParagraphElement>(true, content);
이러면 원시값이 바뀔 때만 훅이 실행 되니까,,,

Comment on lines +43 to +52
const { submit: submitCreate, isPending: isCreating } = useCreatePost();
const { submit: submitUpdate, isPending: isUpdating } = useUpdatePost();
const {
handleWriteClick,
handleSkipProfile,
cardinalModalOpen,
setCardinalModalOpen,
profileModalOpen,
setProfileModalOpen,
} = useWritePost();
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.

헤더에 CRUD 로직까지 섞여도 괜찮을까.. 고민이 되네요.. 너무 역할이 많아지는 느낌.. 버튼 게시 영역을 컴포넌트로 빼면 어떨까요,,,

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.

흠... 저도 유진님과 동일한 생각입니닷... 버튼 게시 영역만 컴포넌트로 분리해도 좋을 것 같아요

const initializedRef = useRef(false);
if (!initializedRef.current) {
initializedRef.current = true;
usePostStore.getState().initFromDetail(post);
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.

렌더중 store 변경은 권장되지 않으니까,, seEffect + ref guard 패턴이 더 좋을 거 같습니다!

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.

수고하셨습니다ㅜㅜ!!! 전반적으로 코드 주석이 조금 많은 느낌인데 나중에 기능 자체가 조금씩 변경되거나 수정되는 경우에는 주석도 함께 수정을 해줘야되다보니 최대한 함수명에 해당 기능을 자세히 설명하도록 하거나 중요한 주석을 제외한 부분은 삭제해도 좋을 것 같아욤!!☺️☺️

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.

해당 파일에서 PostCardTitle, useLineClamp, ExpandButton, PostCardListContent, PostCardDetailContent 얘네는 훅은 훅대로 컴포넌트는 컴포넌트로 분리해도 좋을 것 같습니다!!

<Icon src={MoreVerticalIcon} size={16} className="text-icon-normal" />
</Button>
</DropdownMenuTrigger>
</span>
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.

이벤트 전파를 span에서 처리하신 특별한 이유가 있으실까욤?? eslint disable처리보다 Button의 onClickCapture로 처리하면 래퍼 자체가 필요 없어질 것 같은데 어떻게 생각하시나욤??

<DropdownMenuTrigger asChild>
  <Button
    type="button"
    variant={triggerVariant}
    size={triggerSize}
    className={cn('h-600 w-600', triggerClassName)}
    aria-label="더보기"
    onClickCapture={(e) => {
      e.preventDefault();
      e.stopPropagation();
    }}
  >
    <Icon src={MoreVerticalIcon} size={16} className="text-icon-normal" />
  </Button>
</DropdownMenuTrigger>

요렇게욤 아닌가 혹시 DropdownMenuTrigger 해당 컴ㅁ포넌트보다 상위에서 이벤트 전파를 막아줘야할까욤...?!!

Comment on lines +20 to +27
/**
* title 구독을 shell 밖으로 격리
*/
function BoundTitleInput() {
const title = usePostStore((s) => s.title);
const setTitle = usePostStore((s) => s.setTitle);
return <TitleInput value={title} onChange={(e) => setTitle(e.target.value)} />;
}
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.

다들 코드 스타일에 따라서 다를 것 같긴한데 저는 단일 책임 원칙에 따라서 최대한 하나의 함수는 단 하나의 책임만 가져야 한다고 최대한 파일도 함수에 따라서 분리를 해두는데 요 부분은 너무 짧아서 .... 굳이 파일 분리를 안해도 될라나요... 헤헷 모르겟다

Comment on lines +43 to +52
const { submit: submitCreate, isPending: isCreating } = useCreatePost();
const { submit: submitUpdate, isPending: isUpdating } = useUpdatePost();
const {
handleWriteClick,
handleSkipProfile,
cardinalModalOpen,
setCardinalModalOpen,
profileModalOpen,
setProfileModalOpen,
} = useWritePost();
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.

흠... 저도 유진님과 동일한 생각입니닷... 버튼 게시 영역만 컴포넌트로 분리해도 좋을 것 같아요

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📬 API 서버 API 통신 ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants