Skip to content

[FE] MVP2-0 Issue 0-5: Organization Members UI 추가 #75

Description

@yoonki1207

목적

MVP 2-0 Organization Membership / Invitation Foundation을 브라우저에서 사용할 수 있게 한다. Settings 화면에 Organization 관리 영역을 추가하고, organization members list, invite modal, invitation accept entry, member state/auth_state update, remove action, team member picker, direct permission user picker를 organization_memberships 기준으로 전환한다.

이 이슈는 새 API를 제품 UI에 연결하는 FE 작업이다. Backend DB/model, permission helper, member/invitation API가 먼저 준비되어야 한다.

선행 조건

Blocked by:

  • MBA-53: [BE][API] MVP2-0 Issue 0-3: organization member/invitation API 추가

Transitive dependencies:

  • MBA-52: permission helper fail-closed 전환
  • MBA-51: organization_memberships model/migration

논리적 연결:

  • Issue 0-4: team add와 user direct permission grant의 backend 검증 기준 변경
  • Issue 0-4가 별도 Linear 이슈로 생성되면 이 이슈의 관련/blocked-by 관계를 추가하는 것을 권장한다.

문서 기준 및 정합성 확인

Source of truth:

  • docs/implementation-plan/mvp-2-0-organization-membership-plan.md
  • docs/api/organization-rbac.md
  • docs/data-model/rbac-permission-policy.md

정합성 확인:

  • 계획서 Issue 0-5의 작업 범위는 organization members list, invite modal, accept invitation entry, member state/auth_state update, remove action, team member picker 변경, direct permission user picker 변경이다.
  • 계획서 16.1 Organization settings에 맞춰 Settings 하위에 Organization/Members/Teams/Permissions 구조를 추가한다.
  • 계획서 16.2 Invite modal에 맞춰 기존 가입 user 검색, organization auth 선택, invite submit만 지원한다.
  • email-only invite, bulk invite, CSV import는 MVP2-0 범위 밖이다.
  • 계획서 16.3 Team member picker에 맞춰 active organization member만 team에 추가 가능하게 표시한다.
  • 계획서 16.4 Resource permission user picker에 맞춰 active organization member만 direct permission 대상으로 표시하고, team membership 여부와 무관하게 선택 가능해야 한다.
  • 계획서 16.5 Active organization switcher는 MVP2-0에서 선택 사항이다. 다중 organization switcher를 넣지 않더라도 current organization 표시와 organization list 기반 초대 accept entry는 제공한다.

현재 코드 기준

현재 확인된 FE 상태:

  • apps/client/app/dashboard/settings/page.tsx는 현재 provider 탭만 가진 LLM Provider 설정 화면이다.
  • apps/client/lib/apiClient.ts에 공용 axios client가 있다.
  • apps/client/app/features/auth/api/authApi.ts처럼 feature별 API wrapper 패턴이 있다.
  • apps/client/app/features/workflow/api/workflowApi.ts는 자체 axios instance를 쓰고 있어 API wrapper 패턴이 혼재되어 있다.
  • Organization member 전용 FE API/type/component는 아직 없다.
  • 기존 team/direct permission UI가 명확히 분리되어 있지 않다면, 이 이슈에서는 새 Organization UI와 reusable picker/API를 먼저 만들고 실제 기존 화면 연결 지점을 찾아 반영한다.

구현 범위

1. FE API client 추가

신규 파일 후보:

  • apps/client/app/features/organization/api/organizationApi.ts
  • apps/client/app/features/organization/types/Organization.ts

공용 apiClient 사용을 우선한다.

필요 type:

type MembershipState = 'invited' | 'active' | 'suspended' | 'removed';
type OrganizationAuthState = 'member' | 'manager';

type OrganizationSummary = {
  id: string;
  name: string;
  membership_state: MembershipState;
  organization_auth_state: OrganizationAuthState;
  is_active: boolean;
};

type OrganizationMember = {
  id: string;
  organization_id: string;
  user_id: string;
  user_email: string;
  user_name: string;
  membership_state: MembershipState;
  organization_auth_state: OrganizationAuthState;
  invited_by?: string | null;
  invited_at?: string | null;
  accepted_at?: string | null;
  removed_at?: string | null;
  created_at: string;
  updated_at: string;
};

type OrganizationMemberInviteRequest = {
  user_id: string;
  organization_auth_state?: OrganizationAuthState;
};

