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>
496 lines
15 KiB
JavaScript
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();
|
|
}); |