background
aurora5분

관리자 페이지 디자인

2025년 8월 28일

Aurora 프로젝트 - 관리자 페이지 시스템 구축

기존 서버 가입 요청 페이지만 있던 관리자 시스템을 확장하여, 완전한 서버 관리 기능을 제공하는 4개의 관리자 페이지를 구축했다. Discord/Slack 스타일의 UI와 체계적인 권한 관리 시스템을 구현했다.

마주한 문제들

1. API 데이터 구조 불일치 문제

useAdmin.ts에서 서버 접근 권한 수정 시 payload 구조가 실제 API와 맞지 않는 문제가 있었다.

typescript
// 문제가 된 코드 - 중첩된 객체 구조
await patchServerAccessMutation.mutateAsync({
  serverUrl,
  status: { status: "Approved" }, // ❌ 불필요한 중첩
  userEmail: userEmail,
});

// 서버로 전송되는 데이터
{
  "status": {
    "status": "Approved"  // ❌ 중복된 구조
  },
  "userEmail": "user@example.com"
}

문제점들:

  • 서버에서는 status: "Approved" 형태를 기대하는데 중첩 객체로 전송
  • TypeScript 타입 정의와 실제 사용이 불일치
  • API 호출 실패 원인

2. 관리자 기능 부족

기존에는 서버 가입 요청 승인/거절 기능만 있어서 완전한 서버 관리가 불가능했다.

부족했던 기능들:

  • 멤버 관리 (역할 변경, 킥/차단)
  • 역할 및 권한 시스템
  • 초대 링크 관리
  • 서버 설정 및 삭제

해결책: 체계적인 관리자 시스템 구축

1. API 데이터 구조 수정

typescript
// frontend/src/app/(servers)/types/ServerAccess.ts

// Before - 중첩된 객체 타입
export interface ServerStatus {
    status : "Approved" | "Pending" | "Banned";
}

// After - 단순한 문자열 유니온 타입
export type ServerStatus = "Approved" | "Pending" | "Banned";
typescript
// frontend/src/app/(servers)/hooks/useAdmin.ts

// Before
await patchServerAccessMutation.mutateAsync({
  serverUrl,
  status: { status: "Approved" },
  userEmail: userEmail,
});

// After
await patchServerAccessMutation.mutateAsync({
  serverUrl,
  status: "Approved", // ✅ 단순한 문자열
  userEmail: userEmail,
});

2. 멤버 관리 시스템 구축

typescript
// frontend/src/app/(servers)/[server_id]/admin/members/page.tsx
const MembersPage = () => {
  const {
    members,
    filteredMembers,
    selectedMembers,
    filterRole,
    filterStatus,
    searchQuery,
    handleSelectMember,
    handleSelectAll,
    handleFilterChange,
    handleSearchChange,
    handleBulkKick,
    handleBulkBan,
    handleRoleChange,
  } = useMembersPage();

  return (
    <div className="flex-1 bg-gray-900 p-6">
      {/* 검색 및 필터 */}
      <MemberFilters
        filterRole={filterRole}
        filterStatus={filterStatus}
        searchQuery={searchQuery}
        onFilterChange={handleFilterChange}
        onSearchChange={handleSearchChange}
      />

      {/* 일괄 작업 */}
      {selectedMembers.size > 0 && (
        <BulkActions
          selectedCount={selectedMembers.size}
          onBulkKick={handleBulkKick}
          onBulkBan={handleBulkBan}
        />
      )}

      {/* 멤버 목록 */}
      <div className="bg-gray-800 rounded-lg overflow-hidden">
        {filteredMembers.map((member) => (
          <MemberCard
            key={member.id}
            member={member}
            isSelected={selectedMembers.has(member.id)}
            onSelect={handleSelectMember}
            onRoleChange={handleRoleChange}
          />
        ))}
      </div>
    </div>
  );
};

3. 역할 관리 시스템 구축

