258 lines
8.7 KiB
TypeScript
258 lines
8.7 KiB
TypeScript
/**
|
|
* 관리자 설정 — 사용자 CRUD · 역할별 메뉴 권한
|
|
*/
|
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
import {
|
|
createAdminUser,
|
|
deleteAdminUser,
|
|
fetchRolePermissions,
|
|
listAdminUsers,
|
|
saveRolePermissions,
|
|
updateAdminUser,
|
|
type UserDto,
|
|
} from '../utils/backendApi';
|
|
import {
|
|
ALL_MENU_IDS,
|
|
MENU_LABELS,
|
|
ROLE_LABELS,
|
|
type UserRole,
|
|
} from '../utils/permissions';
|
|
|
|
const ROLES: UserRole[] = ['ADMIN', 'USER', 'GUEST'];
|
|
|
|
const AdminSettingsPanel: React.FC = () => {
|
|
const [users, setUsers] = useState<UserDto[]>([]);
|
|
const [rolePerms, setRolePerms] = useState<Record<string, Record<string, boolean>>>({});
|
|
const [permRole, setPermRole] = useState<UserRole>('USER');
|
|
const [loading, setLoading] = useState(true);
|
|
const [msg, setMsg] = useState<string | null>(null);
|
|
const [err, setErr] = useState<string | null>(null);
|
|
|
|
const [formMode, setFormMode] = useState<'create' | 'edit' | null>(null);
|
|
const [editId, setEditId] = useState<number | null>(null);
|
|
const [fUsername, setFUsername] = useState('');
|
|
const [fPassword, setFPassword] = useState('');
|
|
const [fDisplayName, setFDisplayName] = useState('');
|
|
const [fRole, setFRole] = useState<UserRole>('USER');
|
|
const [fEnabled, setFEnabled] = useState(true);
|
|
|
|
const refresh = useCallback(async () => {
|
|
setLoading(true);
|
|
setErr(null);
|
|
try {
|
|
const [u, p] = await Promise.all([listAdminUsers(), fetchRolePermissions()]);
|
|
setUsers(u);
|
|
setRolePerms(p);
|
|
} catch (e) {
|
|
setErr(e instanceof Error ? e.message : '데이터 로드 실패');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => { void refresh(); }, [refresh]);
|
|
|
|
const openCreate = () => {
|
|
setFormMode('create');
|
|
setEditId(null);
|
|
setFUsername('');
|
|
setFPassword('');
|
|
setFDisplayName('');
|
|
setFRole('USER');
|
|
setFEnabled(true);
|
|
};
|
|
|
|
const openEdit = (u: UserDto) => {
|
|
setFormMode('edit');
|
|
setEditId(u.id);
|
|
setFUsername(u.username);
|
|
setFPassword('');
|
|
setFDisplayName(u.displayName);
|
|
setFRole((u.role as UserRole) || 'USER');
|
|
setFEnabled(u.enabled);
|
|
};
|
|
|
|
const submitUser = async () => {
|
|
setErr(null);
|
|
setMsg(null);
|
|
try {
|
|
if (formMode === 'create') {
|
|
await createAdminUser({
|
|
username: fUsername.trim(),
|
|
password: fPassword,
|
|
displayName: fDisplayName.trim() || fUsername.trim(),
|
|
role: fRole,
|
|
enabled: fEnabled,
|
|
});
|
|
setMsg('사용자가 등록되었습니다.');
|
|
} else if (formMode === 'edit' && editId != null) {
|
|
await updateAdminUser(editId, {
|
|
displayName: fDisplayName.trim(),
|
|
role: fRole,
|
|
enabled: fEnabled,
|
|
...(fPassword ? { password: fPassword } : {}),
|
|
});
|
|
setMsg('사용자 정보가 저장되었습니다.');
|
|
}
|
|
setFormMode(null);
|
|
await refresh();
|
|
} catch (e) {
|
|
setErr(e instanceof Error ? e.message : '저장 실패');
|
|
}
|
|
};
|
|
|
|
const removeUser = async (id: number) => {
|
|
if (!window.confirm('이 사용자를 삭제하시겠습니까?')) return;
|
|
setErr(null);
|
|
try {
|
|
await deleteAdminUser(id);
|
|
setMsg('삭제되었습니다.');
|
|
await refresh();
|
|
} catch (e) {
|
|
setErr(e instanceof Error ? e.message : '삭제 실패');
|
|
}
|
|
};
|
|
|
|
const savePerms = async () => {
|
|
const perms = rolePerms[permRole];
|
|
if (!perms) return;
|
|
setErr(null);
|
|
try {
|
|
await saveRolePermissions(permRole, perms);
|
|
setMsg(`${ROLE_LABELS[permRole]} 역할 권한이 저장되었습니다.`);
|
|
await refresh();
|
|
} catch (e) {
|
|
setErr(e instanceof Error ? e.message : '권한 저장 실패');
|
|
}
|
|
};
|
|
|
|
const togglePerm = (menuId: string) => {
|
|
setRolePerms(prev => ({
|
|
...prev,
|
|
[permRole]: {
|
|
...(prev[permRole] ?? {}),
|
|
[menuId]: !(prev[permRole]?.[menuId]),
|
|
},
|
|
}));
|
|
};
|
|
|
|
if (loading && users.length === 0) {
|
|
return <p className="stg-loading">관리자 데이터 로딩 중…</p>;
|
|
}
|
|
|
|
return (
|
|
<div className="admin-settings">
|
|
{msg && <p className="admin-msg admin-msg--ok">{msg}</p>}
|
|
{err && <p className="admin-msg admin-msg--err">{err}</p>}
|
|
|
|
<section className="stg-section">
|
|
<h3 className="stg-section-title">사용자 관리</h3>
|
|
<div className="admin-toolbar">
|
|
<button type="button" className="stg-btn stg-btn--primary" onClick={openCreate}>
|
|
+ 사용자 등록
|
|
</button>
|
|
</div>
|
|
<div className="admin-table-wrap">
|
|
<table className="admin-table">
|
|
<thead>
|
|
<tr>
|
|
<th>아이디</th>
|
|
<th>표시명</th>
|
|
<th>역할</th>
|
|
<th>상태</th>
|
|
<th />
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{users.map(u => (
|
|
<tr key={u.id}>
|
|
<td>{u.username}</td>
|
|
<td>{u.displayName}</td>
|
|
<td><span className={`admin-role admin-role--${u.role.toLowerCase()}`}>{ROLE_LABELS[u.role as UserRole] ?? u.role}</span></td>
|
|
<td>{u.enabled ? '활성' : '비활성'}</td>
|
|
<td className="admin-actions">
|
|
<button type="button" className="admin-link" onClick={() => openEdit(u)}>수정</button>
|
|
<button type="button" className="admin-link admin-link--danger" onClick={() => removeUser(u.id)}>삭제</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
{formMode && (
|
|
<section className="stg-section admin-form-section">
|
|
<h3 className="stg-section-title">{formMode === 'create' ? '사용자 등록' : '사용자 수정'}</h3>
|
|
<div className="admin-form-grid">
|
|
{formMode === 'create' && (
|
|
<label>
|
|
<span>아이디</span>
|
|
<input value={fUsername} onChange={e => setFUsername(e.target.value)} />
|
|
</label>
|
|
)}
|
|
<label>
|
|
<span>{formMode === 'create' ? '비밀번호' : '새 비밀번호 (변경 시만)'}</span>
|
|
<input type="text" autoComplete="off" className="stg-input stg-input--sensitive" value={fPassword} onChange={e => setFPassword(e.target.value)} />
|
|
</label>
|
|
<label>
|
|
<span>표시명</span>
|
|
<input value={fDisplayName} onChange={e => setFDisplayName(e.target.value)} />
|
|
</label>
|
|
<label>
|
|
<span>역할</span>
|
|
<select value={fRole} onChange={e => setFRole(e.target.value as UserRole)}>
|
|
{ROLES.map(r => (
|
|
<option key={r} value={r}>{ROLE_LABELS[r]}</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label className="admin-check">
|
|
<input type="checkbox" checked={fEnabled} onChange={e => setFEnabled(e.target.checked)} />
|
|
<span>활성 계정</span>
|
|
</label>
|
|
</div>
|
|
<div className="admin-form-actions">
|
|
<button type="button" className="stg-btn stg-btn--primary" onClick={() => void submitUser()}>저장</button>
|
|
<button type="button" className="stg-btn" onClick={() => setFormMode(null)}>취소</button>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
<section className="stg-section">
|
|
<h3 className="stg-section-title">역할별 메뉴 접근 권한</h3>
|
|
<p className="stg-row-desc" style={{ marginBottom: 12 }}>
|
|
역할마다 상단 메뉴·설정 하위 메뉴·알림 접근을 제어합니다. 비로그인 사용자는 게스트(GUEST) 권한이 적용됩니다.
|
|
</p>
|
|
<div className="admin-perm-role-tabs">
|
|
{ROLES.map(r => (
|
|
<button
|
|
key={r}
|
|
type="button"
|
|
className={`admin-perm-tab${permRole === r ? ' active' : ''}`}
|
|
onClick={() => setPermRole(r)}
|
|
>{ROLE_LABELS[r]}</button>
|
|
))}
|
|
</div>
|
|
<div className="admin-perm-grid">
|
|
{ALL_MENU_IDS.map(menuId => (
|
|
<label key={menuId} className="admin-perm-item">
|
|
<input
|
|
type="checkbox"
|
|
checked={rolePerms[permRole]?.[menuId] === true}
|
|
onChange={() => togglePerm(menuId)}
|
|
/>
|
|
<span>{MENU_LABELS[menuId] ?? menuId}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
<button type="button" className="stg-btn stg-btn--primary" style={{ marginTop: 12 }} onClick={() => void savePerms()}>
|
|
{ROLE_LABELS[permRole]} 권한 저장
|
|
</button>
|
|
</section>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AdminSettingsPanel;
|