Skip to content

Commit bc11080

Browse files
committed
feat: 관리자 역할 수정 모달 추가
1 parent 04872cd commit bc11080

3 files changed

Lines changed: 131 additions & 5 deletions

File tree

src/api-client/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -990,7 +990,7 @@ export const addPoster = (
990990

991991
const formData = new FormData();
992992
formData.append(`image`, addPosterBody.image)
993-
formData.append(`posterRequest`, new Blob([JSON.stringify(addPosterBody.posterRequest)], { type: 'application/json' }));
993+
formData.append(`posterRequest`, JSON.stringify(addPosterBody.posterRequest));
994994

995995
return customMutator<void>(
996996
{url: `/display/posters`, method: 'POST',
@@ -2485,7 +2485,7 @@ export const updatePoster = (
24852485
if(updatePosterBody.image !== undefined) {
24862486
formData.append(`image`, updatePosterBody.image)
24872487
}
2488-
formData.append(`posterRequest`, new Blob([JSON.stringify(updatePosterBody.posterRequest)], { type: 'application/json' }));
2488+
formData.append(`posterRequest`, JSON.stringify(updatePosterBody.posterRequest));
24892489

24902490
return customMutator<void>(
24912491
{url: `/display/posters/${posterId}`, method: 'PATCH',

src/app/(auth)/admin/page.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
'use client';
22

33
import { useQueryClient, useMutation } from '@tanstack/react-query';
4-
import { Search, Plus, ChevronLeft, ChevronRight, Trash2 } from 'lucide-react';
4+
import { Search, Plus, ChevronLeft, ChevronRight, Trash2, Pencil } from 'lucide-react';
55
import { useState, useRef, useEffect } from 'react';
66
import toast from 'react-hot-toast';
77
import { useGetAdminList, deleteAdmins } from '@/api-client';
88
import { addAdmins } from '@/api-client';
9-
import { AdminRequest, AdminMemberDetailRole } from '@/api-client/model';
9+
import { AdminRequest, AdminMemberDetailRole, AdminMemberDetail } from '@/api-client/model';
1010
import AddAdminModal from '@/components/modal/AddAdminModal';
11+
import UpdateRoleModal from '@/components/modal/UpdateRoleModal';
1112
import TableSkeleton from '@/components/ui/table-skeleton';
1213
import { AdminRole } from '@/types/modal';
1314
import { Student } from '@/types/student';
@@ -33,6 +34,7 @@ function RoleBadge({ role }: { role: AdminMemberDetailRole }) {
3334
export default function AdminPage() {
3435
const [currentPage, setCurrentPage] = useState(1);
3536
const [isModalOpen, setIsModalOpen] = useState(false);
37+
const [editTarget, setEditTarget] = useState<AdminMemberDetail | null>(null);
3638
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
3739
const cachedTotalPages = useRef(1);
3840

@@ -205,11 +207,14 @@ export default function AdminPage() {
205207
<th className="whitespace-nowrap px-4 py-3.5 text-left text-xs font-semibold uppercase tracking-wider text-[var(--foreground-muted)]">
206208
권한
207209
</th>
210+
<th className="whitespace-nowrap px-4 py-3.5 text-center text-xs font-semibold uppercase tracking-wider text-[var(--foreground-muted)] w-20">
211+
관리
212+
</th>
208213
</tr>
209214
</thead>
210215
<tbody className="divide-y divide-[var(--border-muted)]">
211216
{isLoading ? (
212-
<TableSkeleton columns={5} rows={10} />
217+
<TableSkeleton columns={6} rows={10} />
213218
) : (
214219
admins.map((admin, index) => (
215220
<tr
@@ -236,6 +241,14 @@ export default function AdminPage() {
236241
<td className="whitespace-nowrap px-4 py-4 text-sm">
237242
<RoleBadge role={admin.role} />
238243
</td>
244+
<td className="whitespace-nowrap px-4 py-4 text-center">
245+
<button
246+
onClick={() => setEditTarget(admin)}
247+
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-[var(--foreground-muted)] hover:bg-[var(--background-hover)] hover:text-[var(--foreground)] transition-colors cursor-pointer"
248+
>
249+
<Pencil className="h-4 w-4" />
250+
</button>
251+
</td>
239252
</tr>
240253
))
241254
)}
@@ -288,6 +301,14 @@ export default function AdminPage() {
288301
onClose={() => setIsModalOpen(false)}
289302
onApply={handleAddAdmins}
290303
/>
304+
305+
{editTarget && (
306+
<UpdateRoleModal
307+
isOpen={!!editTarget}
308+
onClose={() => setEditTarget(null)}
309+
admin={editTarget}
310+
/>
311+
)}
291312
</div>
292313
);
293314
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
'use client';
2+
3+
import { useQueryClient } from '@tanstack/react-query';
4+
import { X } from 'lucide-react';
5+
import { useState } from 'react';
6+
import toast from 'react-hot-toast';
7+
import { useUpdateAdminRole } from '@/api-client';
8+
import { AdminMemberDetailRole } from '@/api-client/model';
9+
import { AdminRoleUpdateRequestRole } from '@/api-client/model';
10+
11+
const ROLE_OPTIONS: { value: AdminRoleUpdateRequestRole; label: string }[] = [
12+
{ value: 'WORKER', label: '근무자' },
13+
{ value: 'GA', label: '총무부' },
14+
{ value: 'ADMIN', label: '관리자' },
15+
];
16+
17+
interface UpdateRoleModalProps {
18+
isOpen: boolean;
19+
onClose: () => void;
20+
admin: {
21+
memberId: number;
22+
name: string;
23+
role: AdminMemberDetailRole;
24+
};
25+
}
26+
27+
export default function UpdateRoleModal({ isOpen, onClose, admin }: UpdateRoleModalProps) {
28+
const [selectedRole, setSelectedRole] = useState<AdminRoleUpdateRequestRole>(admin.role);
29+
const queryClient = useQueryClient();
30+
31+
const { mutate, isPending } = useUpdateAdminRole({
32+
mutation: {
33+
onSuccess: () => {
34+
queryClient.invalidateQueries({ queryKey: ['/admin/members/admins'] });
35+
toast.success('권한이 변경되었습니다.');
36+
onClose();
37+
},
38+
onError: (error: any) => {
39+
const message = error?.response?.data?.message || '권한 변경에 실패했습니다.';
40+
toast.error(message);
41+
},
42+
},
43+
});
44+
45+
if (!isOpen) return null;
46+
47+
const handleSubmit = () => {
48+
if (selectedRole === admin.role) return;
49+
mutate({ memberId: admin.memberId, data: { role: selectedRole } });
50+
};
51+
52+
return (
53+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
54+
<div className="w-full max-w-md rounded-lg bg-[var(--popover)] shadow-xl">
55+
<div className="flex items-center justify-between border-b border-[var(--border)] px-6 py-4">
56+
<h2 className="text-xl font-bold text-[var(--foreground)]">권한 변경</h2>
57+
<button
58+
onClick={onClose}
59+
className="rounded-full p-1 text-[var(--foreground-subtle)] hover:bg-[var(--background-hover)] hover:text-[var(--foreground-muted)] cursor-pointer"
60+
>
61+
<X className="h-5 w-5" />
62+
</button>
63+
</div>
64+
65+
<div className="px-6 py-4">
66+
<p className="mb-4 text-sm text-[var(--foreground-muted)]">
67+
<span className="font-medium text-[var(--foreground)]">{admin.name}</span>의 권한을
68+
변경합니다.
69+
</p>
70+
<div className="flex gap-2">
71+
{ROLE_OPTIONS.map((option) => (
72+
<button
73+
key={option.value}
74+
onClick={() => setSelectedRole(option.value)}
75+
className={`flex-1 rounded-lg border px-4 py-2.5 text-sm font-medium transition-colors cursor-pointer ${
76+
selectedRole === option.value
77+
? 'border-[var(--primary)] bg-[var(--primary)] text-white'
78+
: 'border-[var(--border)] bg-[var(--card)] text-[var(--foreground-muted)] hover:bg-[var(--background-hover)]'
79+
}`}
80+
>
81+
{option.label}
82+
</button>
83+
))}
84+
</div>
85+
</div>
86+
87+
<div className="flex items-center justify-end gap-2 border-t border-[var(--border)] px-6 py-4">
88+
<button
89+
onClick={onClose}
90+
className="rounded-lg px-4 py-2 text-sm font-medium text-[var(--foreground-muted)] hover:bg-[var(--background-hover)] focus:outline-none focus:ring-2 focus:ring-[var(--border)] focus:ring-offset-2 cursor-pointer"
91+
>
92+
취소
93+
</button>
94+
<button
95+
onClick={handleSubmit}
96+
disabled={selectedRole === admin.role || isPending}
97+
className="rounded-lg bg-[var(--primary)] px-4 py-2 text-sm font-medium text-white hover:bg-[var(--primary-hover)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:ring-offset-2 disabled:opacity-50 cursor-pointer"
98+
>
99+
{isPending ? '변경 중...' : '변경'}
100+
</button>
101+
</div>
102+
</div>
103+
</div>
104+
);
105+
}

0 commit comments

Comments
 (0)