feat: 사용자 관리에 부서 정보 추가 및 편집 기능 구현

🏢 Department Management System:
- 5개 부서 지원: 생산, 품질, 구매, 설계, 영업
- 사용자 생성/수정 시 부서 선택 가능
- 부서별 사용자 분류 및 표시

📊 Database Schema Updates:
- department_type ENUM 추가 (production, quality, purchasing, design, sales)
- users 테이블에 department 컬럼 추가
- idx_users_department 인덱스 생성 (성능 최적화)
- 014_add_user_department.sql 마이그레이션 실행

🔧 Backend Enhancements:
- DepartmentType ENUM 클래스 추가 (models.py, schemas.py)
- User 모델에 department 필드 추가
- UserBase, UserUpdate 스키마에 department 필드 포함
- 기존 API 엔드포인트 자동 호환

🎨 Frontend UI Improvements:
- 사용자 추가 폼에 부서 선택 드롭다운 추가
- 사용자 목록에 부서 정보 배지 표시 (녹색 배경)
- 사용자 편집 모달 새로 구현
- 부서명 한글 변환 함수 (AuthAPI.getDepartmentLabel)

 User Management Features:
- 편집 버튼으로 사용자 정보 수정 가능
- 부서, 이름, 권한 실시간 변경
- 사용자 ID는 수정 불가 (읽기 전용)
- 모달 기반 직관적 UI

🔍 Visual Enhancements:
- 부서 정보 아이콘 (fas fa-building)
- 색상 코딩: 부서(녹색), 권한(빨강/파랑)
- 반응형 레이아웃 (flex-1, gap-3)
- 호버 효과 및 트랜지션

🚀 API Integration:
- AuthAPI.getDepartments() - 부서 목록 반환
- AuthAPI.getDepartmentLabel() - 부서명 변환
- AuthAPI.updateUser() - 부서 정보 포함 업데이트
- 기존 createUser API 확장 지원

Expected Result:
 사용자 생성 시 부서 선택 가능
 사용자 목록에 부서 정보 표시
 편집 버튼으로 부서 변경 가능
 5개 부서 분류 시스템 완성
 직관적인 사용자 관리 UI
This commit is contained in:
Hyungi Ahn
2025-10-25 13:29:47 +09:00
parent 7018d381b5
commit 236e1ca493
9 changed files with 279 additions and 9 deletions

View File

@@ -141,6 +141,18 @@
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">부서</label>
<select id="newDepartment" class="input-field w-full px-3 py-2 rounded-lg">
<option value="">부서 선택 (선택사항)</option>
<option value="production">생산</option>
<option value="quality">품질</option>
<option value="purchasing">구매</option>
<option value="design">설계</option>
<option value="sales">영업</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">권한</label>
<select id="newRole" class="input-field w-full px-3 py-2 rounded-lg">
@@ -256,6 +268,80 @@
</div>
</main>
<!-- 사용자 편집 모달 -->
<div id="editUserModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-xl max-w-md w-full p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">사용자 정보 수정</h3>
<button onclick="closeEditModal()" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<form id="editUserForm" class="space-y-4">
<input type="hidden" id="editUserId">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">사용자 ID</label>
<input
type="text"
id="editUsername"
class="input-field w-full px-3 py-2 rounded-lg bg-gray-100"
readonly
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">이름</label>
<input
type="text"
id="editFullName"
class="input-field w-full px-3 py-2 rounded-lg"
placeholder="실명 입력"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">부서</label>
<select id="editDepartment" class="input-field w-full px-3 py-2 rounded-lg">
<option value="">부서 선택 (선택사항)</option>
<option value="production">생산</option>
<option value="quality">품질</option>
<option value="purchasing">구매</option>
<option value="design">설계</option>
<option value="sales">영업</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">권한</label>
<select id="editRole" class="input-field w-full px-3 py-2 rounded-lg">
<option value="user">일반 사용자</option>
<option value="admin">관리자</option>
</select>
</div>
<div class="flex gap-3 pt-4">
<button
type="button"
onclick="closeEditModal()"
class="flex-1 px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400 transition-colors"
>
취소
</button>
<button
type="submit"
class="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
>
<i class="fas fa-save mr-2"></i>저장
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Scripts -->
<script src="/static/js/date-utils.js?v=20250917"></script>
<script src="/static/js/core/permissions.js?v=20251025"></script>
@@ -361,6 +447,7 @@
username: document.getElementById('newUsername').value.trim(),
full_name: document.getElementById('newFullName').value.trim(),
password: document.getElementById('newPassword').value,
department: document.getElementById('newDepartment').value || null,
role: document.getElementById('newRole').value
};
@@ -430,14 +517,19 @@
container.innerHTML = users.map(user => `
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div>
<div class="flex-1">
<div class="font-medium text-gray-800">
<i class="fas fa-user mr-2 text-gray-500"></i>
${user.full_name || user.username}
</div>
<div class="text-sm text-gray-600">
ID: ${user.username}
<span class="ml-2 px-2 py-0.5 rounded text-xs ${
<div class="text-sm text-gray-600 flex items-center gap-3">
<span>ID: ${user.username}</span>
${user.department ? `
<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-700">
<i class="fas fa-building mr-1"></i>${AuthAPI.getDepartmentLabel(user.department)}
</span>
` : ''}
<span class="px-2 py-0.5 rounded text-xs ${
user.role === 'admin'
? 'bg-red-100 text-red-700'
: 'bg-blue-100 text-blue-700'
@@ -447,6 +539,12 @@
</div>
</div>
<div class="flex gap-2">
<button
onclick="editUser(${user.id})"
class="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm"
>
<i class="fas fa-edit mr-1"></i>편집
</button>
<button
onclick="resetPassword('${user.username}')"
class="px-3 py-1 bg-yellow-500 text-white rounded hover:bg-yellow-600 transition-colors text-sm"
@@ -507,6 +605,53 @@
alert(error.message || '삭제에 실패했습니다.');
}
}
// 사용자 편집 모달 열기
function editUser(userId) {
const user = users.find(u => u.id === userId);
if (!user) {
alert('사용자를 찾을 수 없습니다.');
return;
}
// 모달 필드에 현재 값 설정
document.getElementById('editUserId').value = user.id;
document.getElementById('editUsername').value = user.username;
document.getElementById('editFullName').value = user.full_name || '';
document.getElementById('editDepartment').value = user.department || '';
document.getElementById('editRole').value = user.role;
// 모달 표시
document.getElementById('editUserModal').classList.remove('hidden');
}
// 사용자 편집 모달 닫기
function closeEditModal() {
document.getElementById('editUserModal').classList.add('hidden');
}
// 사용자 편집 폼 제출
document.getElementById('editUserForm').addEventListener('submit', async (e) => {
e.preventDefault();
const userId = document.getElementById('editUserId').value;
const userData = {
full_name: document.getElementById('editFullName').value.trim() || null,
department: document.getElementById('editDepartment').value || null,
role: document.getElementById('editRole').value
};
try {
await AuthAPI.updateUser(userId, userData);
alert('사용자 정보가 수정되었습니다.');
closeEditModal();
await loadUsers(); // 목록 새로고침
} catch (error) {
alert(error.message || '사용자 정보 수정에 실패했습니다.');
}
});
// 페이지 권한 관리 기능
let selectedUserId = null;

View File

@@ -432,7 +432,7 @@
<div class="flex-1">
<div class="flex items-center space-x-3 mb-2">
<span class="badge badge-${getStatusBadgeClass(issue.status)}">${getStatusText(issue.status)}</span>
${project ? `<span class="text-sm text-gray-500">${project.name}</span>` : ''}
${project ? `<span class="text-sm text-gray-500">${project.project_name}</span>` : ''}
<span class="text-sm text-gray-400">${completedDate}</span>
</div>
@@ -532,7 +532,7 @@
projects.forEach(project => {
const option = document.createElement('option');
option.value = project.id;
option.textContent = project.name;
option.textContent = project.project_name;
projectFilter.appendChild(option);
});
}

View File

@@ -449,7 +449,7 @@
<div class="flex items-center space-x-3 mb-2">
<span class="badge badge-${getStatusBadgeClass(issue.status)}">${getStatusText(issue.status)}</span>
${getPriorityBadge(issue.priority)}
${project ? `<span class="text-sm text-gray-500">${project.name}</span>` : ''}
${project ? `<span class="text-sm text-gray-500">${project.project_name}</span>` : ''}
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">${issue.description}</h3>
@@ -584,7 +584,7 @@
projects.forEach(project => {
const option = document.createElement('option');
option.value = project.id;
option.textContent = project.name;
option.textContent = project.project_name;
projectFilter.appendChild(option);
});
}

View File

@@ -168,7 +168,23 @@ const AuthAPI = {
body: JSON.stringify({
new_password: newPassword
})
})
}),
// 부서 목록 가져오기
getDepartments: () => [
{ value: 'production', label: '생산' },
{ value: 'quality', label: '품질' },
{ value: 'purchasing', label: '구매' },
{ value: 'design', label: '설계' },
{ value: 'sales', label: '영업' }
],
// 부서명 변환
getDepartmentLabel: (departmentValue) => {
const departments = AuthAPI.getDepartments();
const dept = departments.find(d => d.value === departmentValue);
return dept ? dept.label : departmentValue || '미지정';
}
};
// Issues API