Skip to content

Commit 0815d00

Browse files
authored
Merge pull request #188 from CSE-Shaco/develop
fix(auth): 인증 문제 해결
2 parents c3070ec + 1125914 commit 0815d00

File tree

3 files changed

+299
-288
lines changed

3 files changed

+299
-288
lines changed

src/app/admin/member-manager/page.jsx

Lines changed: 155 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -78,21 +78,34 @@ export default function AdminUsersPage() {
7878

7979
/** 삭제 버튼 렌더링 권한(서버로 체크: LEAD 이상) */
8080
const [canRenderDelete, setCanRenderDelete] = useState(false);
81+
8182
useEffect(() => {
8283
let alive = true;
84+
8385
(async () => {
8486
try {
8587
const res = await apiClient.get('/auth/LEAD', {
8688
validateStatus: (s) => s === 200 || s === 204 || s === 401 || s === 403,
8789
headers: {Accept: 'application/json'},
8890
});
91+
92+
if (!alive) return;
93+
94+
// 403이면 바로 discard
95+
if (res?.status === 403) {
96+
setCanRenderDelete(false);
97+
return;
98+
}
99+
89100
const okHttp = res?.status === 200 || res?.status === 204;
90101
const okBody = (res?.data?.code ?? 200) === 200;
91-
if (alive) setCanRenderDelete(okHttp && okBody);
102+
103+
setCanRenderDelete(okHttp && okBody);
92104
} catch {
93105
if (alive) setCanRenderDelete(false);
94106
}
95107
})();
108+
96109
return () => {
97110
alive = false;
98111
};
@@ -234,13 +247,9 @@ export default function AdminUsersPage() {
234247
// 6개 데이터 컬럼 + ACTIONS
235248
const columns = useMemo(() => {
236249
const base = [{name: 'NAME', uid: 'name', sortable: true}, {
237-
name: 'MAJOR',
238-
uid: 'major',
239-
sortable: true
250+
name: 'MAJOR', uid: 'major', sortable: true
240251
}, {name: 'STUDENT ID', uid: 'studentId', sortable: true}, {
241-
name: 'EMAIL',
242-
uid: 'email',
243-
sortable: true
252+
name: 'EMAIL', uid: 'email', sortable: true
244253
}, {name: 'ROLE', uid: 'userRole', sortable: true}, {name: 'TEAM', uid: 'team', sortable: true},];
245254
if (canRenderDelete) {
246255
base.push({name: 'ACTIONS', uid: 'actions', sortable: false});
@@ -249,144 +258,144 @@ export default function AdminUsersPage() {
249258
}, [canRenderDelete]);
250259

251260
return (<div className="dark text-white py-[30px] px-[96px] mobile:px-[10px]">
252-
<h1 className="text-3xl font-bold mb-6">사용자 관리</h1>
253-
254-
{/* 검색바 */}
255-
<div className="mb-3">
256-
<AdminTableTopContent searchValue={searchValue} setSearchValue={setSearchValue} onSearch={onSearch}/>
257-
</div>
258-
259-
<Table
260-
aria-label="Users table"
261-
className="dark"
262-
sortDescriptor={pendingSort}
263-
onSortChange={setPendingSort}
264-
bottomContent={<AdminTableBottomContent page={page} totalPages={totalPages} totalUsers={totalUsers}
265-
onChangePage={setPage}/>}
266-
>
267-
<TableHeader columns={columns}>
268-
{(col) => (<TableColumn
269-
key={col.uid}
270-
allowsSorting={col.sortable}
271-
className={col.uid === 'userRole' || col.uid === 'team' ? 'w-[220px]' : col.uid === 'email' ? 'w-[260px]' : col.uid === 'actions' ? 'w-[120px] text-right' : ''}
272-
align={col.uid === 'actions' ? 'end' : 'start'}
261+
<h1 className="text-3xl font-bold mb-6">사용자 관리</h1>
262+
263+
{/* 검색바 */}
264+
<div className="mb-3">
265+
<AdminTableTopContent searchValue={searchValue} setSearchValue={setSearchValue} onSearch={onSearch}/>
266+
</div>
267+
268+
<Table
269+
aria-label="Users table"
270+
className="dark"
271+
sortDescriptor={pendingSort}
272+
onSortChange={setPendingSort}
273+
bottomContent={<AdminTableBottomContent page={page} totalPages={totalPages} totalUsers={totalUsers}
274+
onChangePage={setPage}/>}
275+
>
276+
<TableHeader columns={columns}>
277+
{(col) => (<TableColumn
278+
key={col.uid}
279+
allowsSorting={col.sortable}
280+
className={col.uid === 'userRole' || col.uid === 'team' ? 'w-[220px]' : col.uid === 'email' ? 'w-[260px]' : col.uid === 'actions' ? 'w-[120px] text-right' : ''}
281+
align={col.uid === 'actions' ? 'end' : 'start'}
282+
>
283+
{col.name}
284+
</TableColumn>)}
285+
</TableHeader>
286+
287+
<TableBody items={rows} isLoading={loading} loadingContent={<Spinner label="불러오는 중..."/>}
288+
emptyContent={err || '데이터가 없습니다.'}>
289+
{(user) => (<TableRow key={user.id} className="hover:bg-[#35353b99]">
290+
{/* NAME */}
291+
<TableCell>
292+
<span className="font-medium mr-2">{user.name}</span>
293+
<Chip size="sm" variant="flat" color={roleColor(user.userRole)}>
294+
{user.userRole}
295+
</Chip>
296+
</TableCell>
297+
298+
{/* MAJOR */}
299+
<TableCell>{user.major}</TableCell>
300+
301+
{/* STUDENT ID */}
302+
<TableCell>{user.studentId}</TableCell>
303+
304+
{/* EMAIL */}
305+
<TableCell>
306+
<span className="text-sm">{user.email}</span>
307+
</TableCell>
308+
309+
{/* ROLE */}
310+
<TableCell>
311+
<div className="flex items-center gap-3">
312+
<Select
313+
aria-label="역할 수정"
314+
selectedKeys={new Set([user.userRole || ''])}
315+
onSelectionChange={(keys) => {
316+
const nextRole = String(Array.from(keys)[0] || user.userRole);
317+
if (nextRole !== user.userRole) {
318+
const ok = confirm(`역할을 '${user.userRole}' → '${nextRole}' 로 변경할까요?`);
319+
if (ok) void patchSmart({user, nextRole, nextTeam: user.team ?? null});
320+
}
321+
}}
322+
size="sm"
323+
className="min-w-[140px]"
324+
classNames={{
325+
trigger: 'bg-zinc-900 text-white border border-zinc-700 data-[hover=true]:bg-zinc-800',
326+
value: 'text-white',
327+
popoverContent: 'bg-zinc-900 border border-zinc-700',
328+
listbox: 'text-white',
329+
selectorIcon: 'text-zinc-400',
330+
}}
331+
itemClasses={{
332+
base: 'rounded-md data-[hover=true]:bg-zinc-800 data-[focus=true]:bg-zinc-800',
333+
title: 'text-white',
334+
}}
335+
>
336+
{ROLE_OPTIONS.map((r) => (<SelectItem key={r} value={r}>
337+
{r}
338+
</SelectItem>))}
339+
</Select>
340+
</div>
341+
</TableCell>
342+
343+
{/* TEAM */}
344+
<TableCell>
345+
<Select
346+
aria-label="팀 수정"
347+
selectedKeys={new Set([user.team || ''])}
348+
onSelectionChange={(keys) => {
349+
const k = String(Array.from(keys)[0] ?? '');
350+
const nextTeam = k === '' ? null : k; // 서버에는 enum name로 전송
351+
if ((nextTeam ?? null) !== (user.team ?? null)) {
352+
const old = user.team ? TEAM_LABEL[user.team] || user.team : '(없음)';
353+
const neu = nextTeam ? TEAM_LABEL[nextTeam] || nextTeam : '(없음)';
354+
const ok = confirm(`팀을 '${old}' → '${neu}' 로 변경할까요?`);
355+
if (ok) void patchSmart({user, nextRole: user.userRole, nextTeam});
356+
}
357+
}}
358+
size="sm"
359+
className="min-w-[160px]"
360+
classNames={{
361+
trigger: 'bg-zinc-900 text-white border border-zinc-700 data-[hover=true]:bg-zinc-800',
362+
value: 'text-white',
363+
popoverContent: 'bg-zinc-900 border border-zinc-700',
364+
listbox: 'text-white',
365+
selectorIcon: 'text-zinc-400',
366+
}}
367+
itemClasses={{
368+
base: 'rounded-md data-[hover=true]:bg-zinc-800 data-[focus=true]:bg-zinc-800',
369+
title: 'text-white',
370+
}}
273371
>
274-
{col.name}
275-
</TableColumn>)}
276-
</TableHeader>
277-
278-
<TableBody items={rows} isLoading={loading} loadingContent={<Spinner label="불러오는 중..."/>}
279-
emptyContent={err || '데이터가 없습니다.'}>
280-
{(user) => (<TableRow key={user.id} className="hover:bg-[#35353b99]">
281-
{/* NAME */}
282-
<TableCell>
283-
<span className="font-medium mr-2">{user.name}</span>
284-
<Chip size="sm" variant="flat" color={roleColor(user.userRole)}>
285-
{user.userRole}
286-
</Chip>
287-
</TableCell>
288-
289-
{/* MAJOR */}
290-
<TableCell>{user.major}</TableCell>
291-
292-
{/* STUDENT ID */}
293-
<TableCell>{user.studentId}</TableCell>
294-
295-
{/* EMAIL */}
296-
<TableCell>
297-
<span className="text-sm">{user.email}</span>
298-
</TableCell>
299-
300-
{/* ROLE */}
301-
<TableCell>
302-
<div className="flex items-center gap-3">
303-
<Select
304-
aria-label="역할 수정"
305-
selectedKeys={new Set([user.userRole || ''])}
306-
onSelectionChange={(keys) => {
307-
const nextRole = String(Array.from(keys)[0] || user.userRole);
308-
if (nextRole !== user.userRole) {
309-
const ok = confirm(`역할을 '${user.userRole}' → '${nextRole}' 로 변경할까요?`);
310-
if (ok) void patchSmart({user, nextRole, nextTeam: user.team ?? null});
311-
}
312-
}}
313-
size="sm"
314-
className="min-w-[140px]"
315-
classNames={{
316-
trigger: 'bg-zinc-900 text-white border border-zinc-700 data-[hover=true]:bg-zinc-800',
317-
value: 'text-white',
318-
popoverContent: 'bg-zinc-900 border border-zinc-700',
319-
listbox: 'text-white',
320-
selectorIcon: 'text-zinc-400',
321-
}}
322-
itemClasses={{
323-
base: 'rounded-md data-[hover=true]:bg-zinc-800 data-[focus=true]:bg-zinc-800',
324-
title: 'text-white',
325-
}}
326-
>
327-
{ROLE_OPTIONS.map((r) => (<SelectItem key={r} value={r}>
328-
{r}
329-
</SelectItem>))}
330-
</Select>
331-
</div>
332-
</TableCell>
333-
334-
{/* TEAM */}
335-
<TableCell>
336-
<Select
337-
aria-label="팀 수정"
338-
selectedKeys={new Set([user.team || ''])}
339-
onSelectionChange={(keys) => {
340-
const k = String(Array.from(keys)[0] ?? '');
341-
const nextTeam = k === '' ? null : k; // 서버에는 enum name로 전송
342-
if ((nextTeam ?? null) !== (user.team ?? null)) {
343-
const old = user.team ? TEAM_LABEL[user.team] || user.team : '(없음)';
344-
const neu = nextTeam ? TEAM_LABEL[nextTeam] || nextTeam : '(없음)';
345-
const ok = confirm(`팀을 '${old}' → '${neu}' 로 변경할까요?`);
346-
if (ok) void patchSmart({user, nextRole: user.userRole, nextTeam});
347-
}
348-
}}
349-
size="sm"
350-
className="min-w-[160px]"
351-
classNames={{
352-
trigger: 'bg-zinc-900 text-white border border-zinc-700 data-[hover=true]:bg-zinc-800',
353-
value: 'text-white',
354-
popoverContent: 'bg-zinc-900 border border-zinc-700',
355-
listbox: 'text-white',
356-
selectorIcon: 'text-zinc-400',
357-
}}
358-
itemClasses={{
359-
base: 'rounded-md data-[hover=true]:bg-zinc-800 data-[focus=true]:bg-zinc-800',
360-
title: 'text-white',
361-
}}
362-
>
363-
<SelectItem key="" value="">
364-
(없음)
365-
</SelectItem>
366-
{TEAM_ENUM_VALUES.map((t) => (<SelectItem key={t} value={t}>
367-
{TEAM_LABEL[t]}
368-
</SelectItem>))}
369-
</Select>
370-
</TableCell>
371-
372-
{/* ACTIONS (Delete) */}
373-
{canRenderDelete ? (<TableCell>
374-
<div className="flex justify-end">
375-
{user.id !== me?.id ? (<Button
376-
size="sm"
377-
color="danger"
378-
variant="flat"
379-
onClick={(e) => {
380-
e.stopPropagation();
381-
void handleDelete(user);
382-
}}
383-
>
384-
삭제
385-
</Button>) : null}
386-
</div>
387-
</TableCell>) : null}
388-
</TableRow>)}
389-
</TableBody>
390-
</Table>
391-
</div>);
372+
<SelectItem key="" value="">
373+
(없음)
374+
</SelectItem>
375+
{TEAM_ENUM_VALUES.map((t) => (<SelectItem key={t} value={t}>
376+
{TEAM_LABEL[t]}
377+
</SelectItem>))}
378+
</Select>
379+
</TableCell>
380+
381+
{/* ACTIONS (Delete) */}
382+
{canRenderDelete ? (<TableCell>
383+
<div className="flex justify-end">
384+
{user.id !== me?.id ? (<Button
385+
size="sm"
386+
color="danger"
387+
variant="flat"
388+
onClick={(e) => {
389+
e.stopPropagation();
390+
void handleDelete(user);
391+
}}
392+
>
393+
삭제
394+
</Button>) : null}
395+
</div>
396+
</TableCell>) : null}
397+
</TableRow>)}
398+
</TableBody>
399+
</Table>
400+
</div>);
392401
}

0 commit comments

Comments
 (0)