typescript
// frontend/src/app/(servers)/[server_id]/admin/roles/page.tsx
const RolesPage = () => {
  const {
    roles,
    showCreateModal,
    showEditModal,
    editingRole,
    handleCreateRole,
    handleEditRole,
    handleDeleteRole,
    handlePermissionChange,
    setShowCreateModal,
    setShowEditModal,
    setEditingRole,
  } = useRolesPage();

  return (
    <div className="flex-1 bg-gray-900 p-6">
      {/* 역할 목록 */}
      <div className="space-y-4">
        {roles.map((role, index) => (
          <RoleCard
            key={role.id}
            role={role}
            position={index + 1}
            onEdit={(role) => {
              setEditingRole(role);
              setShowEditModal(true);
            }}
            onDelete={handleDeleteRole}
            onPermissionChange={handlePermissionChange}
          />
        ))}
      </div>

      {/* 모달들 */}
      {showCreateModal && (
        <CreateRoleModal
          onClose={() => setShowCreateModal(false)}
          onSubmit={handleCreateRole}
        />
      )}

      {showEditModal && editingRole && (
        <EditRoleModal
          role={editingRole}
          onClose={() => {
            setShowEditModal(false);
            setEditingRole(null);
          }}
          onSubmit={handleEditRole}
        />
      )}
    </div>
  );
};

4. 초대 관리 시스템 구축

typescript
// frontend/src/app/(servers)/[server_id]/admin/invitations/page.tsx
const InvitationsPage = () => {
  const {
    invitations,
    showCreateModal,
    setShowCreateModal,
    handleCreateInvite,
    handleDeleteInvite,
    handleCopyInvite,
  } = useInvitationsPage();

  return (
    <div className="flex-1 bg-gray-900 p-6">
      {/* 초대 목록 */}
      <div className="space-y-4">
        {invitations.map((invitation) => (
          <InviteCard
            key={invitation.id}
            invitation={invitation}
            onDelete={handleDeleteInvite}
            onCopy={handleCopyInvite}
          />
        ))}
      </div>

      {/* 초대 생성 모달 */}
      {showCreateModal && (
        <CreateInviteModal
          onClose={() => setShowCreateModal(false)}
          onSubmit={handleCreateInvite}
        />
      )}
    </div>
  );
};

5. 서버 설정 페이지 구축

typescript
// frontend/src/app/(servers)/[server_id]/admin/settings/page.tsx
const SettingsPage = () => {
  const serverInfo = useCurrentServerInfo();
  const [confirmDeleteText, setConfirmDeleteText] = useState("");

  const handleDeleteServer = async () => {
    if (confirmDeleteText !== serverInfo?.serverName) {
      alert("서버 이름이 정확하지 않습니다.");
      return;
    }

    if (!confirm("정말로 이 서버를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.")) {
      return;
    }

    // 서버 삭제 로직
  };

  return (
    <div className="flex-1 bg-gray-900 p-6">
      {/* 일반 설정 */}
      <div className="mb-8">
        <h2 className="text-xl font-bold text-white mb-4">일반 설정</h2>
        <div className="bg-gray-800 rounded-lg p-6">
          {/* 서버 이름, 설명 등 */}
        </div>
      </div>

      {/* 보안 설정 */}
      <div className="mb-8">
        <h2 className="text-xl font-bold text-white mb-4">보안 설정</h2>
        <div className="bg-gray-800 rounded-lg p-6">
          {/* 자동 승인, 초대 정책 등 */}
        </div>
      </div>

      {/* 위험 영역 - 서버 삭제 */}
      <div className="mb-8">
        <h2 className="text-xl font-bold text-red-400 mb-4">위험 영역</h2>
        <div className="bg-red-900/20 border border-red-500/30 rounded-lg p-6">
          <input
            type="text"
            value={confirmDeleteText}
            onChange={(e) => setConfirmDeleteText(e.target.value)}
            placeholder="서버 이름을 입력하세요"
            className="w-full bg-gray-700 border border-red-600 rounded-md px-3 py-2 text-white"
          />
          <button
            onClick={handleDeleteServer}
            disabled={confirmDeleteText !== serverInfo?.serverName}
            className="w-full px-4 py-3 bg-red-600 text-white rounded-md mt-4 disabled:opacity-50"
          >
            서버 영구 삭제
          </button>
        </div>
      </div>
    </div>
  );
};

