Skip to content

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

Open
jjjsun wants to merge 14 commits intodevelopfrom
refactor/#110
Open

[Refactor/#110] 멤버 관리 페이지 구조 및 UI 리팩토링#116
jjjsun wants to merge 14 commits intodevelopfrom
refactor/#110

Conversation

@jjjsun
Copy link
Collaborator

@jjjsun jjjsun commented Mar 20, 2026

🚨 관련 이슈

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

  • 새로운 기능

    • 팀원 삭제 기능 추가(삭제 확인 모달)
    • 소유권 이전 및 관리자 역할 전환 흐름 개선
  • 버그 수정

    • 초대 실패 시 오류 토스트 표시 및 실패 처리 개선
    • 마지막 관리자 강제 유지(실수로 모두 강등되는 문제 방지)
  • 개선 사항

    • 팀원 검색·선택 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
Copy link

coderabbitai bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

멤버 관리 리팩토링: 제네릭 검색/선택 컴포넌트(SearchSelect) 추가, 멤버 타입 통일(TWorkspaceMember), 페이지 수준 상태 관리로 이동, 멤버 삭제 플로우와 권한 변경(임시저장/저장/취소) 기능이 도입되었습니다.

Changes

Cohort / File(s) Summary
제네릭 검색 선택 컴포넌트
src/components/common/select/SearchSelect.tsx
제네릭 SearchSelect<T> 추가: 입력 기반 필터링, 외부 클릭 닫기, 포커스/오픈 제어, 옵션 렌더링/선택 로직 포함.
멤버 삭제 흐름
src/components/workspace/DeleteMemberModal.tsx, src/components/workspace/MemberItem.tsx, src/components/workspace/MemberList.tsx, src/pages/workspace/MemberManagement.tsx
삭제 확인 모달 추가, 삭제 버튼 콜백화, 페이지 수준에서 삭제 상태/비동기 처리 및 검증(자기 삭제 방지, 마지막 관리자 방지) 구현.
검색/전달 컴포넌트 통합
src/components/workspace/MemberSearchSelect.tsx, src/components/workspace/TransferOwnershipModal.tsx
기존 MemberSearchSelect의 로컬 검색 로직 제거 후 SearchSelect로 위임. 타입을 TTransferCandidateTWorkspaceMember로 통일.
권한/역할 관리 개선
src/components/workspace/MemberRoleSelect.tsx, src/components/workspace/PermissionTable.tsx
역할 선택 드롭다운 스타일 조정, 권한 테이블에 draft/saved 상태 분리, 변경 추적(hasChanges), 저장/취소 플로우 및 비동기 시뮬레이션 추가.
초대 및 아이콘/타입 정리
src/components/workspace/InviteMemberModal.tsx, src/components/workspace/TransferOwnershipBlockedModal.tsx, src/pages/workspace/WorkspaceSetting.tsx, src/types/workspace/workspace.ts
Invite 비동기화 및 에러 핸들링 개선, SVG import를 React 컴포넌트로 전환, TWorkspaceMembermemberId 추가, TPermissionKey로 권한 키 재정의, TTransferCandidate 제거.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant SearchSelect
    participant MemberSearchSelect
    participant TransferOwnershipModal
    participant 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
    participant MemberManagement
    participant DeleteMemberModal
    participant 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

검토 관점 (간단 체크리스트)

  • 구조/상태 관리

    • MemberManagement에서 members·deleting·isDeleteModalOpen·selectedDeleteMember 동기화가 안전한가? 특히 모달 중복 열림/닫힘과 deleting 플래그 경합 여부 확인.
    • 역할 변경(handleRoleChange)에서 마지막 관리자 방지 로직이 race condition에 취약하지 않은지(동시 요청 가정) 검토.
  • SearchSelect

    • 외부 클릭(mousedown) 리스너 등록/해제 로직과 포커스 기반 open/close 호출의 중복 호출·버블링 이슈 검증.
    • getSearchText/trim/lowercase 매칭이 국제화/유니코드 문자열에서도 의도대로 동작하는지 확인(예: 조합문자).
  • 접근성 및 마크업

    • 옵션 버튼의 aria-selected, input aria 속성(placeholder, role 등) 일관성 및 스크린리더 시나리오 검토.
    • 모달 내 경고 아이콘에 aria-hidden 처리와 포커스 이동(모달 오픈 시 포커스 트랩)이 적절한지 확인.
  • 안정성/타입

    • TWorkspaceMember.memberId가 필수화되며 모든 호출 지점(모의 데이터, 테스트, 외부 API 매핑)에 값이 채워지는지 확인.
    • TPermissionKey 리터럴이 백엔드/DB의 키와 일치하는지 검증 필요.
  • 에러 처리/UX

    • 초대/삭제/소유권 이전 등 비동기 흐름에서 실패 시 상태 롤백/모달 상태, 버튼 disabled 상태가 사용자에게 명확한지 확인.
    • toast 메시지와 버튼 label(로딩 상태)이 일관된지 확인.

필요하면 특정 파일(예: SearchSelect의 이벤트 핸들러, MemberManagement의 handleDeleteMember/handleTransferOwnership) 코드를 지정해서 더 깊게 리뷰해줄게.

🚥 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 리팩토링을 명확하게 설명하고 있습니다.
Linked Issues check ✅ Passed 코드 변경사항이 이슈 #110의 모든 체크리스트 항목(리스트 UI 리팩토링, 아이콘 정리, 스타일 수정, 삭제 모달, 권한 변경 버튼 등)을 충족합니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 이슈 #110 범위 내에 있으며, 멤버 관리 페이지 리팩토링 목표에 부합합니다.
Description check ✅ Passed PR 설명이 요구되는 모든 필수 섹션을 포함하고 있으며, 변경사항과 작업 내용이 상세하게 기재되어 있습니다.

✏️ 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
📝 Coding Plan
  • Generate coding plan for human review comments

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

github-actions bot commented Mar 20, 2026

📚 Storybook 배포 완료

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

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

Labels

✨ Feature 기능 개발 🎨 Html&css 마크업 & 스타일링 🔨 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant