- 월별근무표: 휴일/근무일 관리, 자동 초기화 - 메일양식: 템플릿 CRUD, To/CC/BCC 설정 - 그룹정보: 부서 관리, 비트 연산 기반 권한 설정 - 업무일지: 수정 성공 메시지 제거, 오늘 근무시간 필터링 수정 - 웹소켓 메시지 type 충돌 버그 수정 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
534 lines
20 KiB
TypeScript
534 lines
20 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import {
|
|
Users,
|
|
Plus,
|
|
Edit2,
|
|
Trash2,
|
|
Save,
|
|
X,
|
|
Loader2,
|
|
RefreshCw,
|
|
Search,
|
|
Shield,
|
|
Check,
|
|
} from 'lucide-react';
|
|
import { comms } from '@/communication';
|
|
import { UserGroupItem, PermissionInfo } from '@/types';
|
|
|
|
const initialFormData: Partial<UserGroupItem> = {
|
|
dept: '',
|
|
path_kj: '',
|
|
permission: 0,
|
|
advpurchase: false,
|
|
advkisul: false,
|
|
managerinfo: '',
|
|
devinfo: '',
|
|
usemail: false,
|
|
};
|
|
|
|
// 비트 연산 헬퍼 함수
|
|
const getBit = (value: number, index: number): boolean => {
|
|
return ((value >> index) & 1) === 1;
|
|
};
|
|
|
|
const setBit = (value: number, index: number, flag: boolean): number => {
|
|
if (flag) {
|
|
return value | (1 << index);
|
|
} else {
|
|
return value & ~(1 << index);
|
|
}
|
|
};
|
|
|
|
export function UserGroupPage() {
|
|
const [loading, setLoading] = useState(false);
|
|
const [groups, setGroups] = useState<UserGroupItem[]>([]);
|
|
const [searchKey, setSearchKey] = useState('');
|
|
const [showModal, setShowModal] = useState(false);
|
|
const [showPermissionModal, setShowPermissionModal] = useState(false);
|
|
const [editingItem, setEditingItem] = useState<UserGroupItem | null>(null);
|
|
const [formData, setFormData] = useState<Partial<UserGroupItem>>(initialFormData);
|
|
const [permissionInfo, setPermissionInfo] = useState<PermissionInfo[]>([]);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
const loadData = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [groupsRes, permRes] = await Promise.all([
|
|
comms.getUserGroupList(),
|
|
comms.getPermissionInfo()
|
|
]);
|
|
|
|
if (groupsRes.Success && groupsRes.Data) {
|
|
setGroups(groupsRes.Data);
|
|
}
|
|
if (permRes.Success && permRes.Data) {
|
|
setPermissionInfo(permRes.Data);
|
|
}
|
|
} catch (error) {
|
|
console.error('데이터 로드 오류:', error);
|
|
alert('데이터를 불러오는 중 오류가 발생했습니다.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, [loadData]);
|
|
|
|
const filteredItems = groups.filter(item =>
|
|
!searchKey ||
|
|
item.dept?.toLowerCase().includes(searchKey.toLowerCase()) ||
|
|
item.managerinfo?.toLowerCase().includes(searchKey.toLowerCase())
|
|
);
|
|
|
|
const openAddModal = () => {
|
|
setEditingItem(null);
|
|
setFormData(initialFormData);
|
|
setShowModal(true);
|
|
};
|
|
|
|
const openEditModal = (item: UserGroupItem) => {
|
|
setEditingItem(item);
|
|
setFormData({
|
|
dept: item.dept || '',
|
|
path_kj: item.path_kj || '',
|
|
permission: item.permission || 0,
|
|
advpurchase: item.advpurchase || false,
|
|
advkisul: item.advkisul || false,
|
|
managerinfo: item.managerinfo || '',
|
|
devinfo: item.devinfo || '',
|
|
usemail: item.usemail || false,
|
|
});
|
|
setShowModal(true);
|
|
};
|
|
|
|
const openPermissionModal = (item: UserGroupItem) => {
|
|
setEditingItem(item);
|
|
setFormData({
|
|
...formData,
|
|
dept: item.dept,
|
|
permission: item.permission || 0,
|
|
});
|
|
setShowPermissionModal(true);
|
|
};
|
|
|
|
const handlePermissionChange = (index: number, checked: boolean) => {
|
|
const newPermission = setBit(formData.permission || 0, index, checked);
|
|
setFormData({ ...formData, permission: newPermission });
|
|
};
|
|
|
|
const handleSavePermission = async () => {
|
|
if (!editingItem) return;
|
|
|
|
setSaving(true);
|
|
try {
|
|
const response = await comms.editUserGroup(
|
|
editingItem.dept,
|
|
editingItem.dept,
|
|
editingItem.path_kj || '',
|
|
formData.permission || 0,
|
|
editingItem.advpurchase || false,
|
|
editingItem.advkisul || false,
|
|
editingItem.managerinfo || '',
|
|
editingItem.devinfo || '',
|
|
editingItem.usemail || false
|
|
);
|
|
|
|
if (response.Success) {
|
|
setShowPermissionModal(false);
|
|
loadData();
|
|
} else {
|
|
alert(response.Message || '저장에 실패했습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('저장 오류:', error);
|
|
alert('저장 중 오류가 발생했습니다.');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
if (!formData.dept?.trim()) {
|
|
alert('부서명을 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
setSaving(true);
|
|
try {
|
|
let response;
|
|
if (editingItem) {
|
|
response = await comms.editUserGroup(
|
|
editingItem.dept,
|
|
formData.dept || '',
|
|
formData.path_kj || '',
|
|
formData.permission || 0,
|
|
formData.advpurchase || false,
|
|
formData.advkisul || false,
|
|
formData.managerinfo || '',
|
|
formData.devinfo || '',
|
|
formData.usemail || false
|
|
);
|
|
} else {
|
|
response = await comms.addUserGroup(
|
|
formData.dept || '',
|
|
formData.path_kj || '',
|
|
formData.permission || 0,
|
|
formData.advpurchase || false,
|
|
formData.advkisul || false,
|
|
formData.managerinfo || '',
|
|
formData.devinfo || '',
|
|
formData.usemail || false
|
|
);
|
|
}
|
|
|
|
if (response.Success) {
|
|
setShowModal(false);
|
|
loadData();
|
|
} else {
|
|
alert(response.Message || '저장에 실패했습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('저장 오류:', error);
|
|
alert('저장 중 오류가 발생했습니다.');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (item: UserGroupItem) => {
|
|
if (!confirm(`"${item.dept}" 그룹을 삭제하시겠습니까?`)) return;
|
|
|
|
try {
|
|
const response = await comms.deleteUserGroup(item.dept);
|
|
if (response.Success) {
|
|
loadData();
|
|
} else {
|
|
alert(response.Message || '삭제에 실패했습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('삭제 오류:', error);
|
|
alert('삭제 중 오류가 발생했습니다.');
|
|
}
|
|
};
|
|
|
|
// 권한 카운트 계산
|
|
const getPermissionCount = (permission: number): number => {
|
|
let count = 0;
|
|
for (let i = 0; i < 11; i++) {
|
|
if (getBit(permission, i)) count++;
|
|
}
|
|
return count;
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* 헤더 */}
|
|
<div className="glass-effect rounded-2xl p-6">
|
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
|
<div className="flex items-center space-x-3">
|
|
<div className="p-3 bg-primary-500/20 rounded-xl">
|
|
<Users className="w-6 h-6 text-primary-400" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-white">그룹정보</h1>
|
|
<p className="text-white/60 text-sm">부서/그룹 및 권한 관리</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-3">
|
|
{/* 검색 */}
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-white/40" />
|
|
<input
|
|
type="text"
|
|
value={searchKey}
|
|
onChange={(e) => setSearchKey(e.target.value)}
|
|
placeholder="부서명 검색..."
|
|
className="pl-10 pr-4 py-2 bg-white/10 border border-white/10 rounded-lg text-white text-sm focus:outline-none focus:border-primary-500 w-48"
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
onClick={loadData}
|
|
disabled={loading}
|
|
className="flex items-center space-x-2 px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-white transition-colors disabled:opacity-50"
|
|
>
|
|
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
|
</button>
|
|
|
|
<button
|
|
onClick={openAddModal}
|
|
className="flex items-center space-x-2 px-4 py-2 bg-primary-500 hover:bg-primary-600 rounded-lg text-white transition-colors"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span>새 그룹</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 목록 */}
|
|
<div className="glass-effect rounded-2xl overflow-hidden">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-20">
|
|
<Loader2 className="w-8 h-8 text-white animate-spin" />
|
|
</div>
|
|
) : filteredItems.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-20 text-white/50">
|
|
<Users className="w-12 h-12 mb-4 opacity-50" />
|
|
<p>등록된 그룹이 없습니다.</p>
|
|
</div>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="bg-white/10">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">부서명</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">경로</th>
|
|
<th className="px-4 py-3 text-center text-xs font-medium text-white/70 uppercase w-24">권한</th>
|
|
<th className="px-4 py-3 text-center text-xs font-medium text-white/70 uppercase w-20">구매고급</th>
|
|
<th className="px-4 py-3 text-center text-xs font-medium text-white/70 uppercase w-20">기술고급</th>
|
|
<th className="px-4 py-3 text-center text-xs font-medium text-white/70 uppercase w-20">메일</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">관리자정보</th>
|
|
<th className="px-4 py-3 text-center text-xs font-medium text-white/70 uppercase w-28">작업</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-white/5">
|
|
{filteredItems.map((item, index) => (
|
|
<tr key={`${item.dept}-${index}`} className="hover:bg-white/5 transition-colors">
|
|
<td className="px-4 py-3 text-white font-medium">{item.dept}</td>
|
|
<td className="px-4 py-3 text-white/70 text-sm">{item.path_kj || '-'}</td>
|
|
<td className="px-4 py-3 text-center">
|
|
<button
|
|
onClick={() => openPermissionModal(item)}
|
|
className="inline-flex items-center space-x-1 px-2 py-1 bg-primary-500/20 text-primary-400 rounded text-xs hover:bg-primary-500/30 transition-colors"
|
|
>
|
|
<Shield className="w-3 h-3" />
|
|
<span>{getPermissionCount(item.permission || 0)}</span>
|
|
</button>
|
|
</td>
|
|
<td className="px-4 py-3 text-center">
|
|
{item.advpurchase && <Check className="w-4 h-4 text-success-400 mx-auto" />}
|
|
</td>
|
|
<td className="px-4 py-3 text-center">
|
|
{item.advkisul && <Check className="w-4 h-4 text-success-400 mx-auto" />}
|
|
</td>
|
|
<td className="px-4 py-3 text-center">
|
|
{item.usemail && <Check className="w-4 h-4 text-success-400 mx-auto" />}
|
|
</td>
|
|
<td className="px-4 py-3 text-white/70 text-sm truncate max-w-48">{item.managerinfo || '-'}</td>
|
|
<td className="px-4 py-3">
|
|
<div className="flex items-center justify-center space-x-2">
|
|
<button
|
|
onClick={() => openEditModal(item)}
|
|
className="p-1.5 hover:bg-white/10 rounded text-white/70 hover:text-white transition-colors"
|
|
title="수정"
|
|
>
|
|
<Edit2 className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDelete(item)}
|
|
className="p-1.5 hover:bg-danger-500/20 rounded text-white/70 hover:text-danger-400 transition-colors"
|
|
title="삭제"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 그룹 편집 모달 */}
|
|
{showModal && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
|
<div className="bg-slate-800 rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
|
|
{/* 모달 헤더 */}
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
|
<h2 className="text-xl font-bold text-white">
|
|
{editingItem ? '그룹 수정' : '새 그룹'}
|
|
</h2>
|
|
<button
|
|
onClick={() => setShowModal(false)}
|
|
className="p-2 hover:bg-white/10 rounded-lg text-white/70 hover:text-white transition-colors"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* 모달 내용 */}
|
|
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
|
{/* 부서명 */}
|
|
<div>
|
|
<label className="block text-white/70 text-sm mb-1">부서명 *</label>
|
|
<input
|
|
type="text"
|
|
value={formData.dept || ''}
|
|
onChange={(e) => setFormData({ ...formData, dept: e.target.value })}
|
|
className="w-full px-3 py-2 bg-white/10 border border-white/10 rounded-lg text-white focus:outline-none focus:border-primary-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* 경로 */}
|
|
<div>
|
|
<label className="block text-white/70 text-sm mb-1">경로 (path_kj)</label>
|
|
<input
|
|
type="text"
|
|
value={formData.path_kj || ''}
|
|
onChange={(e) => setFormData({ ...formData, path_kj: e.target.value })}
|
|
className="w-full px-3 py-2 bg-white/10 border border-white/10 rounded-lg text-white focus:outline-none focus:border-primary-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* 체크박스들 */}
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<label className="flex items-center space-x-2 text-white/70">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.advpurchase || false}
|
|
onChange={(e) => setFormData({ ...formData, advpurchase: e.target.checked })}
|
|
className="w-4 h-4 rounded"
|
|
/>
|
|
<span>구매고급</span>
|
|
</label>
|
|
<label className="flex items-center space-x-2 text-white/70">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.advkisul || false}
|
|
onChange={(e) => setFormData({ ...formData, advkisul: e.target.checked })}
|
|
className="w-4 h-4 rounded"
|
|
/>
|
|
<span>기술고급</span>
|
|
</label>
|
|
<label className="flex items-center space-x-2 text-white/70">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.usemail || false}
|
|
onChange={(e) => setFormData({ ...formData, usemail: e.target.checked })}
|
|
className="w-4 h-4 rounded"
|
|
/>
|
|
<span>메일 사용</span>
|
|
</label>
|
|
</div>
|
|
|
|
{/* 관리자 정보 */}
|
|
<div>
|
|
<label className="block text-white/70 text-sm mb-1">관리자 정보</label>
|
|
<textarea
|
|
value={formData.managerinfo || ''}
|
|
onChange={(e) => setFormData({ ...formData, managerinfo: e.target.value })}
|
|
rows={2}
|
|
className="w-full px-3 py-2 bg-white/10 border border-white/10 rounded-lg text-white focus:outline-none focus:border-primary-500 resize-none"
|
|
/>
|
|
</div>
|
|
|
|
{/* 개발자 정보 */}
|
|
<div>
|
|
<label className="block text-white/70 text-sm mb-1">개발자 정보</label>
|
|
<textarea
|
|
value={formData.devinfo || ''}
|
|
onChange={(e) => setFormData({ ...formData, devinfo: e.target.value })}
|
|
rows={2}
|
|
className="w-full px-3 py-2 bg-white/10 border border-white/10 rounded-lg text-white focus:outline-none focus:border-primary-500 resize-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 모달 푸터 */}
|
|
<div className="flex items-center justify-end space-x-3 px-6 py-4 border-t border-white/10">
|
|
<button
|
|
onClick={() => setShowModal(false)}
|
|
className="px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-white transition-colors"
|
|
>
|
|
취소
|
|
</button>
|
|
<button
|
|
onClick={handleSave}
|
|
disabled={saving}
|
|
className="flex items-center space-x-2 px-4 py-2 bg-primary-500 hover:bg-primary-600 rounded-lg text-white transition-colors disabled:opacity-50"
|
|
>
|
|
{saving ? (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
) : (
|
|
<Save className="w-4 h-4" />
|
|
)}
|
|
<span>저장</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 권한 설정 모달 */}
|
|
{showPermissionModal && editingItem && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
|
<div className="bg-slate-800 rounded-2xl w-full max-w-md max-h-[90vh] overflow-hidden flex flex-col">
|
|
{/* 모달 헤더 */}
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
|
<div>
|
|
<h2 className="text-xl font-bold text-white">권한 설정</h2>
|
|
<p className="text-white/60 text-sm">{editingItem.dept}</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setShowPermissionModal(false)}
|
|
className="p-2 hover:bg-white/10 rounded-lg text-white/70 hover:text-white transition-colors"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* 모달 내용 */}
|
|
<div className="flex-1 overflow-y-auto p-6">
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{permissionInfo.map((perm) => (
|
|
<label
|
|
key={perm.index}
|
|
className="flex items-center space-x-2 p-2 rounded-lg hover:bg-white/5 cursor-pointer"
|
|
title={perm.description}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={getBit(formData.permission || 0, perm.index)}
|
|
onChange={(e) => handlePermissionChange(perm.index, e.target.checked)}
|
|
className="w-4 h-4 rounded"
|
|
/>
|
|
<span className="text-white/80 text-sm">{perm.label}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 모달 푸터 */}
|
|
<div className="flex items-center justify-end space-x-3 px-6 py-4 border-t border-white/10">
|
|
<button
|
|
onClick={() => setShowPermissionModal(false)}
|
|
className="px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-white transition-colors"
|
|
>
|
|
취소
|
|
</button>
|
|
<button
|
|
onClick={handleSavePermission}
|
|
disabled={saving}
|
|
className="flex items-center space-x-2 px-4 py-2 bg-primary-500 hover:bg-primary-600 rounded-lg text-white transition-colors disabled:opacity-50"
|
|
>
|
|
{saving ? (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
) : (
|
|
<Save className="w-4 h-4" />
|
|
)}
|
|
<span>저장</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|