Skip to content

[Refactor/#110] 멤버 관리 페이지 구조 및 UI 리팩토링 - #116

Merged
jjjsun merged 17 commits into
developfrom
refactor/#110
Mar 21, 2026
Merged

[Refactor/#110] 멤버 관리 페이지 구조 및 UI 리팩토링#116
jjjsun merged 17 commits into
developfrom
refactor/#110

Conversation

@jjjsun

@jjjsun jjjsun commented Mar 20, 2026

Copy link
Copy Markdown
Collaborator

🚨 관련 이슈

Closed #110

✨ 변경사항

  • 🐞 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.)

✏️ 작업 내용

  • 아이콘 경로 에러 수정
  • 아이콘 네이밍 수정
  • 아이콘 스타일 설정
  • 멤버 초대 버튼 에러 수정
  • 직전 PR 코드래빗 수정사항 반영
  • 멤버 관리 페이지 전체적인 상태 관리 구조를 정리하고 관련 컴포넌트 역할 분리
  • 멤버 관리 관련 타입 TWorkspaceMember로 통일해서 중복타입 제거
  • MemberManageMent를 기준으로 멤버 목록, 역할 변경, 관리자 변경, 멤버 삭제 흐름을 같은 멤버 상태 참조하도록 리팩토링 진행
  • 팀원 삭제 확인 모달 추가
  • 관리자 변경 flow 정리, 본인 제외하고 관리자가 없으면 TransferOwnershipBlockedModal 나오도록 수정
  • 관리자 변경 모달에서 사용하는 검색 select를 공용 SearchSelect 기반으로 분리할 수 있도록 구조 정리하고, 멤버 검색 select의 재사용성을 높임
  • 권한 설정 영역에서 토글 즉시 반영 방식 대신, 변경 후 하단의 변경사항 저장하기 버튼 추가하여, 중요도가 높은 작업인 권한 변경작업의 변경하기 쉬운 토글의 위험성 해결
  • MemberRoleSelect 스타일 수정해서 시각적으로 구분이 명확하도록 수정
  • 관리자 변경 모달 내 멤버 리스트 UI 상하 여백 조정하여 선택 영역의 가독성과 사용성 개선
  • 멤버 관련 처리 기준으로 기존의 email에서 memberId 중심으로 정리해서 식별 안정성 높임
멤버관리 리팩토링 image image image image image

😅 미완성 작업

  • API 연동 작업

📢 논의 사항 및 참고 사항

  • 현재 화면 test를 위해mock데이터 만들어서 사용중입니다. API연동시에는 삭제 예정입니다.

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

Summary by CodeRabbit

  • 새로운 기능

    • 팀원 검색·선택 컴포넌트(SearchSelect) 추가
    • 팀원 삭제 확인 모달 추가
  • 버그 수정

    • 초대 실패 시 오류 토스트 표시 및 실패 처리 개선
    • 마지막 관리자의 실수 강등 차단 로직 추가
  • 개선 사항

    • 소유권 이전·관리자 전환 흐름 안정화
    • 권한 변경에 대한 저장/취소 흐름 추가
    • 리스트·아이콘·버튼 UI 및 접근성 개선

@jjjsun
jjjsun requested review from Seojegyeong and YermIm March 20, 2026 09:45
@jjjsun jjjsun self-assigned this Mar 20, 2026
@jjjsun jjjsun added 🎨 Html&css 마크업 & 스타일링 ✨ Feature 기능 개발 🔨 Refactor 코드 리팩토링 labels Mar 20, 2026
@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e35d4121-34fe-4300-affc-9f48d77b24c5

📥 Commits

Reviewing files that changed from the base of the PR and between c6365c9 and 8b412d9.

📒 Files selected for processing (1)
  • src/components/workspace/MemberRoleSelect.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/workspace/MemberRoleSelect.tsx

📝 Walkthrough

Walkthrough

멤버 관리 리팩토링: 제네릭 검색/선택 컴포넌트(SearchSelect) 추가, 멤버 타입을 TWorkspaceMember로 통일, 페이지 수준 상태 관리로 이동, 멤버 삭제/소유권 이전/권한 변경의 비동기 플로우 및 검증 로직(자기 삭제 방지·마지막 관리자 방지) 도입.

Changes

Cohort / File(s) Summary
제네릭 검색 선택 컴포넌트
src/components/common/select/SearchSelect.tsx
제네릭 SearchSelect<T> 추가: 입력 기반 필터링(소문자 부분일치), selectedOption 동기화, 외부 클릭 닫기, 포커스 시 오픈, 옵션 렌더/선택 및 빈 상태 처리.
드롭다운 트리거 확장
src/components/common/dropdownmenu/DropdownMenu.tsx
trigger prop 타입 확장(ReactNode → `ReactNode
멤버 삭제 흐름 및 모달
src/components/workspace/DeleteMemberModal.tsx, src/components/workspace/MemberItem.tsx, src/components/workspace/MemberList.tsx, src/pages/workspace/MemberManagement.tsx
삭제 확인 모달 추가, MemberItem에서 삭제 버튼 콜백화, MemberList가 외부 members·onRoleChange·onDeleteClick 사용으로 변경. MemberManagement에 삭제/삭제 모달/비동기 삭제 처리 및 검증 로직(자기 삭제 방지·마지막 관리자 방지) 추가.
검색 위임 및 타입 통일
src/components/workspace/MemberSearchSelect.tsx, src/components/workspace/TransferOwnershipModal.tsx
MemberSearchSelect가 내부 검색/클로즈 로직을 제거하고 SearchSelect로 위임. 컴포넌트 및 콜백 타입 TTransferCandidateTWorkspaceMember로 통일.
역할/권한 관리 개선
src/components/workspace/MemberRoleSelect.tsx, src/components/workspace/PermissionTable.tsx
MemberRoleSelect가 DropdownMenu 사용으로 간소화(트리거 렌더 함수, 아이콘 회전/스타일 변경). PermissionTable은 saved/draft 분리, 변경 추적(hasChanges), 저장/취소 플로우 및 비동기 저장 시뮬레이션 추가.
초대 로직·아이콘·타입 정리
src/components/workspace/InviteMemberModal.tsx, src/components/workspace/TransferOwnershipBlockedModal.tsx, src/pages/workspace/WorkspaceSetting.tsx, src/types/workspace/workspace.ts
InviteMemberModal 비동기/예외 처리 개선, SVG 임포트를 React 컴포넌트로 변경, 경고 아이콘 사용 통일 및 스타일 조정. TWorkspaceMembermemberId 추가, TPermissionKey로 권한 키 재정의, TTransferCandidate 제거.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant SearchSelect as "SearchSelect"
    participant MemberSearchSelect as "MemberSearchSelect"
    participant TransferOwnershipModal as "TransferOwnershipModal"
    participant MemberManagement as "MemberManagement"

    User->>SearchSelect: 입력/포커스
    SearchSelect->>SearchSelect: keyword 업데이트 및 filteredOptions 계산
    SearchSelect-->>User: 필터된 옵션 목록 렌더
    User->>SearchSelect: 옵션 선택
    SearchSelect->>MemberSearchSelect: onSelect(member)
    MemberSearchSelect->>TransferOwnershipModal: selectedMember 갱신
    User->>TransferOwnershipModal: 소유권 이전 확인
    TransferOwnershipModal->>MemberManagement: onConfirm(selectedMember)
    MemberManagement->>MemberManagement: roles 상태 업데이트 (비동기, toast)
Loading
sequenceDiagram
    actor User
    participant MemberItem as "MemberItem"
    participant MemberManagement as "MemberManagement"
    participant DeleteMemberModal as "DeleteMemberModal"
    participant Backend as "Backend"

    User->>MemberItem: 삭제 버튼 클릭
    MemberItem->>MemberManagement: openDeleteMember(member)
    MemberManagement->>MemberManagement: 검증(자기 삭제/마지막 관리자 차단)
    MemberManagement->>DeleteMemberModal: 모달 오픈(selectedDeleteMember)
    User->>DeleteMemberModal: 삭제 확인 클릭
    DeleteMemberModal->>MemberManagement: onConfirm(member)
    MemberManagement->>Backend: DELETE /members/{memberId}
    Backend-->>MemberManagement: 성공/실패
    MemberManagement->>MemberManagement: members 상태 갱신 및 모달 종료
    MemberManagement-->>User: toast 알림
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • YermIm
  • Seojegyeong
🚥 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 제목이 멤버 관리 페이지의 구조 및 UI 리팩토링이라는 주요 변경사항을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 관련 이슈, 변경사항 체크리스트, 상세 작업 내용, 미완성 작업, 논의사항을 포함하여 템플릿을 잘 따르고 있습니다.
Linked Issues check ✅ Passed 변경사항이 #110 이슈의 모든 주요 요구사항을 충족합니다: 컴포넌트 구조 리팩토링, 아이콘 수정, 멤버 삭제 모달 구현, 권한 변경 UI 개선, TWorkspaceMember 통일 등.
Out of Scope Changes check ✅ Passed SearchSelect 컴포넌트 추가 및 DropdownMenu 트리거 prop 확장은 MemberSearchSelect 재사용성 향상을 위한 필요한 구조 개선으로 보입니다.

✏️ 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 refactor/#110

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.

Tip

You can get early access to new features in CodeRabbit.

Enable the early_access setting to enable early access features such as new models, tools, and more.

@github-actions

github-actions Bot commented Mar 20, 2026

Copy link
Copy Markdown

📚 Storybook 배포 완료

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

@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.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/pages/workspace/WorkspaceSetting.tsx (1)

77-79: ⚠️ Potential issue | 🟡 Minor

useEffect 의존성 배열에 fetchWorkspaceDetail이 누락되었습니다.

ESLint의 react-hooks/exhaustive-deps 규칙에 따르면 fetchWorkspaceDetail이 의존성 배열에 포함되어야 합니다. 현재 orgId만 포함되어 있어 린터 경고가 발생할 수 있습니다.

🐛 제안하는 수정

fetchWorkspaceDetailuseCallback으로 감싸거나, 함수를 useEffect 내부로 이동하세요:

+ const fetchWorkspaceDetail = useCallback(async () => {
+   // ... 기존 로직
+ }, [orgId]);

  useEffect(() => {
    void fetchWorkspaceDetail();
- }, [orgId]);
+ }, [fetchWorkspaceDetail]);

또는 함수를 useEffect 내부에 정의:

  useEffect(() => {
+   const fetchWorkspaceDetail = async () => {
+     // ... 기존 로직
+   };
    void fetchWorkspaceDetail();
  }, [orgId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/workspace/WorkspaceSetting.tsx` around lines 77 - 79, The useEffect
in WorkspaceSetting currently depends only on orgId while calling
fetchWorkspaceDetail, causing an exhaustive-deps lint warning; wrap
fetchWorkspaceDetail with useCallback (e.g., define fetchWorkspaceDetail via
useCallback inside the WorkspaceSetting component) or move the
fetchWorkspaceDetail definition inside the useEffect so that the dependency
array is complete, ensuring the effect lists fetchWorkspaceDetail and orgId (or
only orgId if the function is defined inside the effect) and eliminating the
lint error.
src/components/workspace/MemberRoleSelect.tsx (1)

64-76: ⚠️ Potential issue | 🟡 Minor

disabled 상태에서 opacity가 중복 적용됩니다.

Line 65와 Line 76 모두 disabled 상태일 때 opacity-50을 적용하고 있어, 실제로는 opacity가 0.25(0.5 × 0.5)로 렌더링될 수 있습니다.

🐛 제안하는 수정
      <div
-        className={`overflow-hidden rounded-[22px]  ${disabled ? "opacity-50" : ""}`}
+        className="overflow-hidden rounded-[22px]"
      >
        <button
          type="button"
          disabled={disabled}
          aria-expanded={isOpen}
          aria-haspopup="menu"
          aria-controls={isOpen && !disabled ? menuId : undefined}
          onClick={() => setIsOpen((prev) => !prev)}
          className={`flex h-10 min-w-25 items-center justify-between gap-3 rounded-[22px] px-4 font-body2 transition-colors ${
            triggerStyleMap[role]
          } ${disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer hover:brightness-95"}`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/workspace/MemberRoleSelect.tsx` around lines 64 - 76, The
wrapper div and the button both apply "opacity-50" when disabled, causing
double-opacity; remove the duplicated opacity class from one of them (prefer
keeping it only on the outer div or only on the button) so the disabled state
uses a single "opacity-50" application; update the JSX in MemberRoleSelect (the
outer div and the button that uses disabled, triggerStyleMap, aria-controls and
onClick/setIsOpen) to ensure only one element includes "opacity-50" while
preserving the disabled attribute, cursor-not-allowed class, and existing
triggerStyleMap usage.
🧹 Nitpick comments (7)
src/components/workspace/TransferOwnershipBlockedModal.tsx (1)

22-23: WarnIcon 크기가 다른 모달과 다릅니다.

다른 모달들(DeleteMemberModal, TransferOwnershipModal, WorkspaceSetting)에서는 WarnIconw-15 h-15 크기를 지정하고 있습니다. 일관성을 위해 동일한 크기를 적용하는 것을 권장합니다.

♻️ 제안하는 수정
-        icon={<WarnIcon className="text-status-red" aria-hidden="true" />}
+        icon={<WarnIcon className="text-status-red w-15 h-15" aria-hidden="true" />}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/workspace/TransferOwnershipBlockedModal.tsx` around lines 22 -
23, The WarnIcon used inside ModalContent in TransferOwnershipBlockedModal has a
different size than other modals; update the icon's className from just
"text-status-red" to include the standard sizing "w-15 h-15" (i.e., use
"text-status-red w-15 h-15") so it matches DeleteMemberModal,
TransferOwnershipModal, and WorkspaceSetting; modify the JSX where WarnIcon is
rendered in TransferOwnershipBlockedModal to include the additional classes.
src/components/workspace/PermissionTable.tsx (1)

11-13: 타입 정의가 중복됩니다.

TPermissionRow가 이미 defaultMemberEnabled: boolean을 포함하고 있고, admin/member 필드가 제거되었으므로 Omit 처리가 불필요합니다.

♻️ 제안하는 수정
-const permissionRows: Array<
-  Omit<TPermissionRow, "admin" | "member"> & { defaultMemberEnabled: boolean }
-> = [
+const permissionRows: TPermissionRow[] = [
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/workspace/PermissionTable.tsx` around lines 11 - 13, The type
annotation on permissionRows is redundant because TPermissionRow already
includes defaultMemberEnabled and admin/member were removed; replace the current
Omit-based type with the direct TPermissionRow type (e.g., use
Array<TPermissionRow> or TPermissionRow[]) or remove the explicit annotation to
let TypeScript infer it, and ensure references to permissionRows,
TPermissionRow, defaultMemberEnabled, admin, and member are updated accordingly.
src/components/workspace/MemberItem.tsx (1)

46-55: 역할 스타일이 MemberRoleSelect와 중복됩니다.

member.isMe일 때 적용되는 역할 스타일(Line 50-51)이 MemberRoleSelect.tsxtriggerStyleMap과 동일합니다. 한 곳에서 스타일이 변경되면 불일치가 발생할 수 있습니다.

♻️ 공통 스타일 추출 제안

triggerStyleMap을 별도 상수 파일로 분리하거나, MemberRoleSelect에서 export하여 재사용하는 것을 권장합니다:

// MemberRoleSelect.tsx에서 export
export const roleStyleMap: Record<TMemberRole, string> = {
  ADMIN: "bg-status-blue/80 text-white shadow-sm",
  MEMBER: "bg-chart-3/15 text-text-auth-sub",
};

// MemberItem.tsx에서 import하여 사용
import { roleStyleMap } from "./MemberRoleSelect";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/workspace/MemberItem.tsx` around lines 46 - 55, Extract the
role-to-class mapping into a single exported constant (e.g., roleStyleMap or
roleStyleMap: Record<TMemberRole,string>) and use it from MemberRoleSelect (or a
new shared constants file) in both components; in MemberRoleSelect keep using
the exported map for its triggerStyleMap logic and in MemberItem replace the
inline ternary inside the member.isMe span with roleStyleMap[member.role] and
use the same text ("관리자"/"멤버") logic, ensuring both components import the shared
roleStyleMap so styles stay consistent.
src/components/workspace/TransferOwnershipModal.tsx (1)

51-51: className에 불필요한 trailing space가 있습니다.

"px-2 py-6 " 끝에 공백이 포함되어 있습니다. 기능에 영향은 없지만 정리하면 좋겠습니다.

♻️ 제안하는 수정
-      <div className="text-center px-2 py-6 ">
+      <div className="text-center px-2 py-6">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/workspace/TransferOwnershipModal.tsx` at line 51, The
className string in TransferOwnershipModal's JSX div (the element with className
"text-center px-2 py-6 ") contains an unnecessary trailing space; edit the JSX
in TransferOwnershipModal.tsx to remove the trailing space so the className
becomes "text-center px-2 py-6" (update the div where className is set).
src/pages/workspace/WorkspaceSetting.tsx (1)

310-310: WarnIcon 크기가 모달 내부와 다릅니다.

Line 310에서는 w-12 h-12, Line 322-323에서는 w-15 h-15를 사용하고 있습니다. ControlBoxModal 컨텍스트 차이로 의도된 것일 수 있으나, 디자인 시스템 관점에서 일관성 검토를 권장합니다.

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

In `@src/pages/workspace/WorkspaceSetting.tsx` at line 310, The WarnIcon size is
inconsistent: it's rendered as w-12 h-12 in the leadingSlot (WarnIcon) but as
w-15 h-15 elsewhere inside the same modal/ControlBox context; update the
className on the WarnIcon in the leadingSlot (or the other occurrences) so all
instances use the same sizing token (e.g., change w-12 h-12 to w-15 h-15 or vice
versa) to match the design system, and ensure any surrounding components
(ControlBox / Modal) don't override sizing via CSS so the icon appears
consistently.
src/components/workspace/MemberRoleSelect.tsx (1)

83-101: 키보드 내비게이션 추가를 권장합니다.

현재 role="menu"role="menuitem"이 설정되어 있지만, 키보드 내비게이션(화살표 키, Enter, Escape)이 구현되어 있지 않습니다. 접근성 표준(WCAG)에서는 메뉴 컴포넌트의 키보드 조작을 권장합니다.

향후 개선 시 고려해 주세요.

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

In `@src/components/workspace/MemberRoleSelect.tsx` around lines 83 - 101, The
menu currently renders with role="menu" and role="menuitem" but lacks keyboard
navigation; update the MemberRoleSelect component to add keyboard handlers that,
when isOpen is true, trap focus and allow ArrowDown/ArrowUp to move a focused
index over restOptions, Enter/Space to call handleSelect(option) for the
highlighted item, and Escape to close the menu (mirroring whatever closes
isOpen). Ensure each rendered button uses a focusable/tabindex state (or set
aria-activedescendant/aria-selected) and that menuId and restOptions are used to
locate items; keep role attributes (role="menu"/"menuitem") and ensure focus is
set to the first/selected item when opening and returned to the trigger when
closing.
src/components/common/select/SearchSelect.tsx (1)

89-95: 공용 searchable select라면 combobox ARIA를 같이 연결해두는 게 좋겠습니다.

지금은 입력창과 팝업이 시각적으로만 연결되어 있어서, 보조기기에서는 드롭다운 열린 상태와 옵션 목록의 관계를 알기 어렵습니다. aria-expanded/aria-controls, role="listbox"/option 정도는 이 컴포넌트 레벨에서 붙여두는 편이 재사용 시 안전합니다.

As per coding guidelines, src/**: 접근성: 시맨틱 HTML, ARIA 속성 사용 확인.

Also applies to: 99-120

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

In `@src/components/common/select/SearchSelect.tsx` around lines 89 - 95, The
Input in SearchSelect.tsx (value={keyword}, onFocus={handleFocus}, onChange ->
handleChangeKeyword) needs ARIA combobox linking: add aria-expanded tied to the
component's open state (e.g., isOpen), aria-controls pointing to the popup/list
id, and role="combobox" on the input (or wrapper) as appropriate; ensure the
popup/list element has role="listbox" and each item has role="option" and an id
that matches aria-activedescendant when navigating. Update handleFocus/other
open/close logic to maintain the isOpen state used for aria-expanded and ensure
the generated list id is stable (e.g., derive from component id or a generated
uid) so the input's aria-controls correctly references the listbox.
🤖 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/common/select/SearchSelect.tsx`:
- Around line 41-43: The effect that resets keyword runs because getOptionLabel
is included as a dependency and is passed inline from MemberSearchSelect; change
the effect in useEffect(() => { setKeyword(selectedOption ?
getOptionLabel(selectedOption) : ""); }, [selectedOption, getOptionLabel]); to
synchronize only when selectedOption actually changes (remove getOptionLabel
from deps or compare previous selectedOption) so typing isn't reset on parent
re-renders — alternatively stabilize the parent's callback by wrapping
getOptionLabel in useCallback in MemberSearchSelect; additionally, add
accessibility attributes: wire the input to the dropdown with aria-expanded and
aria-controls on the input, give the dropdown container role="listbox" and each
item role="option" (and ensure options have appropriate aria-selected/ids) so
screen readers can navigate the custom select.

In `@src/components/workspace/InviteMemberModal.tsx`:
- Around line 50-61: handleInvite currently always shows success because there
is no await/API call; replace the placeholder with a real async request that
constructs a TInviteMemberRequest (using trimmedEmail), await the API call
inside handleInvite, only call toast.success and setForm({ email: "" }) after
the awaited call succeeds, and move toast.error and console.log("초대 실패", error)
into the catch to surface the actual error; ensure you reference handleInvite,
TInviteMemberRequest, setForm and toast so the request, response handling, and
error logging are implemented correctly.

In `@src/components/workspace/MemberList.tsx`:
- Around line 133-139: In MemberList.tsx change the JSX list key to use the
memberId identifier to match the callbacks: when mapping members (members.map)
pass key={member.memberId} to the MemberItem component (the map that renders
<MemberItem ... />) so the reconciliation key aligns with the
onRoleChange/onDeleteClick callbacks that use member.memberId, preventing stale
row reuse when emails change or duplicate emails exist.

In `@src/pages/workspace/MemberManagement.tsx`:
- Around line 108-116: handleRoleChange currently applies newRole blindly which
can break the admin invariant; before calling setMembers in handleRoleChange
compute current adminCount from members, locate the target member, and if the
target is an ADMIN and newRole !== 'ADMIN' and adminCount === 1, reject the
change (no-op or show error); additionally, if targetMemberId equals the current
user's id (e.g. currentUserId/sessionUserId) prevent demoting your own role to
non-ADMIN unless ownership transfer flow completed; only call setMembers(prev =>
prev.map(...)) after these checks so member.role updates cannot create
adminCount === 0 or allow self-demotion without transfer.
- Around line 88-90: The members state is initialized once with
useState(mockMembers) so it persists across workspaceId changes; update
MemberManagement to reset or reload members whenever workspaceId changes by
adding an effect that watches useParams()/workspaceId (orgId) and either clears
setMembers([]) or triggers the members fetch, and when using react-query include
workspaceId/orgId in the query key so cached data is per-workspace. Also revise
handleRoleChange to enforce the same "last admin" constraint as openDeleteMember
by checking current members for remaining admins before allowing a role change
that would remove the last admin, and surface a validation/error if the change
is forbidden.

---

Outside diff comments:
In `@src/components/workspace/MemberRoleSelect.tsx`:
- Around line 64-76: The wrapper div and the button both apply "opacity-50" when
disabled, causing double-opacity; remove the duplicated opacity class from one
of them (prefer keeping it only on the outer div or only on the button) so the
disabled state uses a single "opacity-50" application; update the JSX in
MemberRoleSelect (the outer div and the button that uses disabled,
triggerStyleMap, aria-controls and onClick/setIsOpen) to ensure only one element
includes "opacity-50" while preserving the disabled attribute,
cursor-not-allowed class, and existing triggerStyleMap usage.

In `@src/pages/workspace/WorkspaceSetting.tsx`:
- Around line 77-79: The useEffect in WorkspaceSetting currently depends only on
orgId while calling fetchWorkspaceDetail, causing an exhaustive-deps lint
warning; wrap fetchWorkspaceDetail with useCallback (e.g., define
fetchWorkspaceDetail via useCallback inside the WorkspaceSetting component) or
move the fetchWorkspaceDetail definition inside the useEffect so that the
dependency array is complete, ensuring the effect lists fetchWorkspaceDetail and
orgId (or only orgId if the function is defined inside the effect) and
eliminating the lint error.

---

Nitpick comments:
In `@src/components/common/select/SearchSelect.tsx`:
- Around line 89-95: The Input in SearchSelect.tsx (value={keyword},
onFocus={handleFocus}, onChange -> handleChangeKeyword) needs ARIA combobox
linking: add aria-expanded tied to the component's open state (e.g., isOpen),
aria-controls pointing to the popup/list id, and role="combobox" on the input
(or wrapper) as appropriate; ensure the popup/list element has role="listbox"
and each item has role="option" and an id that matches aria-activedescendant
when navigating. Update handleFocus/other open/close logic to maintain the
isOpen state used for aria-expanded and ensure the generated list id is stable
(e.g., derive from component id or a generated uid) so the input's aria-controls
correctly references the listbox.

In `@src/components/workspace/MemberItem.tsx`:
- Around line 46-55: Extract the role-to-class mapping into a single exported
constant (e.g., roleStyleMap or roleStyleMap: Record<TMemberRole,string>) and
use it from MemberRoleSelect (or a new shared constants file) in both
components; in MemberRoleSelect keep using the exported map for its
triggerStyleMap logic and in MemberItem replace the inline ternary inside the
member.isMe span with roleStyleMap[member.role] and use the same text
("관리자"/"멤버") logic, ensuring both components import the shared roleStyleMap so
styles stay consistent.

In `@src/components/workspace/MemberRoleSelect.tsx`:
- Around line 83-101: The menu currently renders with role="menu" and
role="menuitem" but lacks keyboard navigation; update the MemberRoleSelect
component to add keyboard handlers that, when isOpen is true, trap focus and
allow ArrowDown/ArrowUp to move a focused index over restOptions, Enter/Space to
call handleSelect(option) for the highlighted item, and Escape to close the menu
(mirroring whatever closes isOpen). Ensure each rendered button uses a
focusable/tabindex state (or set aria-activedescendant/aria-selected) and that
menuId and restOptions are used to locate items; keep role attributes
(role="menu"/"menuitem") and ensure focus is set to the first/selected item when
opening and returned to the trigger when closing.

In `@src/components/workspace/PermissionTable.tsx`:
- Around line 11-13: The type annotation on permissionRows is redundant because
TPermissionRow already includes defaultMemberEnabled and admin/member were
removed; replace the current Omit-based type with the direct TPermissionRow type
(e.g., use Array<TPermissionRow> or TPermissionRow[]) or remove the explicit
annotation to let TypeScript infer it, and ensure references to permissionRows,
TPermissionRow, defaultMemberEnabled, admin, and member are updated accordingly.

In `@src/components/workspace/TransferOwnershipBlockedModal.tsx`:
- Around line 22-23: The WarnIcon used inside ModalContent in
TransferOwnershipBlockedModal has a different size than other modals; update the
icon's className from just "text-status-red" to include the standard sizing
"w-15 h-15" (i.e., use "text-status-red w-15 h-15") so it matches
DeleteMemberModal, TransferOwnershipModal, and WorkspaceSetting; modify the JSX
where WarnIcon is rendered in TransferOwnershipBlockedModal to include the
additional classes.

In `@src/components/workspace/TransferOwnershipModal.tsx`:
- Line 51: The className string in TransferOwnershipModal's JSX div (the element
with className "text-center px-2 py-6 ") contains an unnecessary trailing space;
edit the JSX in TransferOwnershipModal.tsx to remove the trailing space so the
className becomes "text-center px-2 py-6" (update the div where className is
set).

In `@src/pages/workspace/WorkspaceSetting.tsx`:
- Line 310: The WarnIcon size is inconsistent: it's rendered as w-12 h-12 in the
leadingSlot (WarnIcon) but as w-15 h-15 elsewhere inside the same
modal/ControlBox context; update the className on the WarnIcon in the
leadingSlot (or the other occurrences) so all instances use the same sizing
token (e.g., change w-12 h-12 to w-15 h-15 or vice versa) to match the design
system, and ensure any surrounding components (ControlBox / Modal) don't
override sizing via CSS so the icon appears consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 082c6f37-3bd4-4145-a8ab-e8c19de1381c

📥 Commits

Reviewing files that changed from the base of the PR and between dd200a8 and 43d1f02.

⛔ Files ignored due to path filters (1)
  • src/assets/icon/common/mail.svg is excluded by !**/*.svg and included by src/**
📒 Files selected for processing (13)
  • src/components/common/select/SearchSelect.tsx
  • src/components/workspace/DeleteMemberModal.tsx
  • src/components/workspace/InviteMemberModal.tsx
  • src/components/workspace/MemberItem.tsx
  • src/components/workspace/MemberList.tsx
  • src/components/workspace/MemberRoleSelect.tsx
  • src/components/workspace/MemberSearchSelect.tsx
  • src/components/workspace/PermissionTable.tsx
  • src/components/workspace/TransferOwnershipBlockedModal.tsx
  • src/components/workspace/TransferOwnershipModal.tsx
  • src/pages/workspace/MemberManagement.tsx
  • src/pages/workspace/WorkspaceSetting.tsx
  • src/types/workspace/workspace.ts

Comment thread src/components/common/select/SearchSelect.tsx Outdated
Comment thread src/components/workspace/InviteMemberModal.tsx
Comment thread src/components/workspace/MemberList.tsx
Comment thread src/pages/workspace/MemberManagement.tsx Outdated
Comment thread src/pages/workspace/MemberManagement.tsx
Comment thread src/pages/workspace/MemberManagement.tsx
Comment thread src/components/workspace/PermissionTable.tsx Outdated
@Seojegyeong

Seojegyeong commented Mar 21, 2026

Copy link
Copy Markdown
Collaborator

P4: 관리자, 멤버 선택 부분이 다른 드롭다운 디자인과 통일성이 없어보이는 것 같습니다. 드롭다운의 기본 형태는 유지하고 그 안에서 디자인을 수정하는게 좋을 것 같습니다!
image
(직책 같은 title은 제거하고)

@YermIm

YermIm commented Mar 21, 2026

Copy link
Copy Markdown
Collaborator

P4: 저도 제경님이 말씀하신 것처럼 드롭다운 형태 가져가는 것이 좋을 것 같습니다!

@YermIm

YermIm commented Mar 21, 2026

Copy link
Copy Markdown
Collaborator

P3: 팀 구성원에서 멤버/관리자 변경하는 것은 즉시 반영되는 것인가요?

@jjjsun

jjjsun commented Mar 21, 2026

Copy link
Copy Markdown
Collaborator Author

P3: 팀 구성원에서 멤버/관리자 변경하는 것은 즉시 반영되는 것인가요?

네 역할 변경은 클릭을 두번해야하는 방식이라 한번 클릭으로 적용되는 토글보다 위험성이 적어서 즉시 반영으로 넣었습니다

@jjjsun

jjjsun commented Mar 21, 2026

Copy link
Copy Markdown
Collaborator Author
image

역할설정 DropdownMenu 컴포넌트 사용으로 변경했습니다!

@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.

Actionable comments posted: 1

🤖 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/workspace/MemberRoleSelect.tsx`:
- Around line 31-42: The menu remains interactive when the component is disabled
because only the trigger click is blocked; update MemberRoleSelect to close the
menu and block item interactions when disabled is true: ensure the menu's open
state (or menu controller) is set to closed whenever disabled becomes true
(e.g., in a useEffect watching disabled) and guard the items' onClick handlers
(the TMenuItem entries that call onChange("ADMIN") / onChange("MEMBER")) by
checking disabled before invoking onChange, or remove/disable those items when
disabled, so clicks cannot call onChange while disabled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7b580ca8-157d-44ec-a7f1-6c5ae4f99e14

📥 Commits

Reviewing files that changed from the base of the PR and between e0bd5bd and c6365c9.

📒 Files selected for processing (3)
  • src/components/common/dropdownmenu/DropdownMenu.tsx
  • src/components/workspace/MemberRoleSelect.tsx
  • src/components/workspace/PermissionTable.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/workspace/PermissionTable.tsx

Comment thread src/components/workspace/MemberRoleSelect.tsx Outdated
@jjjsun
jjjsun merged commit 6e2056f into develop Mar 21, 2026
3 checks passed
@jjjsun
jjjsun deleted the refactor/#110 branch March 21, 2026 08:13
@jjjsun jjjsun added the 🐞 BugFix Something isn't working label Mar 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 BugFix Something isn't working ✨ Feature 기능 개발 🎨 Html&css 마크업 & 스타일링 🔨 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

♻️ [Refactor] 멤버 목록 UI 리팩토링 및 디자인 디테일 수정

3 participants