diff --git a/user-management/api/controllers/userController.js b/user-management/api/controllers/userController.js index 2b00b40..792400e 100644 --- a/user-management/api/controllers/userController.js +++ b/user-management/api/controllers/userController.js @@ -24,7 +24,7 @@ async function getUsers(req, res, next) { */ async function createUser(req, res, next) { try { - const { username, password, name, full_name, department, department_id, role } = req.body; + const { username, password, name, full_name, department, department_id, role, hire_date } = req.body; if (!username || !password) { return res.status(400).json({ success: false, error: '사용자명과 비밀번호는 필수입니다' }); @@ -41,7 +41,8 @@ async function createUser(req, res, next) { name: name || full_name, department, department_id: department_id || null, - role + role, + hire_date: hire_date || null }); res.status(201).json({ success: true, data: user }); } catch (err) { diff --git a/user-management/api/index.js b/user-management/api/index.js index a56c161..eb1d747 100644 --- a/user-management/api/index.js +++ b/user-management/api/index.js @@ -90,8 +90,9 @@ app.use((err, req, res, next) => { // Startup: 마이그레이션 완료 후 서버 시작 async function start() { try { - const { runMigration } = require('./models/vacationSettingsModel'); + const { runMigration, runGenericMigration } = require('./models/vacationSettingsModel'); await runMigration(); + await runGenericMigration('20260323_add_resigned_date.sql'); } catch (err) { if (!['ER_DUP_FIELDNAME', 'ER_TABLE_EXISTS_ERROR', 'ER_DUP_KEYNAME'].includes(err.code)) { console.error('Fatal migration error:', err.message); diff --git a/user-management/api/models/userModel.js b/user-management/api/models/userModel.js index 5c438bc..eba6dd2 100644 --- a/user-management/api/models/userModel.js +++ b/user-management/api/models/userModel.js @@ -79,18 +79,18 @@ async function findById(userId) { async function findAll() { const db = getPool(); const [rows] = await db.query( - 'SELECT user_id, username, name, department, department_id, role, system1_access, system2_access, system3_access, is_active, last_login, created_at, hire_date FROM sso_users WHERE partner_company_id IS NULL ORDER BY user_id' + 'SELECT user_id, username, name, department, department_id, role, system1_access, system2_access, system3_access, is_active, last_login, created_at, hire_date, resigned_date FROM sso_users WHERE partner_company_id IS NULL ORDER BY user_id' ); return rows; } -async function create({ username, password, name, department, department_id, role }) { +async function create({ username, password, name, department, department_id, role, hire_date }) { const db = getPool(); const password_hash = await hashPassword(password); const [result] = await db.query( - `INSERT INTO sso_users (username, password_hash, name, department, department_id, role) - VALUES (?, ?, ?, ?, ?, ?)`, - [username, password_hash, name || null, department || null, department_id || null, role || 'user'] + `INSERT INTO sso_users (username, password_hash, name, department, department_id, role, hire_date) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [username, password_hash, name || null, department || null, department_id || null, role || 'user', hire_date || null] ); return findById(result.insertId); } @@ -109,6 +109,7 @@ async function update(userId, data) { if (data.system3_access !== undefined) { fields.push('system3_access = ?'); values.push(data.system3_access); } if (data.is_active !== undefined) { fields.push('is_active = ?'); values.push(data.is_active); } if (data.hire_date !== undefined) { fields.push('hire_date = ?'); values.push(data.hire_date || null); } + if (data.resigned_date !== undefined) { fields.push('resigned_date = ?'); values.push(data.resigned_date || null); } if (data.password) { fields.push('password_hash = ?'); values.push(await hashPassword(data.password)); @@ -126,7 +127,7 @@ async function update(userId, data) { async function deleteUser(userId) { const db = getPool(); - await db.query('UPDATE sso_users SET is_active = FALSE WHERE user_id = ?', [userId]); + await db.query('UPDATE sso_users SET is_active = FALSE, resigned_date = COALESCE(resigned_date, CURDATE()) WHERE user_id = ?', [userId]); } module.exports = { diff --git a/user-management/api/models/vacationSettingsModel.js b/user-management/api/models/vacationSettingsModel.js index e1ccd78..1b96e6a 100644 --- a/user-management/api/models/vacationSettingsModel.js +++ b/user-management/api/models/vacationSettingsModel.js @@ -59,4 +59,25 @@ async function runMigration() { console.log('[tkuser] Sprint 001 migration completed'); } -module.exports = { getAll, getByKey, updateSettings, loadAsObject, runMigration }; +async function runGenericMigration(filename) { + const db = getPool(); + const fs = require('fs'); + const path = require('path'); + const sqlFile = path.join(__dirname, '..', '..', 'migrations', filename); + const sql = fs.readFileSync(sqlFile, 'utf8'); + const statements = sql.split(';').map(s => s.trim()).filter(s => s.length > 0); + for (const stmt of statements) { + try { + await db.query(stmt); + } catch (err) { + if (['ER_DUP_FIELDNAME', 'ER_TABLE_EXISTS_ERROR', 'ER_DUP_KEYNAME'].includes(err.code)) { + // Already migrated — safe to ignore + } else { + throw err; + } + } + } + console.log(`[tkuser] Migration ${filename} completed`); +} + +module.exports = { getAll, getByKey, updateSettings, loadAsObject, runMigration, runGenericMigration }; diff --git a/user-management/migrations/20260323_add_resigned_date.sql b/user-management/migrations/20260323_add_resigned_date.sql new file mode 100644 index 0000000..f6c78e3 --- /dev/null +++ b/user-management/migrations/20260323_add_resigned_date.sql @@ -0,0 +1 @@ +ALTER TABLE sso_users ADD COLUMN IF NOT EXISTS resigned_date DATE NULL COMMENT '퇴사일'; diff --git a/user-management/web/index.html b/user-management/web/index.html index 5a485ca..4b37fa1 100644 --- a/user-management/web/index.html +++ b/user-management/web/index.html @@ -137,6 +137,10 @@ +
${err.message}
등록된 사용자가 없습니다.
'; return; } - c.innerHTML = users.map(u => ` -등록된 사용자가 없습니다.
'; } + else { c.innerHTML = activeUsers.map(u => renderUserRow(u, false)).join(''); } + + const resignedSection = document.getElementById('resignedSection'); + const resignedList = document.getElementById('resignedUserList'); + const resignedCount = document.getElementById('resignedCount'); + if (resignedUsers.length > 0) { + resignedSection.classList.remove('hidden'); + resignedCount.textContent = `(${resignedUsers.length}명)`; + resignedList.innerHTML = resignedUsers.map(u => renderUserRow(u, true)).join(''); + } else { + resignedSection.classList.add('hidden'); + } +} + +function toggleResignedList() { + const list = document.getElementById('resignedUserList'); + const btn = document.getElementById('resignedToggleBtn'); + if (list.classList.contains('hidden')) { + list.classList.remove('hidden'); + btn.innerHTML = '접기'; + } else { + list.classList.add('hidden'); + btn.innerHTML = '펼치기'; + } +} + +async function reactivateUser(id, name) { + if (!confirm(`${name}을(를) 재활성화하시겠습니까?`)) return; + try { + await api(`/users/${id}`, { method: 'PUT', body: JSON.stringify({ is_active: true, resigned_date: null }) }); + showToast('재활성화 완료'); + await loadUsers(); + } catch(e) { showToast(e.message, 'error'); } } document.getElementById('addUserForm').addEventListener('submit', async e => { e.preventDefault(); const deptIdVal = document.getElementById('newDepartmentId').value; + const hireDateVal = document.getElementById('newHireDate').value; try { - await api('/users', { method:'POST', body: JSON.stringify({ username: document.getElementById('newUsername').value.trim(), name: document.getElementById('newFullName').value.trim(), password: document.getElementById('newPassword').value, department_id: deptIdVal ? parseInt(deptIdVal) : null, role: document.getElementById('newRole').value }) }); - showToast('사용자가 추가되었습니다.'); document.getElementById('addUserForm').reset(); await loadUsers(); + await api('/users', { method:'POST', body: JSON.stringify({ username: document.getElementById('newUsername').value.trim(), name: document.getElementById('newFullName').value.trim(), password: document.getElementById('newPassword').value, department_id: deptIdVal ? parseInt(deptIdVal) : null, role: document.getElementById('newRole').value, hire_date: hireDateVal || null }) }); + showToast('사용자가 추가되었습니다.'); document.getElementById('addUserForm').reset(); document.getElementById('newHireDate').value = getSeoulToday(); await loadUsers(); } catch(e) { showToast(e.message,'error'); } }); @@ -167,6 +209,7 @@ function editUser(id) { document.getElementById('editDepartmentId').value=u.department_id||''; document.getElementById('editRole').value=u.role; document.getElementById('editHireDate').value = formatDate(u.hire_date); + document.getElementById('editResignedDate').value = formatDate(u.resigned_date); document.getElementById('editUserModal').classList.remove('hidden'); } function closeEditModal() { document.getElementById('editUserModal').classList.add('hidden'); } @@ -175,7 +218,7 @@ document.getElementById('editUserForm').addEventListener('submit', async e => { e.preventDefault(); const deptIdVal = document.getElementById('editDepartmentId').value; try { - await api(`/users/${document.getElementById('editUserId').value}`, { method:'PUT', body: JSON.stringify({ name: document.getElementById('editFullName').value.trim()||null, department_id: deptIdVal ? parseInt(deptIdVal) : null, role: document.getElementById('editRole').value, hire_date: document.getElementById('editHireDate').value || null }) }); + await api(`/users/${document.getElementById('editUserId').value}`, { method:'PUT', body: JSON.stringify({ name: document.getElementById('editFullName').value.trim()||null, department_id: deptIdVal ? parseInt(deptIdVal) : null, role: document.getElementById('editRole').value, hire_date: document.getElementById('editHireDate').value || null, resigned_date: document.getElementById('editResignedDate').value || null }) }); showToast('수정되었습니다.'); closeEditModal(); await loadUsers(); } catch(e) { showToast(e.message,'error'); } });