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. 재사용 가능한 컴포넌트 구조
plainadmin/components/ ├── MemberCard.tsx # 개별 멤버 표시 ├── MemberFilters.tsx # 검색/필터 UI ├── BulkActions.tsx # 일괄 작업 UI ├── RoleCard.tsx # 개별 역할 표시 ├── CreateRoleModal.tsx # 역할 생성 모달 ├── EditRoleModal.tsx # 역할 수정 모달 ├── InviteCard.tsx # 개별 초대 표시 └── CreateInviteModal.tsx # 초대 생성 모달
2. 커스텀 훅 분리
plainhooks/ ├── 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: 명확한 액션 버튼, 상태 표시
- ✅ 안전장치: 위험한 작업에 대한 확인 절차
권한 시스템 완성
plainOwner (서버 소유자) ├── 모든 권한 보유 ├── 서버 삭제 가능 └── 다른 사용자 역할 변경 불가 (보호) Admin (관리자) ├── 멤버 관리 (킥/차단/역할 변경) ├── 채널/역할 관리 ├── 초대 링크 관리 └── 서버 설정 (삭제 제외) Member (일반 멤버) ├── 채널 보기/메시지 전송 ├── 음성 채널 참여 └── 기본적인 서버 기능 사용
태그
#aurora#Next.js
