This commit is contained in:
NAS-Admin 2026-02-09 01:04:41 +09:00
parent 5e7a53e731
commit 3e1cd3adab
5 changed files with 321 additions and 18 deletions

100
api.php
View file

@ -488,6 +488,15 @@ last_confirmed_date
$old_stmt->execute([$data['id']]); $old_stmt->execute([$data['id']]);
$old_asset = $old_stmt->fetch(); $old_asset = $old_stmt->fetch();
// Auto-relocate based on status if needed (optional)
// If assigned_user_name is missing but current_user_id exists, try to fill it for logging
if (empty($data['assigned_user_name']) && !empty($data['current_user_id'])) {
$u_stmt = $db->prepare("SELECT name FROM users WHERE id = ?");
$u_stmt->execute([$data['current_user_id']]);
$data['assigned_user_name'] = $u_stmt->fetchColumn() ?: '';
}
$stmt = $db->prepare("UPDATE laptop_assets SET $stmt = $db->prepare("UPDATE laptop_assets SET
asset_tag = ?, model_id = ?, current_user_id = ?, status = ?, serial_number = ?, purchase_date = ?, ip_address = ?, asset_tag = ?, model_id = ?, current_user_id = ?, status = ?, serial_number = ?, purchase_date = ?, ip_address = ?,
cpu = ?, npu = ?, hdd0_model = ?, hdd0_capacity = ?, hdd1_model = ?, hdd1_capacity = ?, ram = ?, cpu = ?, npu = ?, hdd0_model = ?, hdd0_capacity = ?, hdd1_model = ?, hdd1_capacity = ?, ram = ?,
@ -523,10 +532,11 @@ WHERE id = ?");
]); ]);
// Auto-log assignment change // Auto-log assignment change
if ($data['assigned_user_name'] && $data['assigned_user_name'] !== ($old_asset['assigned_user_name'] ?? '')) { $new_name = $data['assigned_user_name'] ?: '';
$log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date) VALUES (?, $old_name = $old_asset['assigned_user_name'] ?? '';
'assignment', ?, ?)"); if ($new_name !== $old_name) {
$log_stmt->execute([$data['id'], $data['assigned_user_name'], date('Y-m-d')]); $log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date) VALUES (?, 'assignment', ?, ?)");
$log_stmt->execute([$data['id'], $new_name ?: '재고(반납)', date('Y-m-d')]);
} }
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
@ -632,6 +642,11 @@ LIMIT 1")->fetchColumn();
if ($newStatus) { if ($newStatus) {
$updates[] = "status = ?"; $updates[] = "status = ?";
$params[] = $newStatus; $params[] = $newStatus;
// 만약 상태를 'stock'(재고)으로 변경하는 경우 배정 정보도 초기화
if ($newStatus === 'stock') {
$updates[] = "current_user_id = NULL";
$updates[] = "assigned_user_name = NULL";
}
} }
if ($newModelId) { if ($newModelId) {
$updates[] = "model_id = ?"; $updates[] = "model_id = ?";
@ -799,6 +814,83 @@ LIMIT 1")->fetchColumn();
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
break; break;
case 'archive_users':
$data = json_decode(file_get_contents('php://input'), true);
$userIds = $data['user_ids'];
if (empty($userIds)) {
echo json_encode(['success' => false, 'error' => 'No users selected']);
break;
}
$db->beginTransaction();
try {
$placeholders = implode(',', array_fill(0, count($userIds), '?'));
// Copy to archive
$db->prepare("INSERT INTO users_archive (id, emp_id, name, department_id, position, email, phone, mobile, accounting_type, status)
SELECT id, emp_id, name, department_id, position, email, phone, mobile, accounting_type, status FROM users WHERE id IN ($placeholders)")
->execute($userIds);
// Delete from original
$db->prepare("DELETE FROM users WHERE id IN ($placeholders)")->execute($userIds);
$db->commit();
echo json_encode(['success' => true]);
} catch (Exception $e) {
$db->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
break;
case 'get_archived_users':
$query = "SELECT u.*, dp.path as dept_name
FROM users_archive u
LEFT JOIN (
WITH RECURSIVE dept_path(id, path) AS (
SELECT id, name FROM departments WHERE parent_id IS NULL
UNION ALL
SELECT d.id, dp.path || ' > ' || d.name
FROM departments d JOIN dept_path dp ON d.parent_id = dp.id
) SELECT * FROM dept_path
) dp ON u.department_id = dp.id
ORDER BY u.archived_at DESC";
echo json_encode($db->query($query)->fetchAll());
break;
case 'restore_users':
$data = json_decode(file_get_contents('php://input'), true);
$userIds = $data['user_ids'];
if (empty($userIds)) {
echo json_encode(['success' => false, 'error' => 'No users selected']);
break;
}
$db->beginTransaction();
try {
$placeholders = implode(',', array_fill(0, count($userIds), '?'));
// Restore to users
$db->prepare("INSERT INTO users (id, emp_id, name, department_id, position, email, phone, mobile, accounting_type, status)
SELECT id, emp_id, name, department_id, position, email, phone, mobile, accounting_type, status FROM users_archive WHERE id IN ($placeholders)")
->execute($userIds);
// Delete from archive
$db->prepare("DELETE FROM users_archive WHERE id IN ($placeholders)")->execute($userIds);
$db->commit();
echo json_encode(['success' => true]);
} catch (Exception $e) {
$db->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
break;
case 'bulk_delete_users':
$data = json_decode(file_get_contents('php://input'), true);
$userIds = $data['user_ids'];
if (empty($userIds)) {
echo json_encode(['success' => false, 'error' => 'No users selected']);
break;
}
$placeholders = implode(',', array_fill(0, count($userIds), '?'));
$db->prepare("DELETE FROM users WHERE id IN ($placeholders)")->execute($userIds);
echo json_encode(['success' => true]);
break;
case 'get_general_rentals': case 'get_general_rentals':
$query = "SELECT r.*, GROUP_CONCAT(i.item_name, ', ') as items $query = "SELECT r.*, GROUP_CONCAT(i.item_name, ', ') as items
FROM general_rentals r FROM general_rentals r

BIN
assets.db

Binary file not shown.

9
fix_missing_column.php Normal file
View file

@ -0,0 +1,9 @@
<?php
require_once 'config.php';
try {
$db->exec("ALTER TABLE laptop_assets ADD COLUMN last_confirmed_date TEXT");
echo "Column 'last_confirmed_date' added successfully to laptop_assets table.\n";
} catch (Exception $e) {
echo "Error or column already exists: " . $e->getMessage() . "\n";
}
?>

View file

@ -38,6 +38,18 @@
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-blue-500 transition-colors"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input type="text" x-model="searchQuery"
class="block w-64 pl-10 pr-3 py-2.5 border border-slate-200 rounded-xl leading-5 bg-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all text-sm shadow-sm"
placeholder="자산번호, 모델명, 사용자, IP 검색...">
</div>
<button @click="exportToExcel()" <button @click="exportToExcel()"
class="bg-white border border-slate-200 text-slate-700 px-5 py-2.5 rounded-xl font-bold hover:bg-slate-50 transition-all flex items-center shadow-sm text-sm"> class="bg-white border border-slate-200 text-slate-700 px-5 py-2.5 rounded-xl font-bold hover:bg-slate-50 transition-all flex items-center shadow-sm text-sm">
<svg class="w-4 h-4 mr-2 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 mr-2 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -94,7 +106,7 @@
<!-- Laptop List Table --> <!-- Laptop List Table -->
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden" <div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden"
x-show="!loading && assets.length > 0"> x-show="!loading && filteredAssets.length > 0">
<table class="min-w-full divide-y divide-slate-100"> <table class="min-w-full divide-y divide-slate-100">
<thead class="bg-slate-50/50"> <thead class="bg-slate-50/50">
<tr> <tr>
@ -205,9 +217,14 @@
<div x-show="loading" class="p-20 flex justify-center"> <div x-show="loading" class="p-20 flex justify-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div> <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div> </div>
<div x-show="!loading && assets.length === 0" <div x-show="!loading && filteredAssets.length === 0"
class="p-20 text-center bg-white rounded-3xl border border-dashed border-slate-300"> class="p-20 text-center bg-white rounded-3xl border border-dashed border-slate-300">
<p class="text-slate-400 font-medium italic">등록된 노트북 자산이 없습니다.</p> <template x-if="searchQuery">
<p class="text-slate-400 font-medium italic">검색 결과와 일치하는 자산이 없습니다.</p>
</template>
<template x-if="!searchQuery">
<p class="text-slate-400 font-medium italic">등록된 노트북 자산이 없습니다.</p>
</template>
</div> </div>
</main> </main>
@ -263,7 +280,7 @@
<div> <div>
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">배정 <label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">배정
사용자</label> 사용자</label>
<select x-model="formData.current_user_id" <select x-model="formData.current_user_id" @change="updateAssignedUserName()"
class="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700"> class="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700">
<option value="">미배정 (재고)</option> <option value="">미배정 (재고)</option>
<template x-for="user in allUsers" :key="user.id"> <template x-for="user in allUsers" :key="user.id">
@ -398,6 +415,7 @@
selectedIds: [], selectedIds: [],
bulkStatus: '', bulkStatus: '',
bulkModelId: '', bulkModelId: '',
searchQuery: '',
sortKey: 'asset_tag', sortKey: 'asset_tag',
sortOrder: 'asc', sortOrder: 'asc',
formData: { formData: {
@ -458,8 +476,23 @@
}); });
}, },
get filteredAssets() {
let result = [...this.assets];
if (this.searchQuery) {
const q = this.searchQuery.toLowerCase();
result = result.filter(a => (a.asset_tag && a.asset_tag.toLowerCase().includes(q)) ||
(a.model_name && a.model_name.toLowerCase().includes(q)) ||
(a.manufacturer && a.manufacturer.toLowerCase().includes(q)) ||
(a.user_name && a.user_name.toLowerCase().includes(q)) ||
(a.ip_address && a.ip_address.toLowerCase().includes(q)) ||
(a.serial_number && a.serial_number.toLowerCase().includes(q))
);
}
return result;
},
get sortedAssets() { get sortedAssets() {
return [...this.assets].sort((a, b) => { return this.filteredAssets.sort((a, b) => {
let v1 = a[this.sortKey] || ''; let v1 = a[this.sortKey] || '';
let v2 = b[this.sortKey] || ''; let v2 = b[this.sortKey] || '';
if (this.sortOrder === 'asc') return v1 > v2 ? 1 : -1; if (this.sortOrder === 'asc') return v1 > v2 ? 1 : -1;
@ -533,18 +566,35 @@
body: JSON.stringify(this.formData) body: JSON.stringify(this.formData)
}).then(res => res.json()).then(data => { }).then(res => res.json()).then(data => {
if (data.success) { if (data.success) {
window.showAlert(this.isEdit ? '자산 정보가 수정되었습니다.' : '새 자산이 등록되었습니다.', '성공', 'success');
this.showModal = false; this.showModal = false;
this.fetchAssets(); this.fetchAssets();
} else {
window.showAlert(data.error || '처리 중 오류가 발생했습니다.', '오류', 'error');
} }
}).catch(err => {
console.error(err);
window.showAlert('서버 통신 오류가 발생했습니다.', '시스템 오류', 'error');
}); });
}, },
updateAssignedUserName() {
if (!this.formData.current_user_id) {
this.formData.assigned_user_name = '';
return;
}
const user = this.allUsers.find(u => u.id == this.formData.current_user_id);
if (user) {
this.formData.assigned_user_name = user.name;
}
},
exportToExcel() { exportToExcel() {
if (this.assets.length === 0) return; if (this.assets.length === 0) return;
const headers = ['자산관리번호', '모델명', '제조사', '프로세서', 'RAM', 'SSD', 'IP주소', '시리얼', '사용자', '상태', '취득일']; const headers = ['자산관리번호', '모델명', '제조사', '프로세서', 'RAM', 'SSD', 'IP주소', '시리얼', '사용자', '상태', '취득일'];
const rows = this.assets.map(a => [ const rows = this.assets.map(a => [
a.asset_tag, a.model_name, a.manufacturer, a.processor || '', a.ram || '', a.storage || '', a.asset_tag, a.model_name, a.manufacturer, a.processor || '', a.ram || '', a.storage || '',
a.ip_address || '', a.serial_number || '', a.user_name || '미배정', a.status === 'assigned' ? '지급됨' : '재고', a.purchase_date || '' a.ip_address || '', a.serial_number || '', a.user_name || '미배정', a.status === 'assigned' ? '지급됨' : '재고', a.purchase_date || ''
]); ]);
let csvContent = "\uFEFF" + headers.join(",") + "\n" + rows.map(r => r.map(c => `"${c}"`).join(",")).join("\n"); let csvContent = "\uFEFF" + headers.join(",") + "\n" + rows.map(r => r.map(c => `"${c}"`).join(",")).join("\n");
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });

168
users.php
View file

@ -71,14 +71,24 @@
class="block w-64 pl-10 pr-3 py-2.5 border border-slate-200 rounded-xl leading-5 bg-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all text-sm" class="block w-64 pl-10 pr-3 py-2.5 border border-slate-200 rounded-xl leading-5 bg-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all text-sm"
placeholder="이름, 사번, 이메일 검색..."> placeholder="이름, 사번, 이메일 검색...">
</div> </div>
<button @click="openAddModal()" <div class="flex items-center gap-2">
class="bg-blue-600 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all flex items-center shrink-0"> <button @click="openAddModal()"
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> class="bg-blue-600 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all flex items-center shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
</svg> d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
신규 등록 </svg>
</button> 신규 등록
</button>
<button @click="openRestoreModal()"
class="bg-slate-800 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-slate-200 hover:bg-slate-900 transition-all flex items-center shrink-0">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
불러오기
</button>
</div>
</div> </div>
</header> </header>
@ -139,6 +149,26 @@
<button @click="applyBulkUpdate" <button @click="applyBulkUpdate"
class="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-blue-500/20 active:scale-95">일괄 class="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-blue-500/20 active:scale-95">일괄
적용</button> 적용</button>
<div class="h-8 w-px bg-white/10 mx-1"></div>
<button @click="archiveUsers"
class="bg-emerald-600 hover:bg-emerald-700 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-emerald-500/20 active:scale-95 flex items-center gap-2">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
</svg>
내보내기
</button>
<button @click="bulkDelete"
class="bg-rose-600 hover:bg-rose-700 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-rose-500/20 active:scale-95 flex items-center gap-2">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
삭제
</button>
<button @click="selectedIds = []" <button @click="selectedIds = []"
class="ml-2 w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 text-slate-400 hover:text-white transition-all"> class="ml-2 w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 text-slate-400 hover:text-white transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -532,6 +562,67 @@
</div> </div>
</div> </div>
<!-- Restore (Import) Modal -->
<div x-show="showRestoreModal" x-cloak class="fixed inset-0 z-[120] flex items-center justify-center p-4">
<div class="fixed inset-0 modal-bg" @click="showRestoreModal = false"></div>
<div
class="bg-white rounded-[2.5rem] p-8 max-w-2xl w-full relative z-[121] shadow-2xl overflow-hidden flex flex-col max-h-[85vh]">
<div class="flex justify-between items-start mb-6">
<div>
<h3 class="text-2xl font-black text-slate-900">아카이브에서 불러오기</h3>
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mt-1">Select Archived Employees
to Restore</p>
</div>
<button @click="showRestoreModal = false" class="text-slate-300 hover:text-slate-900 transition-colors">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex-1 overflow-y-auto pr-2 no-scrollbar">
<template x-if="archivedUsers.length === 0">
<div class="text-center py-20">
<div class="w-16 h-16 bg-slate-50 rounded-2xl flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-slate-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
</svg>
</div>
<p class="text-slate-400 font-bold">내보내기된 직원이 없습니다.</p>
</div>
</template>
<div class="grid grid-cols-1 gap-3">
<template x-for="user in archivedUsers" :key="user.id">
<div
class="p-4 bg-slate-50 border border-slate-100 rounded-2xl flex items-center justify-between group hover:bg-white hover:shadow-lg hover:border-blue-200 transition-all">
<div class="flex items-center gap-4">
<div
class="w-10 h-10 bg-white rounded-xl flex items-center justify-center shadow-sm text-slate-400 group-hover:text-blue-500 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<div>
<div class="text-sm font-black text-slate-900" x-text="user.name"></div>
<div class="text-[10px] font-bold text-slate-400"
x-text="user.dept_name + ' / ' + user.position"></div>
</div>
</div>
<button @click="restoreUser(user.id)"
class="px-4 py-2 bg-slate-900 text-white text-[11px] font-black rounded-xl hover:bg-blue-600 transition-all active:scale-95">
불러오기
</button>
</div>
</template>
</div>
</div>
</div>
</div>
<script> <script>
function userManagement() { function userManagement() {
return { return {
@ -551,7 +642,9 @@
bulkDeptId: '', bulkDeptId: '',
bulkPosition: '', bulkPosition: '',
showRentalListModal: false, showRentalListModal: false,
showRestoreModal: false,
userRentals: [], userRentals: [],
archivedUsers: [],
formData: { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active', phone: '' }, formData: { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active', phone: '' },
init() { init() {
@ -765,6 +858,65 @@
resetForm() { resetForm() {
this.isEdit = false; this.isEdit = false;
this.formData = { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active', phone: '' }; this.formData = { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active', phone: '' };
},
archiveUsers() {
window.showConfirm(`선택한 ${this.selectedIds.length}명의 직원을 내보내기 하시겠습니까?\n내보내기된 정보는 '불러오기' 메뉴에서 확인 가능합니다.`, () => {
fetch('api.php?action=archive_users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_ids: this.selectedIds })
}).then(res => res.json()).then(data => {
if (data.success) {
window.showAlert('선택한 인원이 아카이브로 내보내기 되었습니다.', '내보내기 완료', 'success');
this.selectedIds = [];
this.fetchUsers();
}
});
}, '내보내기 확인');
},
bulkDelete() {
window.showConfirm(`선택한 ${this.selectedIds.length}명의 직원을 영구 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.`, () => {
fetch('api.php?action=bulk_delete_users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_ids: this.selectedIds })
}).then(res => res.json()).then(data => {
if (data.success) {
window.showAlert('영구 삭제되었습니다.', '삭제 완료', 'success');
this.selectedIds = [];
this.fetchUsers();
}
});
}, '영구 삭제 확인');
},
openRestoreModal() {
this.fetchArchivedUsers();
},
fetchArchivedUsers() {
fetch('api.php?action=get_archived_users')
.then(res => res.json())
.then(data => {
this.archivedUsers = data;
this.showRestoreModal = true;
});
},
restoreUser(id) {
fetch('api.php?action=restore_users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_ids: [id] })
}).then(res => res.json()).then(data => {
if (data.success) {
window.showAlert('직원 정보가 복구되었습니다.', '불러오기 성공', 'success');
this.fetchArchivedUsers();
this.fetchUsers();
}
});
} }
} }
} }