type OrganizationMemberUpdateRequest = {
  membership_state?: 'active' | 'suspended';
  organization_auth_state?: OrganizationAuthState;
};

type OrganizationMemberRemoveResponse = {
  status: 'removed';
  removed_team_memberships: number;
  revoked_user_permissions: {
    workflow: number;
    llm_credential: number;
    knowledge_base: number;
    audit: number;
  };
};

필요 API method:

listOrganizations(): Promise<OrganizationSummary[]>
getCurrentOrganization(): Promise<OrganizationSummary>
listMembers(organizationId: string, state?: MembershipState): Promise<OrganizationMember[]>
inviteMember(organizationId: string, payload: OrganizationMemberInviteRequest): Promise<OrganizationMember>
acceptInvitation(organizationId: string): Promise<OrganizationMember>
updateMember(organizationId: string, userId: string, payload: OrganizationMemberUpdateRequest): Promise<OrganizationMember>
removeMember(organizationId: string, userId: string): Promise<OrganizationMemberRemoveResponse>

후보 user search API:

  • Backend에 user search/list endpoint가 이미 있으면 그것을 사용한다.
  • 없으면 Issue 0-3 또는 별도 BE task에 GET /users?query= 같은 기존 user 검색 endpoint 필요성을 남긴다.
  • MVP2-0 invite는 기존 가입 user만 대상이므로 email-only free text invite처럼 보이면 안 된다.

2. Settings 화면 구조 확장

대상 파일:

  • apps/client/app/dashboard/settings/page.tsx

변경 방향:

  • 현재 SettingsTab = 'provider'를 확장한다.
  • 최소 탭 구조:
type SettingsTab = 'organization-members' | 'organization-teams' | 'organization-permissions' | 'provider';

UI 구조:

Settings
  Organization
    Members
    Teams
    Permissions
  Provider

실용적 구현:

  • 기존 LLM Provider UI는 Provider 탭으로 유지한다.
  • 새 Organization 탭 그룹을 Settings 상단 탭 또는 좌측 secondary nav로 추가한다.
  • 큰 navigation 개편은 하지 않는다.
  • 현재 organization 이름과 current user의 organization_auth_state를 상단에 표시한다.
  • invited organization이 있으면 accept entry를 보이게 한다.

3. Organization Members 화면 추가

신규 component 후보:

  • apps/client/app/features/organization/components/OrganizationMembersPanel.tsx
  • apps/client/app/features/organization/components/OrganizationMemberRow.tsx
  • apps/client/app/features/organization/components/MemberStateBadge.tsx
  • apps/client/app/features/organization/components/OrganizationAuthBadge.tsx

Members panel 요구사항:

  • current organization을 로드한다.
  • manager 권한이 있으면 members list를 로드한다.
  • manager가 아니면 member 관리 action은 숨기거나 disabled 처리한다.
  • members table/list는 다음 필드를 표시한다.
    • user name
    • user email
    • membership_state badge: invited, active, suspended, removed
    • organization_auth_state badge: member, manager
    • invited_at / accepted_at
    • row actions
  • removed member는 기본 list에서 숨기고, filter로 선택했을 때만 표시하는 것을 권장한다.
  • state filter: all active/invited/suspended 기본, optional removed
  • loading, empty, error 상태를 구현한다.

Row action:

  • invited: remove invitation
  • active member: suspend, promote/demote, remove
  • active manager: demote 금지 조건 표시, remove 금지 조건 표시
  • suspended: reactivate, remove
  • removed: action 없음 또는 re-invite는 invite modal 흐름으로 처리

UX guard:

  • self remove button은 표시하지 않거나 disabled + reason을 제공한다.
  • self demote button은 표시하지 않거나 disabled + reason을 제공한다.
  • 마지막 manager로 보이는 경우 demote/suspend/remove는 disabled 처리한다.
  • 최종 권한 판단은 BE가 해야 하며, FE guard는 UX 보조다.

4. Invite modal 추가

신규 component 후보:

  • apps/client/app/features/organization/components/InviteOrganizationMemberModal.tsx
  • apps/client/app/features/organization/components/UserSearchCombobox.tsx

Modal 요구사항:

  • 기존 가입 user 검색/선택
  • selected user 표시: name, email
  • organization auth 선택: segmented control 또는 select로 member, manager
  • submit 시 POST /organizations/{organization_id}/members/invitations
  • 성공 시 modal close, members refetch
  • 이미 active/invited인 user에 대한 idempotent 응답은 성공으로 처리하고 list를 갱신한다.
  • suspended user invite 409, deactivated user invite 실패, self invite 400은 inline error로 표시한다.

