[Feat] WTH-241 게시글 작성/수정/삭제 API 연결#39
Conversation
- ActionMenu를 범용 수정/삭제 메뉴로 분리하고 PostActionMenu는 게시글 전용 래퍼로 재작성 - PostDeleteDialog + useDeletePost 훅 추가, 삭제 성공 시 board 경로 revalidate - 댓글/답글은 공통 ActionMenu 재사용
…into WTH-241-게시글-작성-수정-삭제-API-연결
📝 WalkthroughWalkthrough보드 게시물의 작성·수정·삭제 워크플로우를 추가하고 에디터 초기화/레이아웃을 통합한 Changes
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
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: 성공 토스트
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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: 실패 |
🤖 Claude 테스트 제안
변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.
|
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
PR 검증 결과❌ TypeScript: 실패 |
🤖 Claude 테스트 제안
변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.
|
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
PR 검증 결과✅ TypeScript: 통과 🎉 모든 검증을 통과했습니다! |
|
구현한 기능 Preview: https://weeth-8k4l2vp27-weethsite-4975s-projects.vercel.app |
There was a problem hiding this comment.
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:onDeleteSelect와onDelete간의 타입 시그니처 불일치
ActionMenu의onDeleteSelect는 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:onDeleteSelect와onDelete간의 타입 시그니처 불일치
ReplyItem과 동일한 이슈입니다.ActionMenu의onDeleteSelect는(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)를 호출합니다.useDeletePost의submit은 에러를 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: 중복 조건 검사 간소화 가능
isEditPage가true이면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
📒 Files selected for processing (33)
src/app/(private)/(main)/board/(with-nav)/BoardNavClient.tsxsrc/app/(private)/(main)/board/(with-nav)/[id]/PostDetailContent.tsxsrc/app/(private)/(main)/board/edit/[id]/EditClientEditor.tsxsrc/app/(private)/(main)/board/edit/[id]/page.tsxsrc/app/(private)/(main)/board/write/ClientEditor.tsxsrc/app/globals.csssrc/components/board/ActionMenu.tsxsrc/components/board/BoardContent.tsxsrc/components/board/CategorySelector.tsxsrc/components/board/Comment/CommentItem.tsxsrc/components/board/Comment/ReplyItem.tsxsrc/components/board/Editor/index.tsxsrc/components/board/Editor/usePostEditor.tssrc/components/board/PostActionMenu.tsxsrc/components/board/PostCard/PostCardContent.tsxsrc/components/board/PostCard/index.tsxsrc/components/board/PostDeleteDialog.tsxsrc/components/board/PostDetailHeader.tsxsrc/components/board/PostEditorShell.tsxsrc/components/board/index.tssrc/components/home/HomeBoardContent.tsxsrc/components/layout/header/Header.tsxsrc/hooks/board/useCreatePost.tssrc/hooks/board/useDeletePost.tssrc/hooks/board/useFileAttach.tssrc/hooks/board/useUpdatePost.tssrc/hooks/index.tssrc/hooks/useFileUpload.tssrc/hooks/useNavigationGuard.tssrc/lib/actions/board.tssrc/lib/apis/board.server.tssrc/stores/usePostStore.tssrc/types/board.ts
| 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> | ||
| ); |
There was a problem hiding this comment.
존재하지 않는 게시글에 대한 에러 처리 부재
게시글 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.
| // 글쓰기 페이지 진입 시 store 초기화 후 현재 게시판으로 설정 | ||
| useEffect(() => { | ||
| if (board === null && defaultId !== null) { | ||
| setBoard(defaultId); | ||
| reset(); | ||
| if (activeBoardId !== null) { | ||
| setBoard(activeBoardId); | ||
| } | ||
| }, [board, defaultId, setBoard]); | ||
| }, []); |
There was a problem hiding this comment.
의도적인 마운트 전용 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.
| 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 }); |
There was a problem hiding this comment.
수정 모드에서 hasChanges 오탐 가능성
현재 로직은 title.length > 0 || content.length > 0 || files.length > 0으로 변경 여부를 판단합니다. 수정 페이지에서는 initFromDetail로 기존 데이터가 로드되어 처음부터 hasChanges가 true가 됩니다.
이로 인해 사용자가 아무 변경 없이 뒤로가기해도 경고 다이얼로그가 표시될 수 있습니다. 실제 변경 여부를 감지하려면 초기 상태와 비교하는 로직이 필요할 수 있습니다.
💡 해결 방안 예시
// 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.
| fileSize: selected.size, | ||
| contentType: selected.type, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
src/components/layout/header/Header.tsxsrc/hooks/board/useCreatePost.tssrc/hooks/board/useUpdatePost.tssrc/utils/shared/file.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/hooks/board/useCreatePost.ts
| const { | ||
| handleWriteClick, | ||
| handleSkipProfile, | ||
| cardinalModalOpen, | ||
| setCardinalModalOpen, | ||
| profileModalOpen, | ||
| setProfileModalOpen, | ||
| } = useWritePost(); |
There was a problem hiding this comment.
프로필 상태 로딩 중 첫 클릭이 잘못된 모달로 갈 수 있습니다.
useWritePost의 handleWriteClick는 profileStatus가 아직 없으면 !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.
| if (!content.trim()) { | ||
| toast({ title: '내용을 입력해주세요.', variant: 'error' }); | ||
| return; | ||
| } |
There was a problem hiding this comment.
🧩 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*:' srcRepository: Team-Weeth/weeth-client
Length of output: 6410
🏁 Script executed:
cat -n src/hooks/board/useUpdatePost.ts | head -60Repository: Team-Weeth/weeth-client
Length of output: 2230
🏁 Script executed:
fd -type f -name 'PostEditorShell*' srcRepository: Team-Weeth/weeth-client
Length of output: 237
🏁 Script executed:
rg -n 'getHTML|getText|setContent|content.*=' src/components/board/PostEditorShell.tsx -A 3 -B 1Repository: Team-Weeth/weeth-client
Length of output: 305
🏁 Script executed:
rg -n 'interface PostDetail|type PostDetail' src/typesRepository: Team-Weeth/weeth-client
Length of output: 135
🏁 Script executed:
cat -n src/components/board/PostEditorShell.tsxRepository: 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 tsxRepository: Team-Weeth/weeth-client
Length of output: 94
🏁 Script executed:
find src -name 'Editor.tsx' -o -name 'editor.tsx' | head -5Repository: 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 1Repository: Team-Weeth/weeth-client
Length of output: 2616
🏁 Script executed:
rg -n 'isEmpty|getText|getJSON' src/components/board/Editor/ -B 1 -A 1Repository: Team-Weeth/weeth-client
Length of output: 49
🏁 Script executed:
rg -n 'content.*trim|content.*length' src/hooks/ -B 2 -A 1Repository: 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 흐름을 유지하도록 수정하면 됩니다.
JIN921
left a comment
There was a problem hiding this comment.
나영님은 정말 부지런쟁이.... 작업 속도 짱짱...
| const submit = async (postId: number) => { | ||
| const { title, content, files, getPayload, reset } = usePostStore.getState(); | ||
|
|
||
| if (!clubId) { |
There was a problem hiding this comment.
요기 검증 로직 useCreatePost랑 중복되니까 유틸로 빼면 좋을 거 같애용
| import dynamic from 'next/dynamic'; | ||
| import { ActionMenu, type ActionMenuProps } from './ActionMenu'; | ||
|
|
||
| const PostDeleteDialog = dynamic(() => |
There was a problem hiding this comment.
요거 컴포넌트가 몇줄 안 되는데 코드 스플리팅,, 굳이 없어도 되지 않을까!! 싶습니당
| variant = 'list', | ||
| }: PostCardContentProps) { | ||
| const contentRef = useRef<HTMLParagraphElement>(null); | ||
| function PostCardTitle({ title, isNew, size }: PostCardTitleProps) { |
There was a problem hiding this comment.
안에 컴포넌트 function이랑 훅이 혼재하는데 유지 보수 편하게 분리하믄 어떨까요,,
| ro.observe(el); | ||
| return () => ro.disconnect(); | ||
| }, [content, expandable]); | ||
| }, [enabled, ...deps]); |
There was a problem hiding this comment.
요거 spread된 배열을 넣기 보다는 단일 값을 넘겨주는 게 좋을 거 같습니당
export function useLineClamp<T extends HTMLElement>(enabled: boolean, content: string) {
useEffect(() => {
// 생략
}, [enabled, content]);
}
요런 식으루요
호출부는 이렇게
const elementRef = useLineClamp<HTMLParagraphElement>(true, content);
이러면 원시값이 바뀔 때만 훅이 실행 되니까,,,
| const { submit: submitCreate, isPending: isCreating } = useCreatePost(); | ||
| const { submit: submitUpdate, isPending: isUpdating } = useUpdatePost(); | ||
| const { | ||
| handleWriteClick, | ||
| handleSkipProfile, | ||
| cardinalModalOpen, | ||
| setCardinalModalOpen, | ||
| profileModalOpen, | ||
| setProfileModalOpen, | ||
| } = useWritePost(); |
There was a problem hiding this comment.
헤더에 CRUD 로직까지 섞여도 괜찮을까.. 고민이 되네요.. 너무 역할이 많아지는 느낌.. 버튼 게시 영역을 컴포넌트로 빼면 어떨까요,,,
There was a problem hiding this comment.
흠... 저도 유진님과 동일한 생각입니닷... 버튼 게시 영역만 컴포넌트로 분리해도 좋을 것 같아요
| const initializedRef = useRef(false); | ||
| if (!initializedRef.current) { | ||
| initializedRef.current = true; | ||
| usePostStore.getState().initFromDetail(post); |
There was a problem hiding this comment.
렌더중 store 변경은 권장되지 않으니까,, seEffect + ref guard 패턴이 더 좋을 거 같습니다!
woneeeee
left a comment
There was a problem hiding this comment.
수고하셨습니다ㅜㅜ!!! 전반적으로 코드 주석이 조금 많은 느낌인데 나중에 기능 자체가 조금씩 변경되거나 수정되는 경우에는 주석도 함께 수정을 해줘야되다보니 최대한 함수명에 해당 기능을 자세히 설명하도록 하거나 중요한 주석을 제외한 부분은 삭제해도 좋을 것 같아욤!!
There was a problem hiding this comment.
해당 파일에서 PostCardTitle, useLineClamp, ExpandButton, PostCardListContent, PostCardDetailContent 얘네는 훅은 훅대로 컴포넌트는 컴포넌트로 분리해도 좋을 것 같습니다!!
| <Icon src={MoreVerticalIcon} size={16} className="text-icon-normal" /> | ||
| </Button> | ||
| </DropdownMenuTrigger> | ||
| </span> |
There was a problem hiding this comment.
이벤트 전파를 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 해당 컴ㅁ포넌트보다 상위에서 이벤트 전파를 막아줘야할까욤...?!!
| /** | ||
| * 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)} />; | ||
| } |
There was a problem hiding this comment.
다들 코드 스타일에 따라서 다를 것 같긴한데 저는 단일 책임 원칙에 따라서 최대한 하나의 함수는 단 하나의 책임만 가져야 한다고 최대한 파일도 함수에 따라서 분리를 해두는데 요 부분은 너무 짧아서 .... 굳이 파일 분리를 안해도 될라나요... 헤헷 모르겟다
| const { submit: submitCreate, isPending: isCreating } = useCreatePost(); | ||
| const { submit: submitUpdate, isPending: isUpdating } = useUpdatePost(); | ||
| const { | ||
| handleWriteClick, | ||
| handleSkipProfile, | ||
| cardinalModalOpen, | ||
| setCardinalModalOpen, | ||
| profileModalOpen, | ||
| setProfileModalOpen, | ||
| } = useWritePost(); |
There was a problem hiding this comment.
흠... 저도 유진님과 동일한 생각입니닷... 버튼 게시 영역만 컴포넌트로 분리해도 좋을 것 같아요
✅ PR 유형
어떤 변경 사항이 있었나요?
📌 관련 이슈번호
✅ Key Changes
게시글 작성/수정/삭제 API 연결
게시글 수정 페이지
게시글 작성/수정 공통 Shell
게시글 삭제 다이얼로그
네비게이션 개선
📸 스크린샷 or 실행영상
KakaoTalk_20260406_162330705.mp4
/board/write 와 /board/edit/[id[로 확인 가능합니다!
🎸 기타 사항 or 추가 코멘트
clubId는 추후 수정 예정공지 읽음 처리 + 좋아요 토글 + 댓글 작성/수정/삭제 관련은 다음 이슈에서 빠르게 구현해보겠습니다 . . . ㅠ.ㅠ
Summary by CodeRabbit
릴리스 노트
New Features
Improvements
UI