Files
tk-factory-services/system1-factory/web/js/manage-user.js
Hyungi Ahn abd7564e6b refactor: worker_id → user_id 전체 마이그레이션 (Phase 1-4)
sso_users.user_id를 단일 식별자로 통합. JWT에서 worker_id 제거,
department_id/is_production 추가. 백엔드 15개 모델, 11개 컨트롤러,
4개 서비스, 7개 라우트, 프론트엔드 32+ JS/11+ HTML 변환.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:13:10 +09:00

496 lines
15 KiB
JavaScript

import { API, getAuthHeaders, ensureAuthenticated } from '/js/api-config.js';
// 인증 확인
const token = ensureAuthenticated();
const accessLabels = {
worker: '작업자',
group_leader: '그룹장',
support_team: '지원팀',
admin: '관리자',
system: '시스템'
};
// 현재 사용자 정보 가져오기
const currentUser = JSON.parse(localStorage.getItem('sso_user') || '{}');
const isSystemUser = currentUser.access_level === 'system';
function createRow(item, cols, delHandler) {
const tr = document.createElement('tr');
cols.forEach(key => {
const td = document.createElement('td');
td.textContent = item[key] || '-';
tr.appendChild(td);
});
const delBtn = document.createElement('button');
delBtn.textContent = '삭제';
delBtn.className = 'btn-delete';
delBtn.onclick = () => delHandler(item);
const td = document.createElement('td');
td.appendChild(delBtn);
tr.appendChild(td);
return tr;
}
// 내 비밀번호 변경
const myPasswordForm = document.getElementById('myPasswordForm');
myPasswordForm?.addEventListener('submit', async e => {
e.preventDefault();
const currentPassword = document.getElementById('currentPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
// 새 비밀번호 확인
if (newPassword !== confirmPassword) {
alert('❌ 새 비밀번호가 일치하지 않습니다.');
return;
}
// 비밀번호 강도 검사
if (newPassword.length < 6) {
alert('❌ 비밀번호는 최소 6자 이상이어야 합니다.');
return;
}
try {
const res = await fetch(`${API}/auth/change-password`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
currentPassword,
newPassword
})
});
const result = await res.json();
if (res.ok && result.success) {
showToast('✅ 비밀번호가 변경되었습니다.');
myPasswordForm.reset();
// 3초 후 로그인 페이지로 이동
setTimeout(() => {
alert('비밀번호가 변경되어 다시 로그인해주세요.');
if (window.clearSSOAuth) window.clearSSOAuth();
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login';
}, 2000);
} else {
alert('❌ 비밀번호 변경 실패: ' + (result.error || '현재 비밀번호가 올바르지 않습니다.'));
}
} catch (error) {
console.error('Password change error:', error);
alert('🚨 서버 오류: ' + error.message);
}
});
// 시스템 권한자만 볼 수 있는 사용자 비밀번호 변경 섹션
if (isSystemUser) {
const systemCard = document.getElementById('systemPasswordChangeCard');
if (systemCard) {
systemCard.style.display = 'block';
}
// 사용자 비밀번호 변경 (시스템 권한자)
const userPasswordForm = document.getElementById('userPasswordForm');
userPasswordForm?.addEventListener('submit', async e => {
e.preventDefault();
const targetUserId = document.getElementById('targetUserId').value;
const newPassword = document.getElementById('targetNewPassword').value;
if (!targetUserId) {
alert('❌ 사용자를 선택해주세요.');
return;
}
if (newPassword.length < 6) {
alert('❌ 비밀번호는 최소 6자 이상이어야 합니다.');
return;
}
if (!confirm('정말로 이 사용자의 비밀번호를 변경하시겠습니까?')) {
return;
}
try {
const res = await fetch(`${API}/auth/admin/change-password`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
userId: targetUserId,
newPassword
})
});
const result = await res.json();
if (res.ok && result.success) {
showToast('✅ 사용자 비밀번호가 변경되었습니다.');
userPasswordForm.reset();
} else {
alert('❌ 비밀번호 변경 실패: ' + (result.error || '권한이 없습니다.'));
}
} catch (error) {
console.error('Admin password change error:', error);
alert('🚨 서버 오류: ' + error.message);
}
});
}
// 사용자 등록
const userForm = document.getElementById('userForm');
userForm?.addEventListener('submit', async e => {
e.preventDefault();
const body = {
username: document.getElementById('username').value.trim(),
password: document.getElementById('password').value.trim(),
name: document.getElementById('name').value.trim(),
access_level: document.getElementById('access_level').value,
user_id: document.getElementById('user_id').value || null
};
try {
const res = await fetch(`${API}/auth/register`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(body)
});
const result = await res.json();
if (res.ok && result.success) {
showToast('✅ 등록 완료');
userForm.reset();
loadUsers();
} else {
alert('❌ 실패: ' + (result.error || '알 수 없는 오류'));
}
} catch (error) {
console.error('Registration error:', error);
alert('🚨 서버 오류: ' + error.message);
}
});
async function loadUsers() {
const tbody = document.getElementById('userTableBody');
tbody.innerHTML = '<tr><td colspan="6">불러오는 중...</td></tr>';
try {
const res = await fetch(`${API}/auth/users`, {
headers: getAuthHeaders()
});
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const list = await res.json();
tbody.innerHTML = '';
if (Array.isArray(list)) {
// 시스템 권한자용 사용자 선택 옵션도 업데이트
if (isSystemUser) {
const targetUserSelect = document.getElementById('targetUserId');
if (targetUserSelect) {
targetUserSelect.innerHTML = '<option value="">사용자 선택</option>';
list.forEach(user => {
// 본인은 제외
if (user.user_id !== currentUser.user_id) {
const opt = document.createElement('option');
opt.value = user.user_id;
opt.textContent = `${user.name} (${user.username})`;
targetUserSelect.appendChild(opt);
}
});
}
}
list.forEach(item => {
item.access_level = accessLabels[item.access_level] || item.access_level;
item.user_id = item.user_id || '-';
// 행 생성
const tr = document.createElement('tr');
// 데이터 컬럼
['user_id', 'username', 'name', 'access_level', 'user_id'].forEach(key => {
const td = document.createElement('td');
td.textContent = item[key] || '-';
tr.appendChild(td);
});
// 작업 컬럼 (페이지 권한 버튼 + 삭제 버튼)
const actionTd = document.createElement('td');
// 페이지 권한 버튼 (Admin/System이 아닌 경우에만)
if (item.access_level !== '관리자' && item.access_level !== '시스템') {
const pageAccessBtn = document.createElement('button');
pageAccessBtn.textContent = '페이지 권한';
pageAccessBtn.className = 'btn btn-info btn-sm';
pageAccessBtn.style.marginRight = '5px';
pageAccessBtn.onclick = () => openPageAccessModal(item.user_id, item.username, item.name);
actionTd.appendChild(pageAccessBtn);
}
// 삭제 버튼
const delBtn = document.createElement('button');
delBtn.textContent = '삭제';
delBtn.className = 'btn-delete';
delBtn.onclick = async () => {
if (!confirm('삭제하시겠습니까?')) return;
try {
const delRes = await fetch(`${API}/auth/users/${item.user_id}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (delRes.ok) {
showToast('✅ 삭제 완료');
loadUsers();
} else {
alert('❌ 삭제 실패');
}
} catch (error) {
alert('🚨 삭제 중 오류 발생');
}
};
actionTd.appendChild(delBtn);
tr.appendChild(actionTd);
tbody.appendChild(tr);
});
} else {
tbody.innerHTML = '<tr><td colspan="6">데이터 형식 오류</td></tr>';
}
} catch (error) {
console.error('Load users error:', error);
tbody.innerHTML = '<tr><td colspan="6">로드 실패: ' + error.message + '</td></tr>';
}
}
async function loadWorkerOptions() {
const select = document.getElementById('user_id');
if (!select) return;
try {
const res = await fetch(`${API}/workers`, {
headers: getAuthHeaders()
});
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const allWorkers = await res.json();
// 활성화된 작업자만 필터링
const workers = allWorkers.filter(worker => {
return worker.status === 'active' || worker.is_active === 1 || worker.is_active === true;
});
if (Array.isArray(workers)) {
workers.forEach(w => {
const opt = document.createElement('option');
opt.value = w.user_id;
opt.textContent = `${w.worker_name} (${w.user_id})`;
select.appendChild(opt);
});
}
} catch (error) {
console.warn('작업자 목록 불러오기 실패:', error);
}
}
// showToast → api-base.js 전역 사용
// ========== 페이지 접근 권한 관리 ==========
let currentEditingUserId = null;
let currentUserPageAccess = [];
/**
* 페이지 권한 관리 모달 열기
*/
async function openPageAccessModal(userId, username, name) {
currentEditingUserId = userId;
const modal = document.getElementById('pageAccessModal');
const modalUserInfo = document.getElementById('modalUserInfo');
const modalUserRole = document.getElementById('modalUserRole');
modalUserInfo.textContent = `${name} (${username})`;
modalUserRole.textContent = `사용자 ID: ${userId}`;
try {
// 사용자의 페이지 접근 권한 조회
const res = await fetch(`${API}/users/${userId}/page-access`, {
headers: getAuthHeaders()
});
if (!res.ok) {
throw new Error('페이지 접근 권한을 불러오는데 실패했습니다.');
}
const result = await res.json();
if (result.success) {
currentUserPageAccess = result.data.pageAccess;
renderPageAccessList(result.data.pageAccess);
modal.style.display = 'block';
} else {
throw new Error(result.error || '데이터 로드 실패');
}
} catch (error) {
console.error('페이지 권한 로드 오류:', error);
alert('❌ 페이지 권한을 불러오는데 실패했습니다: ' + error.message);
}
}
/**
* 페이지 접근 권한 목록 렌더링
*/
function renderPageAccessList(pageAccess) {
const categories = {
dashboard: document.getElementById('dashboardPageList'),
management: document.getElementById('managementPageList'),
common: document.getElementById('commonPageList')
};
// 카테고리별로 초기화
Object.values(categories).forEach(el => {
if (el) el.innerHTML = '';
});
// 카테고리별로 그룹화
const grouped = pageAccess.reduce((acc, page) => {
if (!acc[page.category]) acc[page.category] = [];
acc[page.category].push(page);
return acc;
}, {});
// 각 카테고리별로 렌더링
Object.keys(grouped).forEach(category => {
const container = categories[category];
if (!container) return;
grouped[category].forEach(page => {
const pageItem = document.createElement('div');
pageItem.className = 'page-item';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `page_${page.page_id}`;
checkbox.checked = page.can_access === 1 || page.can_access === true;
checkbox.dataset.pageId = page.page_id;
const label = document.createElement('label');
label.htmlFor = `page_${page.page_id}`;
label.textContent = page.page_name;
const pathSpan = document.createElement('span');
pathSpan.className = 'page-path';
pathSpan.textContent = page.page_path;
pageItem.appendChild(checkbox);
pageItem.appendChild(label);
pageItem.appendChild(pathSpan);
container.appendChild(pageItem);
});
});
}
/**
* 페이지 권한 변경 사항 저장
*/
async function savePageAccessChanges() {
if (!currentEditingUserId) {
alert('사용자 정보가 없습니다.');
return;
}
// 모든 체크박스 상태 가져오기
const checkboxes = document.querySelectorAll('.page-item input[type="checkbox"]');
const pageAccessUpdates = {};
checkboxes.forEach(checkbox => {
const pageId = parseInt(checkbox.dataset.pageId);
const canAccess = checkbox.checked;
pageAccessUpdates[pageId] = canAccess;
});
try {
// 변경된 페이지 권한을 서버로 전송
const pageIds = Object.keys(pageAccessUpdates).map(id => parseInt(id));
const canAccessValues = pageIds.map(id => pageAccessUpdates[id]);
// 접근 가능한 페이지
const accessiblePages = pageIds.filter((id, index) => canAccessValues[index]);
// 접근 불가능한 페이지
const inaccessiblePages = pageIds.filter((id, index) => !canAccessValues[index]);
// 접근 가능 페이지 업데이트
if (accessiblePages.length > 0) {
await fetch(`${API}/users/${currentEditingUserId}/page-access`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
pageIds: accessiblePages,
canAccess: true
})
});
}
// 접근 불가능 페이지 업데이트
if (inaccessiblePages.length > 0) {
await fetch(`${API}/users/${currentEditingUserId}/page-access`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
pageIds: inaccessiblePages,
canAccess: false
})
});
}
showToast('✅ 페이지 접근 권한이 저장되었습니다.');
closePageAccessModal();
} catch (error) {
console.error('페이지 권한 저장 오류:', error);
alert('❌ 페이지 권한 저장에 실패했습니다: ' + error.message);
}
}
/**
* 페이지 권한 관리 모달 닫기
*/
function closePageAccessModal() {
const modal = document.getElementById('pageAccessModal');
modal.style.display = 'none';
currentEditingUserId = null;
currentUserPageAccess = [];
}
// 모달 닫기 버튼 이벤트
document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById('pageAccessModal');
const closeBtn = modal?.querySelector('.close');
if (closeBtn) {
closeBtn.onclick = closePageAccessModal;
}
// 모달 외부 클릭 시 닫기
window.onclick = (event) => {
if (event.target === modal) {
closePageAccessModal();
}
};
});
// 전역 함수로 노출
window.openPageAccessModal = openPageAccessModal;
window.closePageAccessModal = closePageAccessModal;
window.savePageAccessChanges = savePageAccessChanges;
window.addEventListener('DOMContentLoaded', () => {
loadUsers();
loadWorkerOptions();
});