역할별 권한 설계

typescript
// frontend/src/app/(servers)/hooks/useRoles.ts
const mockRoles: Role[] = [
  {
    id: "owner",
    name: "Owner",
    color: "#FFD700",
    permissions: defaultPermissions.map(p => ({ ...p, enabled: true })), // 모든 권한
    memberCount: 1,
    isOwner: true,
  },
  {
    id: "admin",
    name: "Admin",
    color: "#FF6B6B",
    permissions: defaultPermissions.map(p => ({
      ...p,
      enabled: p.id !== "administrator" // 최고 관리자 권한 제외
    })),
    memberCount: 2,
  },
  {
    id: "member",
    name: "Member",
    color: "#95A5A6",
    permissions: defaultPermissions.map(p => ({
      ...p,
      enabled: ["view_channels", "send_messages", "connect_voice", "speak"].includes(p.id)
    })),
    memberCount: 50,
    isDefault: true,
  },
];

컴포넌트 아키텍처 설계

1. 재사용 가능한 컴포넌트 구조

plain
admin/components/
├── MemberCard.tsx           # 개별 멤버 표시
├── MemberFilters.tsx        # 검색/필터 UI
├── BulkActions.tsx          # 일괄 작업 UI
├── RoleCard.tsx             # 개별 역할 표시
├── CreateRoleModal.tsx      # 역할 생성 모달
├── EditRoleModal.tsx        # 역할 수정 모달
├── InviteCard.tsx           # 개별 초대 표시
└── CreateInviteModal.tsx    # 초대 생성 모달

2. 커스텀 훅 분리

plain
hooks/
├── useMembers.ts            # 멤버 관리 로직
├── useRoles.ts              # 역할 관리 로직
├── useInvitations.ts        # 초대 관리 로직
└── useAdmin.ts              # 기존 가입 요청 관리

3. 일관된 디자인 시스템

typescript
// 공통 스타일 패턴
const cardStyle = "bg-gray-800 rounded-lg border border-gray-700 p-4";
const buttonPrimary = "px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors";
const buttonDanger = "px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors";
const inputStyle = "w-full bg-gray-700 border border-gray-600 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500";

🎉 최종 결과

구축된 관리자 시스템

페이지주요 기능컴포넌트 수
멤버 관리검색/필터, 역할 변경, 일괄 킥/차단3개
역할 관리역할 생성/수정/삭제, 권한 설정3개
초대 관리링크 생성/삭제, 만료시간 설정2개
서버 설정일반 설정, 보안 설정, 서버 삭제1개

기술적 성과

  • API 호환성: 서버 스펙에 맞는 데이터 구조로 수정
  • 타입 안전성: 완전한 TypeScript 타입 커버리지
  • 모듈화: 단일 책임 원칙을 따르는 컴포넌트 설계
  • 재사용성: 독립적으로 사용 가능한 UI 컴포넌트
  • 확장성: 새로운 권한/역할 추가 용이

UI/UX 개선

  • 다크 테마: Discord/Slack 스타일의 모던한 인터페이스
  • 반응형 디자인: 모바일/태블릿 호환
  • 인터랙티브 요소: 호버 효과, 트랜지션 애니메이션
  • 직관적 UX: 명확한 액션 버튼, 상태 표시
  • 안전장치: 위험한 작업에 대한 확인 절차

권한 시스템 완성

plain
Owner (서버 소유자)
├── 모든 권한 보유
├── 서버 삭제 가능
└── 다른 사용자 역할 변경 불가 (보호)

Admin (관리자)
├── 멤버 관리 (킥/차단/역할 변경)
├── 채널/역할 관리
├── 초대 링크 관리
└── 서버 설정 (삭제 제외)

Member (일반 멤버)
├── 채널 보기/메시지 전송
├── 음성 채널 참여
└── 기본적인 서버 기능 사용

태그

#aurora#Next.js