Skip to content

[Feature/#119] 팀원 초대 이메일 발송 API 연동 및 초대 대기 목록 반영 - #139

Merged
jjjsun merged 5 commits into
developfrom
feature/#119
Mar 26, 2026
Merged

[Feature/#119] 팀원 초대 이메일 발송 API 연동 및 초대 대기 목록 반영#139
jjjsun merged 5 commits into
developfrom
feature/#119

Conversation

@jjjsun

@jjjsun jjjsun commented Mar 25, 2026

Copy link
Copy Markdown
Collaborator

🚨 관련 이슈

Closed #119

✨ 변경사항

  • 🐞 BugFix Something isn't working
  • 💻 CrossBrowsing Browser compatibility
  • 🌏 Deploy Deploy
  • 🎨 Design Markup & styling
  • 📃 Docs Documentation writing and editing (README.md, etc.)
  • ✨ Feature Feature
  • 🔨 Refactor Code refactoring
  • ⚙️ Setting Development environment setup
  • ✅ Test Test related (storybook, jest, etc.)

✏️ 작업 내용

  • 팀원 초대 이메일 발송 API POST /api/org/members/${orgId}/invitation 연동
  • 기존 팀원 초대 모달 mock데이터 제거
  • 초대 이메일 발송시, 이메일이 가입 대기중(PENDING) 상태로 리스트에 즉시 반영되도록 구현
  • zod 기반 이메일 유효성 검증 적용

😅 미완성 작업

N/A

📢 논의 사항 및 참고 사항

  • 현재는 초대 대기 목록은 서버 데이터가 아니라 클라이언트 상태로 관리중입니다.
  • 추후에 초대 대기 멤버 조회 API가 추가되면 해당 로직을 서버 데이터 기반으로 변경하는것도 좋을 듯 합니다. (백엔드와 협의가 필요할것같습니다)

💬 리뷰어 가이드 (P-Rules)
P1: 필수 반영 (Critical) - 버그 가능성, 컨벤션 위반. 해결 전 머지 불가.
P2: 적극 권장 (Recommended) - 더 나은 대안 제시. 가급적 반영 권장.
P3: 제안 (Suggestion) - 아이디어 공유. 반영 여부는 드라이버 자율.
P4: 단순 확인/칭찬 (Nit) - 사소한 오타, 칭찬 등 피드백.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 워크스페이스 멤버 이메일 초대 기능 추가
    • 이메일 주소 유효성 검사 기능 추가
    • 대기 중인 초대 현황을 실시간으로 표시
  • 버그 수정

    • 멤버 권한 변경 및 삭제 기능이 서버와 동기화되도록 완성

@jjjsun
jjjsun requested review from Seojegyeong and YermIm March 25, 2026 20:18
@jjjsun jjjsun self-assigned this Mar 25, 2026
@jjjsun jjjsun added ✨ Feature 기능 개발 📬 API 서버 API 통신 labels Mar 25, 2026
@coderabbitai

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

조직 멤버 관리 기능을 구현했습니다. 이메일 초대 API를 추가하고, 멤버 권한 변경 및 삭제 기능을 React Query 뮤테이션으로 연동했습니다. 클라이언트 측 이메일 유효성 검사와 대기 중인 초대 상태를 관리합니다.

Changes

Cohort / File(s) Summary
API 클라이언트 확장
src/api/workspace/org.ts
멤버 관리 API 3개 함수 추가: updateWorkspaceMemberPermission (권한 변경), deleteWorkspaceMember (멤버 삭제), postInviteEmail (이메일 초대).
이메일 초대 UI 통합
src/components/workspace/InviteMemberModal.tsx
React Query 뮤테이션으로 이메일 초대 API 호출, 클라이언트 측 이메일 유효성 검사(emailSchema.safeParse), 로딩 상태 처리 및 쿼리 캐시 무효화 추가.
멤버 목록 상태 관리
src/components/workspace/MemberList.tsx
하드코딩된 목데이터 제거, 대기 중인 초대 목록 상태 추가, 초대 성공 콜백으로 동적 초대 항목 관리.
멤버 관리 뮤테이션 구현
src/pages/workspace/MemberManagement.tsx
권한 변경 및 멤버 삭제 작업을 React Query 뮤테이션으로 구현, 성공/실패 토스트 메시지 처리, 캐시 무효화 처리.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • Seojegyeong
  • YermIm
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항인 팀원 초대 이메일 발송 API 연동과 초대 대기 목록 반영을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 템플릿을 따르고 있으며, 관련 이슈, 변경사항 체크, 작업 내용, 미완성 작업, 참고 사항을 모두 포함하고 있습니다.
Linked Issues check ✅ Passed PR의 모든 변경사항이 이슈 #119의 체크리스트 항목들을 완전히 충족하고 있습니다. API 타입 정리, 요청 구현, 모달 연동, 이메일 검증, 사용자 피드백, 로딩 상태 처리가 모두 구현되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 이슈 #119의 범위 내에 있으며, 이메일 발송 API 연동과 초대 대기 목록 관리 관련 변경만 포함되어 있습니다.

✏️ 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 feature/#119

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

📚 Storybook 배포 완료

항목 링크
📖 Storybook https://69a147b60a56365d9e2185ef-qekmazkdrf.chromatic.com/
🔍 Chromatic https://www.chromatic.com/build?appId=69a147b60a56365d9e2185ef&number=151

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (7)
src/components/workspace/MemberList.tsx (1)

61-72: inviteItems 메모이제이션 권장

inviteItems 배열이 매 렌더마다 재생성됩니다. memberspendingInviteItems가 변경될 때만 재계산되도록 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 컴포넌트 내부에서 isLoading prop으로 닫기를 방지하는 것이 더 명확한 패턴입니다.

현재 구현도 문제없이 동작하므로 참고만 해주세요.

🤖 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: 중복 에러 처리 정리 권장

inviteMutationonError에서 이미 에러 토스트를 표시하고 있으므로, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3e90ee3 and cf437a1.

📒 Files selected for processing (4)
  • src/api/workspace/org.ts
  • src/components/workspace/InviteMemberModal.tsx
  • src/components/workspace/MemberList.tsx
  • src/pages/workspace/MemberManagement.tsx

@Seojegyeong Seojegyeong left a comment

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.

P4: 확인했습니다!

@YermIm

YermIm commented Mar 26, 2026

Copy link
Copy Markdown
Collaborator

P4: 확인했습니다!

@jjjsun
jjjsun merged commit 771f57c into develop Mar 26, 2026
3 checks passed
@jjjsun
jjjsun deleted the feature/#119 branch March 26, 2026 04:23
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.

✨ [Feature] 초대 이메일 발송 API 연동

3 participants