[Feature/#119] 팀원 초대 이메일 발송 API 연동 및 초대 대기 목록 반영 - #139
Conversation
📝 WalkthroughWalkthrough조직 멤버 관리 기능을 구현했습니다. 이메일 초대 API를 추가하고, 멤버 권한 변경 및 삭제 기능을 React Query 뮤테이션으로 연동했습니다. 클라이언트 측 이메일 유효성 검사와 대기 중인 초대 상태를 관리합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as 관리자
participant Modal as InviteMemberModal
participant Validation as 유효성 검사
participant API as postInviteEmail API
participant Server as 서버
participant Cache as React Query 캐시
User->>Modal: 이메일 입력
Modal->>Validation: emailSchema.safeParse()
Validation-->>Modal: 유효성 검사 결과
alt 이메일 유효
User->>Modal: 초대 버튼 클릭
Modal->>API: postInviteEmail(orgId, {email})
API->>Server: POST /api/org/members/{orgId}/invitation
Server-->>API: TInviteMemberResponse
API-->>Modal: 초대 응답
Modal->>Cache: invalidate(workspaceMembers, workspaceMemberCount)
Modal->>User: 성공 토스트 + pendingInviteItems 추가
Modal->>Modal: 폼 초기화
else 이메일 무효
Modal->>User: 유효성 검사 오류 토스트
Modal->>Modal: 초대 버튼 비활성화
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
📚 Storybook 배포 완료
|
There was a problem hiding this comment.
🧹 Nitpick comments (7)
src/components/workspace/MemberList.tsx (1)
61-72: inviteItems 메모이제이션 권장
inviteItems배열이 매 렌더마다 재생성됩니다.members나pendingInviteItems가 변경될 때만 재계산되도록useMemo로 감싸는 것이 좋습니다.♻️ 개선 제안
- const inviteItems: TInviteMemberItem[] = [ - ...pendingInviteItems, - ...members.map((member) => ({ - memberId: member.memberId, - name: member.name, - email: member.email, - profileImageUrl: member.profileImageUrl, - role: member.role, - inviteStatus: "ACTIVE" as const, - isMe: member.isMe, - })), - ]; + const inviteItems: TInviteMemberItem[] = useMemo( + () => [ + ...pendingInviteItems, + ...members.map((member) => ({ + memberId: member.memberId, + name: member.name, + email: member.email, + profileImageUrl: member.profileImageUrl, + role: member.role, + inviteStatus: "ACTIVE" as const, + isMe: member.isMe, + })), + ], + [pendingInviteItems, members], + );
useMemo를 import에 추가하는 것도 잊지 마세요:-import { type RefObject, useState } from "react"; +import { type RefObject, useMemo, useState } from "react";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/workspace/MemberList.tsx` around lines 61 - 72, Wrap the inviteItems array creation in React's useMemo so it is only recomputed when members or pendingInviteItems change: import useMemo from React, replace the current inviteItems assignment with a useMemo callback that returns the combined array and set its dependency array to [members, pendingInviteItems]; keep the same shape and types (TInviteMemberItem, inviteStatus, isMe) inside the memoized callback.src/pages/workspace/MemberManagement.tsx (3)
201-214: TODO 항목 확인: 관리자 변경 API 연동 필요관리자 변경(소유권 이전) 기능에 TODO 주석이 남아있습니다. 현재 API 연동 없이 성공 토스트만 표시되는 상태입니다. 향후 구현 예정이라면 이슈로 추적하는 것을 권장합니다.
해당 기능 구현을 위한 이슈를 생성해 드릴까요?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/workspace/MemberManagement.tsx` around lines 201 - 214, handleTransferOwnership currently only shows a success toast and has a TODO for the actual ownership-transfer API; replace the placeholder with a call to your backend transfer endpoint (invoke the workspace ownership transfer API inside handleTransferOwnership, using the workspace/member identifiers from the TWorkspaceMember argument), await the response, and only show toast.success, close modal via setIsTransferModalOpen(false), navigate("/workspace") and update UI state (setChanging) after verifying a successful response; on failure catch and toast.error and log the error (preserve the existing catch block behavior) and if you intentionally defer implementation, create and link an issue before removing the TODO.
152-186: async 핸들러 메모이제이션 검토
handleRoleChange가 async 함수로 정의되어 있고MemberList에 props로 전달됩니다. 현재 구현은 정상 동작하지만, 매 렌더마다 새 함수가 생성되어MemberList와 하위 컴포넌트의 불필요한 리렌더링을 유발할 수 있습니다.성능이 중요한 경우
useCallback으로 메모이제이션을 고려해보세요. 다만 현재 규모에서는 큰 영향이 없을 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/workspace/MemberManagement.tsx` around lines 152 - 186, The handler handleRoleChange is recreated every render and can cause unnecessary re-renders when passed to MemberList; wrap it in React.useCallback to memoize it, e.g. replace the current function declaration with a useCallback-wrapped version and return the same async function body; include correct dependencies such as members, adminCount, updateMemberRoleMutation, and toast (and any other referenced values) so the callback updates when those change; keep the function name handleRoleChange and its parameter signature unchanged so existing usages (MemberList props) continue to work.
122-150: useEffect 의존성 배열 개선 권장
membersQuery.fetchNextPage는 매 렌더마다 새로운 함수 참조가 생성될 수 있어 불필요한 observer 재설정이 발생할 수 있습니다.membersQuery객체 자체는 안정적이므로, 의존성에서fetchNextPage를 제거하고 callback 내부에서 직접 참조하는 방식을 고려해보세요.♻️ 개선 제안
useEffect(() => { const target = observerRef.current; if (!target) return; if (!membersQuery.hasNextPage) return; const observer = new IntersectionObserver( (entries) => { const firstEntry = entries[0]; if ( firstEntry?.isIntersecting && membersQuery.hasNextPage && !membersQuery.isFetchingNextPage ) { void membersQuery.fetchNextPage(); } }, { root: null, rootMargin: "120px", threshold: 0, }, ); observer.observe(target); return () => observer.disconnect(); - }, [ - membersQuery.hasNextPage, - membersQuery.isFetchingNextPage, - membersQuery.fetchNextPage, - ]); + }, [membersQuery]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/workspace/MemberManagement.tsx` around lines 122 - 150, The effect currently includes membersQuery.fetchNextPage in its dependency array causing unnecessary observer resets because fetchNextPage may be a new function each render; update the useEffect to remove membersQuery.fetchNextPage from the deps and only depend on stable membersQuery properties (membersQuery.hasNextPage and membersQuery.isFetchingNextPage or the whole membersQuery object), and inside the IntersectionObserver callback call membersQuery.fetchNextPage() directly (e.g., reference membersQuery.fetchNextPage within the callback) so the observer is not recreated on every render; keep observerRef and the observer.disconnect cleanup as-is.src/components/workspace/InviteMemberModal.tsx (3)
144-201: 리스트 key 사용 확인
item.email을 key로 사용하고 있습니다. 일반적으로 email은 고유하지만, 동일 이메일이 pending과 active 상태로 동시에 존재할 가능성이 있다면${item.inviteStatus}-${item.email}형태로 복합 key를 사용하는 것이 안전합니다.MemberList.tsx에서 중복 방지 로직이 있으므로 현재 구현도 괜찮습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/workspace/InviteMemberModal.tsx` around lines 144 - 201, The list key uses item.email in the inviteItems.map render which can collide if the same email appears with different inviteStatus; update the key on the <li> in InviteMemberModal (inside inviteItems.map) to include inviteStatus (e.g. combine item.inviteStatus and item.email) so keys are unique across pending/active entries and React reconciliation remains stable.
97-105: 모달 닫기 방지 패턴 개선 고려
onClose={inviteMutation.isPending ? () => {} : onClose}패턴이 동작하긴 하지만, 빈 함수를 전달하는 것보다Modal컴포넌트 내부에서isLoadingprop으로 닫기를 방지하는 것이 더 명확한 패턴입니다.현재 구현도 문제없이 동작하므로 참고만 해주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/workspace/InviteMemberModal.tsx` around lines 97 - 105, Replace the current onClose guard in InviteMemberModal.tsx (onClose={inviteMutation.isPending ? () => {} : onClose}) by passing the loading state into the Modal component instead: keep onClose={onClose} and add an isLoading (or similar prop the Modal supports) with inviteMutation.isPending so the Modal itself disables closing while inviteMutation is pending; update any Modal prop name to match the component API and remove the empty function sentinel.
81-96: 중복 에러 처리 정리 권장
inviteMutation의onError에서 이미 에러 토스트를 표시하고 있으므로,handleInvite의 try-catch에서 별도로 처리할 필요가 없습니다. 현재는console.error만 있어 문제는 없지만, 에러 처리 흐름을 명확히 하기 위해 정리하면 좋겠습니다.♻️ 개선 제안
mutateAsync를 사용하면서 try-catch로 감싸면 에러가 양쪽에서 처리됩니다.mutate를 사용하거나,onError콜백만 사용하는 방식으로 통일하는 것을 권장합니다.const handleInvite = async () => { if (!emailValidation.success) { toast.error( emailValidation.error.issues[0]?.message ?? "올바른 이메일을 입력해주세요", ); return; } - try { - await inviteMutation.mutateAsync({ - email: emailValidation.data, - }); - } catch (error) { - console.error("초대 실패", error); - } + inviteMutation.mutate({ + email: emailValidation.data, + }); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/workspace/InviteMemberModal.tsx` around lines 81 - 96, Summary: Duplicate error handling occurs because inviteMutation.onError already shows an error toast while handleInvite wraps inviteMutation.mutateAsync in a try-catch; unify by removing the local catch. Fix: in handleInvite (the function name), remove the try-catch and call inviteMutation.mutate({ email: emailValidation.data }) instead of await inviteMutation.mutateAsync(...) so errors are handled by inviteMutation.onError; alternatively, if you prefer async/await keep mutateAsync but then remove the onError toast to avoid double notifications—adjust inviteMutation's onError or handleInvite accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/components/workspace/InviteMemberModal.tsx`:
- Around line 144-201: The list key uses item.email in the inviteItems.map
render which can collide if the same email appears with different inviteStatus;
update the key on the <li> in InviteMemberModal (inside inviteItems.map) to
include inviteStatus (e.g. combine item.inviteStatus and item.email) so keys are
unique across pending/active entries and React reconciliation remains stable.
- Around line 97-105: Replace the current onClose guard in InviteMemberModal.tsx
(onClose={inviteMutation.isPending ? () => {} : onClose}) by passing the loading
state into the Modal component instead: keep onClose={onClose} and add an
isLoading (or similar prop the Modal supports) with inviteMutation.isPending so
the Modal itself disables closing while inviteMutation is pending; update any
Modal prop name to match the component API and remove the empty function
sentinel.
- Around line 81-96: Summary: Duplicate error handling occurs because
inviteMutation.onError already shows an error toast while handleInvite wraps
inviteMutation.mutateAsync in a try-catch; unify by removing the local catch.
Fix: in handleInvite (the function name), remove the try-catch and call
inviteMutation.mutate({ email: emailValidation.data }) instead of await
inviteMutation.mutateAsync(...) so errors are handled by inviteMutation.onError;
alternatively, if you prefer async/await keep mutateAsync but then remove the
onError toast to avoid double notifications—adjust inviteMutation's onError or
handleInvite accordingly.
In `@src/components/workspace/MemberList.tsx`:
- Around line 61-72: Wrap the inviteItems array creation in React's useMemo so
it is only recomputed when members or pendingInviteItems change: import useMemo
from React, replace the current inviteItems assignment with a useMemo callback
that returns the combined array and set its dependency array to [members,
pendingInviteItems]; keep the same shape and types (TInviteMemberItem,
inviteStatus, isMe) inside the memoized callback.
In `@src/pages/workspace/MemberManagement.tsx`:
- Around line 201-214: handleTransferOwnership currently only shows a success
toast and has a TODO for the actual ownership-transfer API; replace the
placeholder with a call to your backend transfer endpoint (invoke the workspace
ownership transfer API inside handleTransferOwnership, using the
workspace/member identifiers from the TWorkspaceMember argument), await the
response, and only show toast.success, close modal via
setIsTransferModalOpen(false), navigate("/workspace") and update UI state
(setChanging) after verifying a successful response; on failure catch and
toast.error and log the error (preserve the existing catch block behavior) and
if you intentionally defer implementation, create and link an issue before
removing the TODO.
- Around line 152-186: The handler handleRoleChange is recreated every render
and can cause unnecessary re-renders when passed to MemberList; wrap it in
React.useCallback to memoize it, e.g. replace the current function declaration
with a useCallback-wrapped version and return the same async function body;
include correct dependencies such as members, adminCount,
updateMemberRoleMutation, and toast (and any other referenced values) so the
callback updates when those change; keep the function name handleRoleChange and
its parameter signature unchanged so existing usages (MemberList props) continue
to work.
- Around line 122-150: The effect currently includes membersQuery.fetchNextPage
in its dependency array causing unnecessary observer resets because
fetchNextPage may be a new function each render; update the useEffect to remove
membersQuery.fetchNextPage from the deps and only depend on stable membersQuery
properties (membersQuery.hasNextPage and membersQuery.isFetchingNextPage or the
whole membersQuery object), and inside the IntersectionObserver callback call
membersQuery.fetchNextPage() directly (e.g., reference
membersQuery.fetchNextPage within the callback) so the observer is not recreated
on every render; keep observerRef and the observer.disconnect cleanup as-is.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: d23e3bfe-f492-434e-92c6-048347557f69
📒 Files selected for processing (4)
src/api/workspace/org.tssrc/components/workspace/InviteMemberModal.tsxsrc/components/workspace/MemberList.tsxsrc/pages/workspace/MemberManagement.tsx
|
P4: 확인했습니다! |
🚨 관련 이슈
Closed #119
✨ 변경사항
✏️ 작업 내용
/api/org/members/${orgId}/invitation연동😅 미완성 작업
N/A
📢 논의 사항 및 참고 사항
Summary by CodeRabbit
릴리스 노트
새로운 기능
버그 수정