미지원:

  • email-only invite
  • bulk invite
  • CSV import
  • invitation token 생성
  • email 발송

5. Invitation accept entry 추가

적용 위치 후보:

  • Settings Organization area 상단
  • Dashboard sidebar/header organization summary area가 있으면 해당 위치
  • MVP2-0 최소 구현은 Settings 화면 상단 또는 /dashboard/settings 내부로 충분하다.

요구사항:

  • GET /organizations로 current user의 active/invited organizations를 조회한다.
  • membership_state='invited' organization을 별도 section으로 표시한다.
  • invited row에는 organization name, invited state, accept button을 표시한다.
  • accept button은 POST /organizations/{organization_id}/members/me/accept 호출.
  • accept 성공 후 organization list/current organization/members list를 refetch한다.
  • accept 전에는 resource 화면으로 진입 가능한 UI처럼 보이지 않게 한다.

6. Member update/remove action 연결

Update action:

  • PATCH /organizations/{organization_id}/members/{user_id} 호출
  • suspend/reactivate
  • member/manager auth_state update
  • 성공 후 members list refetch
  • 409/400 guard error는 row-level inline message 또는 toast로 표시

Remove action:

  • DELETE /organizations/{organization_id}/members/{user_id} 호출
  • destructive confirmation modal 사용
  • response의 cleanup count를 성공 메시지에 표시할 수 있다.
  • 성공 후 members list refetch

확인 modal 문구에 포함할 정보:

  • organization에서 제거된다.
  • team membership이 제거된다.
  • direct workflow/LLM permission이 revoke된다.
  • 재초대해도 과거 권한은 자동 복구되지 않는다.

7. Team member picker 변경

대상 후보:

  • 기존 team member add UI가 있으면 해당 component
  • 없다면 Organization Teams 탭의 새 add member control 또는 reusable picker로 구현

요구사항:

  • active organization members만 표시한다.
  • invited/suspended/removed user는 선택 불가다.
  • 이미 해당 team에 속한 active member는 제외하거나 disabled 표시한다.
  • 검색은 local filter 또는 server query를 사용한다.
  • user id 직접 입력 UI가 있다면 active member search/select UI로 교체한다.
  • submit은 기존 team add endpoint를 사용하되, BE가 Issue 0-4에서 최종 검증한다.

Reusable component 후보:

  • ActiveOrganizationMemberPicker

Props 예시:

type ActiveOrganizationMemberPickerProps = {
  organizationId: string;
  excludedUserIds?: string[];
  value?: string;
  onChange: (userId: string | null) => void;
  disabled?: boolean;
};

8. Direct permission user picker 변경

대상 후보:

  • workflow direct user permission UI
  • LLM credential direct user permission UI
  • Organization Permissions 탭을 새로 만든다면 그 안의 user grant form

요구사항:

  • active organization members만 표시한다.
  • team membership 여부와 무관하게 표시한다.
  • 이미 direct permission row가 있으면 현재 resource auth_state를 표시한다.
  • invited/suspended/removed user는 표시하지 않거나 disabled 표시한다.
  • submit은 기존 permission grant endpoint를 사용하되, BE가 Issue 0-4에서 최종 검증한다.

중요 정합성:

  • MVP2-0부터 user direct permission은 team membership이 아니라 active organization membership이 전제다.
  • 따라서 picker에서 "team에 없는 active member"도 direct permission 대상으로 표시되어야 한다.

9. Optional: current organization display

다중 organization switcher는 MVP2-0에서 필수는 아니다.

최소 구현:

  • Settings Organization area 상단에 current organization name과 auth state를 표시한다.
  • GET /organizations 결과가 여러 개면 active/invited 목록을 보여준다.
  • invited organization은 accept action만 제공하고 resource 화면 진입 action은 제공하지 않는다.

제외:

  • active organization header/session/cookie 계약 확정
  • full organization switcher
  • multi-org route scope 전환

10. 상태 처리와 접근성

필수 상태:

  • loading skeleton 또는 loading row
  • empty members state
  • API error state
  • invite submit pending
  • update/remove pending per row
  • 401 redirect는 기존 apiClient interceptor에 맡긴다.
  • 403은 권한 없음 상태로 화면에 표시한다.

접근성/UX:

  • action button에는 명확한 accessible label을 둔다.
  • destructive action은 confirmation을 거친다.
  • 긴 email/name이 table layout을 깨지 않게 truncate 처리한다.
  • 모바일에서는 table이 깨지지 않도록 card/list fallback 또는 horizontal scroll을 적용한다.

테스트 범위

단위/컴포넌트 테스트 후보:

  • apps/client/app/features/organization/api/organizationApi.test.ts
  • apps/client/app/features/organization/components/OrganizationMembersPanel.test.tsx
  • apps/client/app/features/organization/components/InviteOrganizationMemberModal.test.tsx
  • apps/client/app/features/organization/components/ActiveOrganizationMemberPicker.test.tsx

필수 테스트:

  1. Members panel이 active/invited/suspended state badge를 구분해 렌더링한다.
  2. manager row에는 organization auth badge가 표시된다.
  3. manager 권한이 없으면 invite/update/remove action이 숨겨지거나 disabled 된다.
  4. invite modal은 existing user 선택 전 submit disabled다.
  5. invite modal은 organization_auth_state 기본값을 member로 보낸다.
  6. invite 성공 후 members list refetch가 호출된다.
  7. invited organization entry에서 accept 호출 후 organization list refetch가 호출된다.
  8. suspended member는 reactivate action을 표시한다.
  9. active member는 suspend/remove action을 표시한다.
  10. self remove/self demote/last manager action은 UI에서 disabled 처리된다.
  11. remove confirmation 후 cleanup count success message를 표시한다.
  12. active member picker는 active member만 options에 노출한다.
  13. direct permission picker는 team membership이 없는 active member도 options에 노출한다.
  14. invited/suspended/removed user는 team/direct permission picker에서 선택할 수 없다.
  15. 403 response는 권한 없음 메시지로 표시된다.

E2E/manual smoke:

  1. manager로 로그인한다.
  2. Settings > Organization > Members에서 기존 user를 invite한다.
  3. invited user로 로그인한다.
  4. invitation entry가 보이고 accept할 수 있다.
  5. manager가 user를 team에는 넣지 않고 direct permission 대상으로 선택할 수 있다.
  6. active member만 team picker에 표시된다.
  7. manager가 user를 remove하면 members list에서 상태가 반영된다.

완료 조건

  • organization FE types/API wrapper가 추가된다.
  • Settings 화면에 Organization 영역 또는 탭 그룹이 추가된다.
  • Members list가 user name/email, membership_state, organization_auth_state를 표시한다.
  • invited/active/suspended/removed 상태가 시각적으로 구분된다.
  • manager가 기존 가입 user를 invite할 수 있다.
  • invite modal은 member/manager auth 선택을 지원한다.
  • email-only invite, bulk invite, CSV import는 UI에 노출하지 않는다.
  • invited user가 invitation entry에서 accept할 수 있다.
  • manager가 active member를 suspend하고 suspended member를 reactivate할 수 있다.
  • manager가 member/manager auth_state를 변경할 수 있다.
  • manager가 member를 remove할 수 있다.
  • remove action은 confirmation을 거치고 cleanup 의미를 안내한다.
  • self remove/self demote/last manager 위험 action은 UI에서 차단 또는 disabled 된다.
  • team member picker가 active organization member만 표시한다.
  • direct permission user picker가 active organization member만 표시한다.
  • direct permission user picker는 team에 없는 active member도 표시한다.
  • 기존 Provider 설정 화면은 계속 접근 가능하다.
  • 기존 MVP1 team/resource permission API shape 변경을 전제하지 않는다.
  • 관련 FE 테스트 또는 최소 manual smoke가 통과한다.

명시적 제외 범위

이 이슈에서 하지 않는다.

  • Backend organization_memberships model/migration
  • Backend permission helper fail-closed 전환
  • Backend organization member/invitation API 구현
  • Backend team add/user direct permission grant 검증 기준 변경
  • email-only invite, invitation token, email 발송
  • bulk invite, CSV import
  • SSO/OIDC 자동 organization join
  • full multi-organization switcher와 active organization header/session/cookie 계약
  • MVP2 knowledge permission UI
  • audit log search UI

후속 연결

이 이슈는 MVP2-0 demo script의 UI 구간을 담당한다.

  • Organization Settings > Members에서 invite
  • invited user의 accept entry
  • manager의 direct permission target picker
  • manager의 team member picker
  • member remove 후 UI 반영

Issue 0-6 regression에서 이 흐름을 browser smoke로 다시 검증한다.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions