This commit is contained in:
NAS-Admin 2026-02-08 18:10:54 +09:00
parent 82191ed161
commit 9584156f9a
8 changed files with 1618 additions and 190 deletions

298
api.php
View file

@ -101,6 +101,37 @@ try {
echo json_encode($db->query($query)->fetchAll()); echo json_encode($db->query($query)->fetchAll());
break; break;
case 'get_rental_laptops':
$query = "SELECT a.*, m.model_name, m.manufacturer, m.product_name
FROM laptop_assets a
LEFT JOIN laptop_models m ON a.model_id = m.id
WHERE a.assigned_user_name LIKE '%업무용%'";
echo json_encode($db->query($query)->fetchAll());
break;
case 'update_rental':
$data = json_decode(file_get_contents('php://input'), true);
$stmt = $db->prepare("UPDATE laptop_assets SET rental_user_name = ? WHERE id = ?");
$stmt->execute([$data['rental_user_name'], $data['id']]);
// Log the rental action
if ($data['rental_user_name']) {
$log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date) VALUES (?, 'rental', ?, ?)");
$log_stmt->execute([$data['id'], $data['rental_user_name'], date('Y-m-d')]);
}
echo json_encode(['success' => true]);
break;
case 'get_asset_history':
$asset_id = $_GET['asset_id'];
$rental_logs = $db->query("SELECT * FROM asset_history WHERE asset_id = $asset_id AND log_type = 'rental' ORDER BY id DESC LIMIT 20")->fetchAll();
$assign_logs = $db->query("SELECT * FROM asset_history WHERE asset_id = $asset_id AND log_type = 'assignment' ORDER BY id DESC LIMIT 20")->fetchAll();
echo json_encode([
'rental' => $rental_logs,
'assignment' => $assign_logs
]);
break;
case 'get_cards': case 'get_cards':
echo json_encode($db->query("SELECT * FROM access_cards")->fetchAll()); echo json_encode($db->query("SELECT * FROM access_cards")->fetchAll());
break; break;
@ -143,15 +174,66 @@ try {
case 'add_model': case 'add_model':
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
$stmt = $db->prepare("INSERT INTO laptop_models (model_name, manufacturer, specs) VALUES (?, ?, ?)"); $stmt = $db->prepare("INSERT INTO laptop_models (
$stmt->execute([$data['model_name'], $data['manufacturer'], $data['specs'] ?? '']); model_name, manufacturer, specs, cpu, npu, hdd0_model, hdd0_capacity,
hdd1_model, hdd1_capacity, ram, asset_status, assigned_user, fixed_ip,
remarks, options, power_rating, purchase_date, vendor, product_name
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
$data['model_name'],
$data['manufacturer'],
$data['specs'] ?? '',
$data['cpu'] ?? '',
$data['npu'] ?? '',
$data['hdd0_model'] ?? '',
$data['hdd0_capacity'] ?? '',
$data['hdd1_model'] ?? '',
$data['hdd1_capacity'] ?? '',
$data['ram'] ?? '',
$data['asset_status'] ?? '',
$data['assigned_user'] ?? '',
$data['fixed_ip'] ?? '',
$data['remarks'] ?? '',
$data['options'] ?? '',
$data['power_rating'] ?? '',
$data['purchase_date'] ?? '',
$data['vendor'] ?? '',
$data['product_name'] ?? ''
]);
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
break; break;
case 'update_model': case 'update_model':
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
$stmt = $db->prepare("UPDATE laptop_models SET model_name = ?, manufacturer = ?, specs = ? WHERE id = ?"); $stmt = $db->prepare("UPDATE laptop_models SET
$stmt->execute([$data['model_name'], $data['manufacturer'], $data['specs'] ?? '', $data['id']]); model_name = ?, manufacturer = ?, specs = ?, cpu = ?, npu = ?,
hdd0_model = ?, hdd0_capacity = ?, hdd1_model = ?, hdd1_capacity = ?,
ram = ?, asset_status = ?, assigned_user = ?, fixed_ip = ?,
remarks = ?, options = ?, power_rating = ?, purchase_date = ?,
vendor = ?, product_name = ?
WHERE id = ?");
$stmt->execute([
$data['model_name'],
$data['manufacturer'],
$data['specs'] ?? '',
$data['cpu'] ?? '',
$data['npu'] ?? '',
$data['hdd0_model'] ?? '',
$data['hdd0_capacity'] ?? '',
$data['hdd1_model'] ?? '',
$data['hdd1_capacity'] ?? '',
$data['ram'] ?? '',
$data['asset_status'] ?? '',
$data['assigned_user'] ?? '',
$data['fixed_ip'] ?? '',
$data['remarks'] ?? '',
$data['options'] ?? '',
$data['power_rating'] ?? '',
$data['purchase_date'] ?? '',
$data['vendor'] ?? '',
$data['product_name'] ?? '',
$data['id']
]);
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
break; break;
@ -172,22 +254,11 @@ try {
case 'add_laptop_asset': case 'add_laptop_asset':
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
$stmt = $db->prepare("INSERT INTO laptop_assets (asset_tag, model_id, current_user_id, status, serial_number, purchase_date, ip_address) VALUES (?, ?, ?, ?, ?, ?, ?)"); $stmt = $db->prepare("INSERT INTO laptop_assets (
$stmt->execute([ asset_tag, model_id, current_user_id, status, serial_number, purchase_date, ip_address,
$data['asset_tag'], cpu, npu, hdd0_model, hdd0_capacity, hdd1_model, hdd1_capacity, ram,
$data['model_id'], asset_status, assigned_user_name, fixed_ip, options, power_rating, manufacturer, vendor, product_name, remarks, last_confirmed_date
$data['current_user_id'] ?: null, ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$data['current_user_id'] ? 'assigned' : 'stock',
$data['serial_number'],
$data['purchase_date'],
$data['ip_address'] ?? null
]);
echo json_encode(['success' => true]);
break;
case 'update_laptop_asset':
$data = json_decode(file_get_contents('php://input'), true);
$stmt = $db->prepare("UPDATE laptop_assets SET asset_tag = ?, model_id = ?, current_user_id = ?, status = ?, serial_number = ?, purchase_date = ?, ip_address = ? WHERE id = ?");
$stmt->execute([ $stmt->execute([
$data['asset_tag'], $data['asset_tag'],
$data['model_id'], $data['model_id'],
@ -196,8 +267,79 @@ try {
$data['serial_number'], $data['serial_number'],
$data['purchase_date'], $data['purchase_date'],
$data['ip_address'] ?? null, $data['ip_address'] ?? null,
$data['cpu'] ?? '',
$data['npu'] ?? '',
$data['hdd0_model'] ?? '',
$data['hdd0_capacity'] ?? '',
$data['hdd1_model'] ?? '',
$data['hdd1_capacity'] ?? '',
$data['ram'] ?? '',
$data['asset_status'] ?? '',
$data['assigned_user_name'] ?? '',
$data['fixed_ip'] ?? '',
$data['options'] ?? '',
$data['power_rating'] ?? '',
$data['manufacturer'] ?? '',
$data['vendor'] ?? '',
$data['product_name'] ?? '',
$data['remarks'] ?? '',
$data['last_confirmed_date'] ?? null
]);
$new_asset_id = $db->lastInsertId();
if ($data['assigned_user_name']) {
$log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date) VALUES (?, 'assignment', ?, ?)");
$log_stmt->execute([$new_asset_id, $data['assigned_user_name'], date('Y-m-d')]);
}
echo json_encode(['success' => true]);
break;
case 'update_laptop_asset':
$data = json_decode(file_get_contents('php://input'), true);
// Get old data for logging comparison
$old_stmt = $db->prepare("SELECT assigned_user_name FROM laptop_assets WHERE id = ?");
$old_stmt->execute([$data['id']]);
$old_asset = $old_stmt->fetch();
$stmt = $db->prepare("UPDATE laptop_assets SET
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 = ?,
asset_status = ?, assigned_user_name = ?, fixed_ip = ?, options = ?, power_rating = ?, manufacturer = ?, vendor = ?, product_name = ?, remarks = ?, last_confirmed_date = ?
WHERE id = ?");
$stmt->execute([
$data['asset_tag'],
$data['model_id'],
$data['current_user_id'] ?: null,
$data['current_user_id'] ? 'assigned' : 'stock',
$data['serial_number'],
$data['purchase_date'],
$data['ip_address'] ?? null,
$data['cpu'] ?? '',
$data['npu'] ?? '',
$data['hdd0_model'] ?? '',
$data['hdd0_capacity'] ?? '',
$data['hdd1_model'] ?? '',
$data['hdd1_capacity'] ?? '',
$data['ram'] ?? '',
$data['asset_status'] ?? '',
$data['assigned_user_name'] ?? '',
$data['fixed_ip'] ?? '',
$data['options'] ?? '',
$data['power_rating'] ?? '',
$data['manufacturer'] ?? '',
$data['vendor'] ?? '',
$data['product_name'] ?? '',
$data['remarks'] ?? '',
$data['last_confirmed_date'] ?? null,
$data['id'] $data['id']
]); ]);
// Auto-log assignment change
if ($data['assigned_user_name'] && $data['assigned_user_name'] !== ($old_asset['assigned_user_name'] ?? '')) {
$log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date) VALUES (?, 'assignment', ?, ?)");
$log_stmt->execute([$data['id'], $data['assigned_user_name'], date('Y-m-d')]);
}
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
break; break;
@ -231,6 +373,122 @@ try {
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
break; break;
case 'bulk_update_laptops':
$data = json_decode(file_get_contents('php://input'), true);
$ids = $data['ids'];
$newStatus = $data['status'] ?? null;
$newModelId = $data['model_id'] ?? null;
if (empty($ids)) {
echo json_encode(['success' => false, 'error' => 'No items selected']);
break;
}
$updates = [];
$params = [];
if ($newStatus) {
$updates[] = "status = ?";
$params[] = $newStatus;
}
if ($newModelId) {
$updates[] = "model_id = ?";
$params[] = $newModelId;
}
if (empty($updates)) {
echo json_encode(['success' => false, 'error' => 'No updates specified']);
break;
}
$sql = "UPDATE laptop_assets SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(0, count($ids), "?")) . ")";
$stmt = $db->prepare($sql);
$stmt->execute(array_merge($params, $ids));
echo json_encode(['success' => true]);
break;
case 'bulk_update_cards':
$data = json_decode(file_get_contents('php://input'), true);
$ids = $data['ids'];
$newStatus = $data['status'] ?? null;
$newCardType = $data['card_type'] ?? null;
if (empty($ids)) {
echo json_encode(['success' => false, 'error' => 'No items selected']);
break;
}
$updates = [];
$params = [];
if ($newStatus) {
$updates[] = "status = ?";
$params[] = $newStatus;
}
if ($newCardType) {
$updates[] = "card_type = ?";
$params[] = $newCardType;
}
if (empty($updates)) {
echo json_encode(['success' => false, 'error' => 'No updates specified']);
break;
}
$sql = "UPDATE access_cards SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(0, count($ids), "?")) . ")";
$stmt = $db->prepare($sql);
$stmt->execute(array_merge($params, $ids));
echo json_encode(['success' => true]);
break;
case 'bulk_update_mfp':
$data = json_decode(file_get_contents('php://input'), true);
$ids = $data['ids'];
$newStatus = $data['status'] ?? null;
$newPurpose = $data['purpose'] ?? null;
if (empty($ids)) {
echo json_encode(['success' => false, 'error' => 'No items selected']);
break;
}
$updates = [];
$params = [];
if ($newStatus) {
$updates[] = "status = ?";
$params[] = $newStatus;
}
if ($newPurpose) {
$updates[] = "purpose = ?";
$params[] = $newPurpose;
}
if (empty($updates)) {
echo json_encode(['success' => false, 'error' => 'No updates specified']);
break;
}
$sql = "UPDATE mfp_accounts SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(0, count($ids), "?")) . ")";
$stmt = $db->prepare($sql);
$stmt->execute(array_merge($params, $ids));
echo json_encode(['success' => true]);
break;
case 'bulk_update_models':
$data = json_decode(file_get_contents('php://input'), true);
$ids = $data['ids'];
$newManufacturer = $data['manufacturer'] ?? null;
$newProductName = $data['product_name'] ?? null;
if (empty($ids)) {
echo json_encode(['success' => false, 'error' => 'No items selected']);
break;
}
$updates = [];
$params = [];
if ($newManufacturer) {
$updates[] = "manufacturer = ?";
$params[] = $newManufacturer;
}
if ($newProductName) {
$updates[] = "product_name = ?";
$params[] = $newProductName;
}
if (empty($updates)) {
echo json_encode(['success' => false, 'error' => 'No updates specified']);
break;
}
$sql = "UPDATE laptop_models SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(0, count($ids), "?")) . ")";
$stmt = $db->prepare($sql);
$stmt->execute(array_merge($params, $ids));
echo json_encode(['success' => true]);
break;
case 'bulk_update_users': case 'bulk_update_users':
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
$userIds = $data['user_ids']; $userIds = $data['user_ids'];

View file

@ -31,16 +31,51 @@
<h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">출입증 관리</h2> <h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">출입증 관리</h2>
<p class="text-slate-500 mt-1">보안실 연동 임시 출입증 NFC ID 관리</p> <p class="text-slate-500 mt-1">보안실 연동 임시 출입증 NFC ID 관리</p>
</div> </div>
<button @click="showModal = true" <button
@click="showModal = true; isEdit = false; formData = { id: '', card_number: '', card_type: '', nfc_id: '' }"
class="bg-emerald-600 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-emerald-200 hover:bg-emerald-700 transition-all">신규 class="bg-emerald-600 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-emerald-200 hover:bg-emerald-700 transition-all">신규
카드 등록</button> 카드 등록</button>
</header> </header>
<!-- Bulk Actions -->
<div x-show="selectedIds.length > 0" x-transition x-cloak
class="mb-6 bg-slate-900 text-white px-6 py-3 rounded-2xl flex items-center justify-between shadow-xl">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<span class="text-emerald-400 font-black" x-text="selectedIds.length"></span>
<span class="text-xs font-bold text-slate-400"> 선택됨</span>
</div>
<div class="h-4 w-px bg-slate-700"></div>
<div class="flex items-center gap-3">
<span class="text-xs font-bold text-slate-400 uppercase">일괄 변경:</span>
<select x-model="bulkStatus"
class="bg-slate-800 border-none rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-emerald-500">
<option value="">상태 변경...</option>
<option value="available">가용됨(available)</option>
<option value="in_use">사용중(in_use)</option>
</select>
<input type="text" x-model="bulkCardType" placeholder="카드 종류 일괄 입력..."
class="bg-slate-800 border-none rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-emerald-500 w-48">
<button @click="applyBulkUpdate"
class="bg-emerald-500 hover:bg-emerald-600 px-4 py-1.5 rounded-lg text-xs font-black transition-all">적용</button>
</div>
</div>
<button @click="selectedIds = []" class="text-slate-400 hover:text-white 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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Card List Table --> <!-- Card List Table -->
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden" x-show="cards.length > 0"> <div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden" x-show="cards.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>
<th class="px-6 py-4 text-left w-10">
<input type="checkbox" @change="toggleSelectAll($event.target.checked)"
class="w-4 h-4 rounded border-slate-300 text-emerald-600 focus:ring-emerald-500">
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">카드 번호 <th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">카드 번호
</th> </th>
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">카드 종류 <th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">카드 종류
@ -52,19 +87,23 @@
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-slate-50"> <tbody class="divide-y divide-slate-50">
<template x-for="card in cards" :key="card.card_number"> <template x-for="card in cards" :key="card.id">
<tr @click="editCard(card)" class="hover:bg-slate-50/80 transition-colors cursor-pointer group"> <tr class="hover:bg-slate-50/80 transition-colors group">
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4">
<input type="checkbox" :value="card.id" x-model="selectedIds"
class="w-4 h-4 rounded border-slate-300 text-emerald-600 focus:ring-emerald-500">
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editCard(card)">
<span class="text-sm font-black text-emerald-600 px-2.5 py-1 bg-emerald-50 rounded-lg" <span class="text-sm font-black text-emerald-600 px-2.5 py-1 bg-emerald-50 rounded-lg"
x-text="card.card_number"></span> x-text="card.card_number"></span>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editCard(card)">
<div class="text-sm font-bold text-slate-900" x-text="card.card_type"></div> <div class="text-sm font-bold text-slate-900" x-text="card.card_type"></div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editCard(card)">
<div class="text-xs font-mono text-slate-500" x-text="card.nfc_id"></div> <div class="text-xs font-mono text-slate-500" x-text="card.nfc_id"></div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editCard(card)">
<span <span
:class="card.status === 'available' ? 'bg-emerald-100 text-emerald-600' : 'bg-amber-100 text-amber-600'" :class="card.status === 'available' ? 'bg-emerald-100 text-emerald-600' : 'bg-amber-100 text-amber-600'"
class="px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-tighter" class="px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-tighter"
@ -106,7 +145,13 @@
<script> <script>
function cardManagement() { function cardManagement() {
return { return {
cards: [], showModal: false, isEdit: false, formData: { id: '', card_number: '', card_type: '', nfc_id: '' }, cards: [],
showModal: false,
isEdit: false,
selectedIds: [],
bulkStatus: '',
bulkCardType: '',
formData: { id: '', card_number: '', card_type: '', nfc_id: '' },
init() { this.fetchCards(); }, init() { this.fetchCards(); },
fetchCards() { fetchCards() {
const scrollPos = window.scrollY; const scrollPos = window.scrollY;
@ -115,6 +160,30 @@
this.$nextTick(() => window.scrollTo(0, scrollPos)); this.$nextTick(() => window.scrollTo(0, scrollPos));
}); });
}, },
toggleSelectAll(checked) {
this.selectedIds = checked ? this.cards.map(c => c.id) : [];
},
applyBulkUpdate() {
if (this.selectedIds.length === 0) return;
if (!this.bulkStatus && !this.bulkCardType) { alert('변경할 항목을 선택해주세요.'); return; }
fetch('api.php?action=bulk_update_cards', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ids: this.selectedIds,
status: this.bulkStatus,
card_type: this.bulkCardType
})
}).then(res => res.json()).then(data => {
if (data.success) {
this.selectedIds = [];
this.bulkStatus = '';
this.bulkCardType = '';
this.fetchCards();
}
});
},
editCard(card) { editCard(card) {
this.isEdit = true; this.isEdit = true;
this.formData = { ...card }; this.formData = { ...card };

View file

@ -201,20 +201,49 @@ try {
echo "<p class='success'>✔ " . count($users_raw) . " users processed.</p>"; echo "<p class='success'>✔ " . count($users_raw) . " users processed.</p>";
// 5. 노트북 모델 & 자산 등록 // 5. 노트북 모델 & 자산 등록
echo "<p class='info'>⏳ Processing Laptop Assets...</p>"; echo "<p class='info'>⏳ Processing Laptop Assets with Detailed Specs...</p>";
// 테이블 스키마에 스펙 컬럼이 없다면 추가 (최초 1회만) // 테이블 스키마 상세 사양 컬럼 확장
try { $spec_columns = [
$db->exec("ALTER TABLE laptop_assets ADD COLUMN cpu TEXT"); 'cpu' => 'TEXT',
$db->exec("ALTER TABLE laptop_assets ADD COLUMN ram TEXT"); 'npu' => 'TEXT',
$db->exec("ALTER TABLE laptop_assets ADD COLUMN storage TEXT"); 'hdd0_model' => 'TEXT',
} catch (Exception $e) { /* 이미 존재하면 무시 */ 'hdd0_capacity' => 'TEXT',
'hdd1_model' => 'TEXT',
'hdd1_capacity' => 'TEXT',
'ram' => 'TEXT',
'asset_status' => 'TEXT',
'assigned_user_name' => 'TEXT',
'fixed_ip' => 'TEXT',
'options' => 'TEXT',
'power_rating' => 'TEXT',
'manufacturer' => 'TEXT',
'vendor' => 'TEXT',
'product_name' => 'TEXT'
];
foreach ($spec_columns as $col => $type) {
try {
$db->exec("ALTER TABLE laptop_assets ADD COLUMN $col $type");
} catch (Exception $e) { /* 컬럼 이미 존재 시 무시 */
}
} }
$laptops_raw = getCSV($files['laptops']); $laptops_raw = getCSV($files['laptops']);
$model_map = []; $model_map = [];
$model_stmt = $db->prepare("INSERT INTO laptop_models (model_name, manufacturer) VALUES (?, ?)"); $model_stmt = $db->prepare("INSERT INTO laptop_models (model_name, manufacturer) VALUES (?, ?)");
$asset_stmt = $db->prepare("INSERT INTO laptop_assets (asset_tag, model_id, current_user_id, status, serial_number, purchase_date, cpu, ram, storage) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
// 확장된 INSERT 문
$asset_sql = "INSERT INTO laptop_assets (
asset_tag, model_id, current_user_id, status, serial_number, purchase_date, remarks,
cpu, npu, hdd0_model, hdd0_capacity, hdd1_model, hdd1_capacity, ram,
asset_status, assigned_user_name, fixed_ip, options, power_rating, manufacturer, vendor, product_name
) VALUES (
?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?
)";
$asset_stmt = $db->prepare($asset_sql);
foreach ($laptops_raw as $row) { foreach ($laptops_raw as $row) {
$model_name = trim($row['자산명'] ?? $row['모델명'] ?? 'Unknown Model'); $model_name = trim($row['자산명'] ?? $row['모델명'] ?? 'Unknown Model');
@ -233,22 +262,20 @@ try {
if (count($candidates) === 1) { if (count($candidates) === 1) {
$current_user_id = $candidates[0]['id']; $current_user_id = $candidates[0]['id'];
} else { } else {
// 사번으로 매칭 시도
foreach ($candidates as $c) { foreach ($candidates as $c) {
if ($raw_emp_id && $c['emp_id'] === $raw_emp_id) { if ($raw_emp_id && $c['emp_id'] === $raw_emp_id) {
$current_user_id = $c['id']; $current_user_id = $c['id'];
break; break;
} }
} }
// 사번 매칭 실패시 첫 번째 후보 (또는 로직 고도화 가능)
if (!$current_user_id) if (!$current_user_id)
$current_user_id = $candidates[0]['id']; $current_user_id = $candidates[0]['id'];
} }
} }
$status = $current_user_id ? 'assigned' : 'stock'; $internal_status = $current_user_id ? 'assigned' : 'stock';
if (strpos($user_name, '업무용') !== false || strpos($user_name, '임시') !== false) { if (strpos($user_name, '업무용') !== false || strpos($user_name, '임시') !== false) {
$status = 'stock'; // 공용/업무용은 재고로 간주하거나 별도 상태 부여 가능 $internal_status = 'stock';
} }
$p_date = trim($row['취득일자'] ?? ''); $p_date = trim($row['취득일자'] ?? '');
@ -258,24 +285,49 @@ try {
$p_date = "{$parts[2]}-{$parts[0]}-{$parts[1]}"; $p_date = "{$parts[2]}-{$parts[0]}-{$parts[1]}";
} }
// 스펙 정보 추출 // 상세 사양 매핑
$cpu = $row['CPU'] ?? $row['프로세서'] ?? ''; $cpu = $row['CPU'] ?? $row['프로세서'] ?? '';
$ram = $row['RAM'] ?? ''; $npu = $row['NPU'] ?? '';
$storage = $row['HDD 0 / 설치 값'] ?? ''; $hdd0_model = $row['HDD 0 / 타입'] ?? '';
$hdd0_capacity = $row['HDD 0 / 설치 값'] ?? '';
$hdd1_model = $row['HDD 1 / 타입'] ?? '';
$hdd1_capacity = $row['HDD 1 / 설치 값'] ?? '';
$ram_val = $row['RAM'] ?? '';
$asset_status = $row['자산상태'] ?? '';
$assigned_user_name = $row['배정 사용자'] ?? '';
$fixed_ip = $row['고정IP'] ?? $row['고정 IP'] ?? '';
$options = $row['옵션'] ?? '';
$power_rating = $row['정격입력/출력'] ?? '';
$vendor = $row['판매사'] ?? '';
$product_name = $row['품명'] ?? '';
$remarks = $row['비고'] ?? $row['특기사항'] ?? '';
$asset_stmt->execute([ $asset_stmt->execute([
$row['자산관리번호'], $row['자산관리번호'],
$model_map[$model_name], $model_map[$model_name],
$current_user_id, $current_user_id,
$status, $internal_status,
$row['시리얼넘버/EX'], $row['시리얼넘버/EX'],
$p_date, $p_date,
$remarks,
$cpu, $cpu,
$ram, $npu,
$storage $hdd0_model,
$hdd0_capacity,
$hdd1_model,
$hdd1_capacity,
$ram_val,
$asset_status,
$assigned_user_name,
$fixed_ip,
$options,
$power_rating,
$manufacturer,
$vendor,
$product_name
]); ]);
} }
echo "<p class='success'>✔ Laptop assets imported with specs.</p>"; echo "<p class='success'>✔ Laptop assets imported with all detailed specs.</p>";
// 6. 기타 계정 정보 // 6. 기타 계정 정보
$mfp_raw = getCSV($files['mfp']); $mfp_raw = getCSV($files['mfp']);

View file

@ -52,42 +52,125 @@
</div> </div>
</header> </header>
<!-- Bulk Actions -->
<div x-show="selectedIds.length > 0" x-transition x-cloak
class="mb-6 bg-slate-900 text-white px-6 py-3 rounded-2xl flex items-center justify-between shadow-xl">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<span class="text-blue-400 font-black" x-text="selectedIds.length"></span>
<span class="text-xs font-bold text-slate-400"> 선택됨</span>
</div>
<div class="h-4 w-px bg-slate-700"></div>
<div class="flex items-center gap-3">
<span class="text-xs font-bold text-slate-400 uppercase">일괄 변경:</span>
<select x-model="bulkStatus"
class="bg-slate-800 border-none rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-blue-500">
<option value="">상태 변경...</option>
<option value="assigned">지급됨(assigned)</option>
<option value="stock">재고(stock)</option>
</select>
<select x-model="bulkModelId"
class="bg-slate-800 border-none rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-blue-500">
<option value="">모델 변경...</option>
<template x-for="model in models" :key="model.id">
<option :value="model.id" x-text="'[' + model.manufacturer + '] ' + model.model_name">
</option>
</template>
</select>
<button @click="applyBulkUpdate"
class="bg-blue-500 hover:bg-blue-600 px-4 py-1.5 rounded-lg text-xs font-black transition-all">적용</button>
</div>
</div>
<button @click="selectedIds = []" class="text-slate-400 hover:text-white 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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- 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 && assets.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>
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">자산번호 <th class="px-6 py-4 text-left">
<input type="checkbox" @change="toggleSelectAll($event.target.checked)"
class="w-4 h-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500">
</th>
<th @click="sortBy('asset_tag')"
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
자산번호 <span x-show="sortKey === 'asset_tag'" x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
</th>
<th @click="sortBy('purchase_date')"
class="px-4 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
취득년 <span x-show="sortKey === 'purchase_date'"
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
</th>
<th @click="sortBy('model_name')"
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
기기 정보 / 사양 <span x-show="sortKey === 'model_name'"
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
</th>
<th @click="sortBy('ip_address')"
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
IP / 시리얼 <span x-show="sortKey === 'ip_address'"
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
</th>
<th @click="sortBy('user_name')"
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
배정 사용자 <span x-show="sortKey === 'user_name'"
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
</th>
<th @click="sortBy('last_confirmed_date')"
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
확인일 <span x-show="sortKey === 'last_confirmed_date'"
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
</th>
<th @click="sortBy('status')"
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
상태 <span x-show="sortKey === 'status'" x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
</th> </th>
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">기기 정보 / 사양</th>
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">IP 주소 / 시리얼</th>
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">배정 사용자</th>
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">취득일</th>
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">상태</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-slate-50"> <tbody class="divide-y divide-slate-50">
<template x-for="asset in assets" :key="asset.asset_tag"> <template x-for="asset in sortedAssets" :key="asset.id">
<tr @click="editAsset(asset)" <tr class="hover:bg-slate-50/80 transition-colors group">
class="hover:bg-slate-50/80 transition-colors cursor-pointer group"> <td class="px-6 py-4">
<td class="px-6 py-4 whitespace-nowrap"> <input type="checkbox" :value="asset.id" x-model="selectedIds"
class="w-4 h-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500">
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editAsset(asset)">
<span <span
class="text-sm font-black text-slate-900 px-2.5 py-1 bg-slate-100 rounded-lg group-hover:bg-white transition-colors" class="text-sm font-black text-slate-900 px-2.5 py-1 bg-slate-100 rounded-lg group-hover:bg-white transition-colors"
x-text="asset.asset_tag"></span> x-text="asset.asset_tag"></span>
</td> </td>
<td class="px-4 py-4 whitespace-nowrap">
<span class="text-xs font-black text-blue-600 bg-blue-50 px-2 py-1 rounded"
x-text="asset.purchase_date ? asset.purchase_date.substring(0,4) : '-'"></span>
</td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<div class="text-[10px] font-bold text-slate-900" x-text="asset.model_name"></div> <div class="text-[11px] font-black text-slate-800" x-text="asset.model_name"></div>
<div class="text-[9px] font-black text-blue-500 uppercase tracking-widest" x-text="asset.manufacturer"></div> <div class="text-[9px] font-black text-slate-400 uppercase tracking-widest mt-0.5"
x-text="asset.manufacturer"></div>
<div class="text-[10px] text-slate-400 mt-1 space-y-0.5"> <div class="text-[10px] text-slate-400 mt-1 space-y-0.5">
<div x-show="asset.processor" class="flex items-center"><span class="w-8 shrink-0">CPU</span> <span x-text="asset.processor"></span></div> <div x-show="asset.processor" class="flex items-center"><span
<div x-show="asset.ram" class="flex items-center"><span class="w-8 shrink-0">RAM</span> <span x-text="asset.ram"></span></div> class="w-8 shrink-0">CPU</span> <span x-text="asset.processor"></span></div>
<div x-show="asset.storage" class="flex items-center"><span class="w-8 shrink-0">DISK</span> <span x-text="asset.storage"></span></div> <div x-show="asset.ram" class="flex items-center"><span
class="w-8 shrink-0">RAM</span>
<span x-text="asset.ram"></span>
</div>
<div x-show="asset.storage" class="flex items-center"><span
class="w-8 shrink-0">DISK</span>
<span x-text="asset.storage"></span>
</div>
</div> </div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<div class="text-xs font-black text-slate-900 mb-1" x-text="asset.ip_address || '-'"></div> <div class="text-xs font-black text-slate-900 mb-1" x-text="asset.ip_address || '-'">
<div class="text-[10px] font-mono text-slate-400" x-text="asset.serial_number || '-'"></div> </div>
<div class="text-[10px] font-mono text-slate-400" x-text="asset.serial_number || '-'">
</div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center" x-show="asset.user_name"> <div class="flex items-center" x-show="asset.user_name">
@ -97,13 +180,17 @@
</div> </div>
<div x-show="!asset.user_name" class="text-xs text-slate-400 font-medium">배정 대기</div> <div x-show="!asset.user_name" class="text-xs text-slate-400 font-medium">배정 대기</div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500" <td class="px-6 py-4 whitespace-nowrap text-xs text-slate-500"
x-text="asset.purchase_date || '-'"></td> x-text="asset.last_confirmed_date || '-'"></td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<span <span x-show="asset.user_name || asset.assigned_user_name"
:class="asset.status === 'assigned' ? 'bg-blue-50 text-blue-600 border-blue-100' : 'bg-slate-50 text-slate-500 border-slate-100'" :class="( (asset.user_name && asset.user_name.includes('업무용')) || (asset.assigned_user_name && asset.assigned_user_name.includes('업무용')) ) ? 'bg-amber-50 text-amber-600 border-amber-100' : 'bg-blue-50 text-blue-600 border-blue-100'"
class="px-3 py-1 rounded-full text-[10px] font-black border uppercase tracking-tighter" class="px-3 py-1 rounded-full text-[10px] font-black border uppercase tracking-tighter"
x-text="asset.status === 'assigned' ? 'ASIGNED' : 'STOCK'"></span> x-text="( (asset.user_name && asset.user_name.includes('업무용')) || (asset.assigned_user_name && asset.assigned_user_name.includes('업무용')) ) ? '업무용' : '직원배정'"></span>
<span x-show="!asset.user_name && !asset.assigned_user_name"
class="px-3 py-1 rounded-full text-[10px] font-black border uppercase tracking-tighter bg-slate-50 text-slate-500 border-slate-100">
STOCK
</span>
</td> </td>
</tr> </tr>
</template> </template>
@ -124,7 +211,7 @@
<div x-show="showModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4"> <div x-show="showModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
<div x-show="showModal" @click="showModal = false" class="fixed inset-0 modal-bg transition-opacity"></div> <div x-show="showModal" @click="showModal = false" class="fixed inset-0 modal-bg transition-opacity"></div>
<div x-show="showModal" <div x-show="showModal"
class="bg-white rounded-3xl p-8 max-w-lg w-full relative z-[111] shadow-2xl overflow-hidden"> class="bg-white rounded-3xl p-8 max-w-4xl w-full relative z-[111] shadow-2xl overflow-y-auto max-h-[90vh]">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h3 class="text-2xl font-black text-slate-900" x-text="isEdit ? '자산 정보 수정' : '신규 자산 등록'"></h3> <h3 class="text-2xl font-black text-slate-900" x-text="isEdit ? '자산 정보 수정' : '신규 자산 등록'"></h3>
<button @click="showModal = false" class="text-slate-400 hover:text-slate-600"> <button @click="showModal = false" class="text-slate-400 hover:text-slate-600">
@ -135,30 +222,17 @@
</button> </button>
</div> </div>
<form @submit.prevent="submitAsset" class="space-y-5"> <form @submit.prevent="submitAsset" class="space-y-6">
<div class="grid grid-cols-2 gap-4"> <!-- 기본 정보 섹션 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div> <div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">자산관리번호</label> <label class="block text-xs font-bold text-slate-500 uppercase mb-2">자산관리번호</label>
<input type="text" x-model="formData.asset_tag" required <input type="text" x-model="formData.asset_tag" required
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all"> class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all font-bold">
</div> </div>
<div> <div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">IP 주소</label> <label class="block text-xs font-bold text-slate-500 uppercase mb-2">노트북 모델 연동</label>
<input type="text" x-model="formData.ip_address" placeholder="192.168.x.x" <select x-model="formData.model_id" required @change="applyModelDefaults"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
</div>
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">시리얼 번호</label>
<input type="text" x-model="formData.serial_number"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">노트북 모델 연동</label>
<div class="relative">
<select x-model="formData.model_id" required
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all appearance-none"> class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all appearance-none">
<option value="">모델 선택</option> <option value="">모델 선택</option>
<template x-for="model in models" :key="model.id"> <template x-for="model in models" :key="model.id">
@ -166,44 +240,123 @@
</option> </option>
</template> </template>
</select> </select>
<div </div>
class="absolute inset-y-0 right-0 pr-4 flex items-center pointer-events-none text-slate-400"> <div>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <label class="block text-xs font-bold text-slate-500 uppercase mb-2">시리얼 번호</label>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <input type="text" x-model="formData.serial_number"
d="M19 9l-7 7-7-7" /> class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
</svg> </div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">배정 사용자</label>
<select x-model="formData.current_user_id"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
<option value="">미배정 (재고)</option>
<template x-for="user in allUsers" :key="user.id">
<option :value="user.id" x-text="user.name + ' (' + user.emp_id + ')'"></option>
</template>
</select>
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">고정 IP 주소</label>
<input type="text" x-model="formData.ip_address" placeholder="192.168.x.x"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">취득일</label>
<input type="date" x-model="formData.purchase_date"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2 text-blue-600">실사
확인일</label>
<input type="date" x-model="formData.last_confirmed_date"
class="w-full px-4 py-3 bg-blue-50 border border-blue-100 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
</div>
</div>
<hr class="border-slate-100">
<!-- 상세 사양 섹션 (모델에서 기본값 상속 가능) -->
<div class="flex items-center justify-between">
<h4 class="text-sm font-black text-slate-400 uppercase tracking-widest">Hardware Specifications</h4>
<button type="button" @click="applyModelDefaults" x-show="formData.model_id"
class="text-[10px] font-bold text-blue-600 bg-blue-50 px-2 py-1 rounded hover:bg-blue-100 transition-colors">
모델 기본값 불러오기
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">CPU</label>
<input type="text" x-model="formData.cpu"
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">NPU</label>
<input type="text" x-model="formData.npu"
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">RAM</label>
<input type="text" x-model="formData.ram"
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-slate-50 p-4 rounded-xl space-y-3">
<label class="block text-[10px] font-black text-slate-400 uppercase">Storage 0</label>
<div class="grid grid-cols-2 gap-2">
<input type="text" x-model="formData.hdd0_model" placeholder="모델명/타입"
class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
<input type="text" x-model="formData.hdd0_capacity" placeholder="용량"
class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
</div> </div>
</div> </div>
<div class="mt-2 text-[10px] text-blue-500 font-bold bg-blue-50 px-3 py-1.5 rounded-lg border border-blue-100" <div class="bg-slate-50 p-4 rounded-xl space-y-3">
x-show="formData.model_id"> <label class="block text-[10px] font-black text-slate-400 uppercase">Storage 1</label>
<span class="uppercase">Hardware Specs:</span> <span <div class="grid grid-cols-2 gap-2">
x-text="getModelSpecs(formData.model_id) || '사양 정보 없음'"></span> <input type="text" x-model="formData.hdd1_model" placeholder="모델명/타입"
class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
<input type="text" x-model="formData.hdd1_capacity" placeholder="용량"
class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">노트북 상태</label>
<input type="text" x-model="formData.asset_status"
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">정격입출력</label>
<input type="text" x-model="formData.power_rating"
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">옵션</label>
<input type="text" x-model="formData.options"
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm">
</div> </div>
</div> </div>
<div> <div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">배정 사용자 (옵션)</label> <label class="block text-xs font-bold text-slate-500 uppercase mb-2">비고 (특기사항)</label>
<select x-model="formData.current_user_id" <textarea x-model="formData.remarks" rows="2" placeholder="자산과 관련된 특이사항을 입력하세요."
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all"> class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none resize-none transition-all"></textarea>
<option value="">미배정 (재고)</option>
<template x-for="user in allUsers" :key="user.id">
<option :value="user.id" x-text="user.name + ' (' + user.emp_id + ')'"></option>
</template>
</select>
</div> </div>
<div> <div class="pt-4 flex gap-4">
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">취득일</label>
<input type="date" x-model="formData.purchase_date"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
</div>
<div class="pt-4 flex gap-3">
<button type="button" @click="showModal = false" <button type="button" @click="showModal = false"
class="flex-1 py-4 bg-slate-100 text-slate-600 rounded-2xl font-bold hover:bg-slate-200 transition-all">취소</button> class="flex-1 py-4 bg-slate-100 text-slate-600 rounded-2xl font-bold hover:bg-slate-200 transition-all">취소</button>
<button type="submit" <button type="submit"
class="flex-1 py-4 bg-blue-600 text-white rounded-2xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all" class="flex-1 py-4 bg-blue-600 text-white rounded-2xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all"
x-text="isEdit ? '수정 완료' : '저장하기'"></button> x-text="isEdit ? '수정 완료' : '자산 등록하기'"></button>
</div> </div>
</form> </form>
</div> </div>
@ -218,7 +371,20 @@
loading: true, loading: true,
showModal: false, showModal: false,
isEdit: false, isEdit: false,
formData: { id: '', asset_tag: '', model_id: '', current_user_id: '', status: 'stock', serial_number: '', purchase_date: '', ip_address: '' }, selectedIds: [],
bulkStatus: '',
bulkModelId: '',
sortKey: 'asset_tag',
sortOrder: 'asc',
formData: {
id: '', asset_tag: '', model_id: '', current_user_id: '', status: 'stock',
serial_number: '', purchase_date: '', ip_address: '',
cpu: '', npu: '', ram: '',
hdd0_model: '', hdd0_capacity: '',
hdd1_model: '', hdd1_capacity: '',
asset_status: '', assigned_user_name: '', fixed_ip: '', options: '', power_rating: '',
manufacturer: '', vendor: '', product_name: '', remarks: ''
},
init() { init() {
this.fetchAssets(); this.fetchAssets();
@ -226,6 +392,32 @@
this.fetchUsers(); this.fetchUsers();
}, },
toggleSelectAll(checked) {
this.selectedIds = checked ? this.assets.map(a => a.id) : [];
},
applyBulkUpdate() {
if (this.selectedIds.length === 0) return;
if (!this.bulkStatus && !this.bulkModelId) { alert('변경할 항목을 선택해주세요.'); return; }
fetch('api.php?action=bulk_update_laptops', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ids: this.selectedIds,
status: this.bulkStatus,
model_id: this.bulkModelId
})
}).then(res => res.json()).then(data => {
if (data.success) {
this.selectedIds = [];
this.bulkStatus = '';
this.bulkModelId = '';
this.fetchAssets();
}
});
},
fetchAssets() { fetchAssets() {
const scrollPos = window.scrollY; const scrollPos = window.scrollY;
this.loading = true; this.loading = true;
@ -236,6 +428,24 @@
}); });
}, },
get sortedAssets() {
return [...this.assets].sort((a, b) => {
let v1 = a[this.sortKey] || '';
let v2 = b[this.sortKey] || '';
if (this.sortOrder === 'asc') return v1 > v2 ? 1 : -1;
return v1 < v2 ? 1 : -1;
});
},
sortBy(key) {
if (this.sortKey === key) {
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
} else {
this.sortKey = key;
this.sortOrder = 'asc';
}
},
fetchModels() { fetchModels() {
fetch('api.php?action=get_models').then(res => res.json()).then(data => this.models = data); fetch('api.php?action=get_models').then(res => res.json()).then(data => this.models = data);
}, },
@ -244,14 +454,38 @@
fetch('api.php?action=get_users').then(res => res.json()).then(data => this.allUsers = data); fetch('api.php?action=get_users').then(res => res.json()).then(data => this.allUsers = data);
}, },
getModelSpecs(modelId) { applyModelDefaults() {
const model = this.models.find(m => m.id == modelId); if (!this.formData.model_id) return;
return model ? model.specs : ''; const model = this.models.find(m => m.id == this.formData.model_id);
if (model) {
// 모델에 정의된 사양을 자산 폼에 기본값으로 복사
this.formData.cpu = model.cpu || this.formData.cpu;
this.formData.npu = model.npu || this.formData.npu;
this.formData.ram = model.ram || this.formData.ram;
this.formData.hdd0_model = model.hdd0_model || this.formData.hdd0_model;
this.formData.hdd0_capacity = model.hdd0_capacity || this.formData.hdd0_capacity;
this.formData.hdd1_model = model.hdd1_model || this.formData.hdd1_model;
this.formData.hdd1_capacity = model.hdd1_capacity || this.formData.hdd1_capacity;
this.formData.power_rating = model.power_rating || this.formData.power_rating;
this.formData.options = model.options || this.formData.options;
this.formData.manufacturer = model.manufacturer;
this.formData.product_name = model.product_name || this.formData.product_name;
this.formData.vendor = model.vendor || this.formData.vendor;
if (!this.isEdit) {
this.formData.remarks = model.remarks || this.formData.remarks;
}
}
}, },
openAddModal() { openAddModal() {
this.isEdit = false; this.isEdit = false;
this.formData = { id: '', asset_tag: '', model_id: '', current_user_id: '', status: 'stock', serial_number: '', purchase_date: new Date().toISOString().split('T')[0], ip_address: '' }; this.formData = {
id: '', asset_tag: '', model_id: '', current_user_id: '', status: 'stock',
serial_number: '', purchase_date: new Date().toISOString().split('T')[0], last_confirmed_date: '', ip_address: '',
cpu: '', npu: '', ram: '', hdd0_model: '', hdd0_capacity: '', hdd1_model: '', hdd1_capacity: '',
asset_status: '', assigned_user_name: '', fixed_ip: '', options: '', power_rating: '',
manufacturer: '', vendor: '', product_name: '', remarks: ''
};
this.showModal = true; this.showModal = true;
}, },

86
mfp.php
View file

@ -31,16 +31,51 @@
<h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">복합기 계정 관리</h2> <h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">복합기 계정 관리</h2>
<p class="text-slate-500 mt-1">임시/공용 복합기 계정 비밀번호 관리</p> <p class="text-slate-500 mt-1">임시/공용 복합기 계정 비밀번호 관리</p>
</div> </div>
<button @click="showModal = true" <button
@click="showModal = true; isEdit = false; formData = { id: '', account_id: '', account_pw: '', purpose: '' }"
class="bg-amber-600 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-amber-200 hover:bg-amber-700 transition-all">신규 class="bg-amber-600 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-amber-200 hover:bg-amber-700 transition-all">신규
계정 등록</button> 계정 등록</button>
</header> </header>
<!-- Bulk Actions -->
<div x-show="selectedIds.length > 0" x-transition x-cloak
class="mb-6 bg-slate-900 text-white px-6 py-3 rounded-2xl flex items-center justify-between shadow-xl">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<span class="text-amber-400 font-black" x-text="selectedIds.length"></span>
<span class="text-xs font-bold text-slate-400"> 선택됨</span>
</div>
<div class="h-4 w-px bg-slate-700"></div>
<div class="flex items-center gap-3">
<span class="text-xs font-bold text-slate-400 uppercase">일괄 변경:</span>
<select x-model="bulkStatus"
class="bg-slate-800 border-none rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-amber-500">
<option value="">상태 변경...</option>
<option value="active">사용중(active)</option>
<option value="inactive">비활성(inactive)</option>
</select>
<input type="text" x-model="bulkPurpose" placeholder="사용 목적 일괄 입력..."
class="bg-slate-800 border-none rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-amber-500 w-48">
<button @click="applyBulkUpdate"
class="bg-amber-500 hover:bg-amber-600 px-4 py-1.5 rounded-lg text-xs font-black transition-all">적용</button>
</div>
</div>
<button @click="selectedIds = []" class="text-slate-400 hover:text-white 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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- MFP List Table --> <!-- MFP List Table -->
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden" x-show="mfpList.length > 0"> <div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden" x-show="mfpList.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>
<th class="px-6 py-4 text-left w-10">
<input type="checkbox" @change="toggleSelectAll($event.target.checked)"
class="w-4 h-4 rounded border-slate-300 text-amber-600 focus:ring-amber-500">
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">계정 ID <th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">계정 ID
</th> </th>
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider"> <th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">
@ -52,9 +87,13 @@
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-slate-50"> <tbody class="divide-y divide-slate-50">
<template x-for="mfp in mfpList" :key="mfp.account_id"> <template x-for="mfp in mfpList" :key="mfp.id">
<tr @click="editMfp(mfp)" class="hover:bg-slate-50/80 transition-colors cursor-pointer group"> <tr class="hover:bg-slate-50/80 transition-colors group">
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4">
<input type="checkbox" :value="mfp.id" x-model="selectedIds"
class="w-4 h-4 rounded border-slate-300 text-amber-600 focus:ring-amber-500">
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editMfp(mfp)">
<div class="flex items-center"> <div class="flex items-center">
<div class="p-2 bg-amber-50 rounded-lg text-amber-600 mr-3"> <div class="p-2 bg-amber-50 rounded-lg text-amber-600 mr-3">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -65,14 +104,15 @@
<span class="text-sm font-black text-slate-900" x-text="mfp.account_id"></span> <span class="text-sm font-black text-slate-900" x-text="mfp.account_id"></span>
</div> </div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editMfp(mfp)">
<span <span
class="px-2 py-1 bg-slate-100 border border-slate-200 rounded text-xs font-mono font-bold text-slate-700" class="px-2 py-1 bg-slate-100 border border-slate-200 rounded text-xs font-mono font-bold text-slate-700"
x-text="mfp.account_pw"></span> x-text="mfp.account_pw"></span>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500" x-text="mfp.purpose || '-'"> <td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 cursor-pointer"
@click="editMfp(mfp)" x-text="mfp.purpose || '-'">
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editMfp(mfp)">
<span <span
class="px-3 py-1 bg-emerald-100 text-emerald-600 rounded-full text-[10px] font-black uppercase tracking-tighter" class="px-3 py-1 bg-emerald-100 text-emerald-600 rounded-full text-[10px] font-black uppercase tracking-tighter"
x-text="mfp.status"></span> x-text="mfp.status"></span>
@ -112,7 +152,13 @@
<script> <script>
function mfpManagement() { function mfpManagement() {
return { return {
mfpList: [], showModal: false, isEdit: false, formData: { id: '', account_id: '', account_pw: '', purpose: '' }, mfpList: [],
showModal: false,
isEdit: false,
selectedIds: [],
bulkStatus: '',
bulkPurpose: '',
formData: { id: '', account_id: '', account_pw: '', purpose: '' },
init() { this.fetchMfp(); }, init() { this.fetchMfp(); },
fetchMfp() { fetchMfp() {
const scrollPos = window.scrollY; const scrollPos = window.scrollY;
@ -121,6 +167,30 @@
this.$nextTick(() => window.scrollTo(0, scrollPos)); this.$nextTick(() => window.scrollTo(0, scrollPos));
}); });
}, },
toggleSelectAll(checked) {
this.selectedIds = checked ? this.mfpList.map(m => m.id) : [];
},
applyBulkUpdate() {
if (this.selectedIds.length === 0) return;
if (!this.bulkStatus && !this.bulkPurpose) { alert('변경할 항목을 선택해주세요.'); return; }
fetch('api.php?action=bulk_update_mfp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ids: this.selectedIds,
status: this.bulkStatus,
purpose: this.bulkPurpose
})
}).then(res => res.json()).then(data => {
if (data.success) {
this.selectedIds = [];
this.bulkStatus = '';
this.bulkPurpose = '';
this.fetchMfp();
}
});
},
editMfp(mfp) { editMfp(mfp) {
this.isEdit = true; this.isEdit = true;
this.formData = { ...mfp }; this.formData = { ...mfp };

View file

@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DB 관리 - 노트북 모델 | FKI ASSET</title> <title>DB 관리 | FKI ASSET</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
@ -25,100 +25,449 @@
</style> </style>
</head> </head>
<body class="bg-[#f8fafc] text-slate-800" x-data="modelManagement()"> <body class="bg-[#f8fafc] text-slate-800" x-data="dbManagement()">
<?php include 'nav.php'; ?> <?php include 'nav.php'; ?>
<main class="max-w-[1600px] mx-auto p-8 pt-10"> <main class="max-w-[1600px] mx-auto p-8 pt-10">
<header class="mb-10 flex flex-col md:flex-row md:items-center justify-between gap-4"> <header class="mb-10 flex flex-col md:flex-row md:items-center justify-between gap-4">
<div> <div>
<h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">DB 관리: 노트북 모델</h2> <h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">DB 마스터 관리</h2>
<p class="text-slate-500 mt-1">자산 등록 사용될 하드웨어 모델 정보 상세 사양 관리</p> <p class="text-slate-500 mt-1">시스템 운영을 위한 기초 자산 모델 조직 정보를 관리합니다.</p>
</div>
<div class="flex gap-3">
<button x-show="currentTab === 'models'" @click="openAddModal()"
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">신규
모델 등록</button>
<button x-show="currentTab === 'depts'" @click="openAddDeptModal()"
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">신규
부서 생성</button>
</div> </div>
<button @click="openAddModal()"
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">신규
모델 등록</button>
</header> </header>
<!-- Model List Table --> <!-- Tabs -->
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden"> <div class="flex items-center space-x-2 mb-8 bg-slate-100 p-1.5 rounded-2xl w-fit">
<table class="min-w-full divide-y divide-slate-100"> <button @click="currentTab = 'models'"
<thead class="bg-slate-50/50"> :class="currentTab === 'models' ? 'bg-white shadow-sm text-blue-600' : 'text-slate-500 hover:text-slate-700'"
<tr> class="px-6 py-2 rounded-xl text-sm font-black transition-all">노트북 모델</button>
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">제조사 <button @click="currentTab = 'depts'"
</th> :class="currentTab === 'depts' ? 'bg-white shadow-sm text-blue-600' : 'text-slate-500 hover:text-slate-700'"
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">모델명 class="px-6 py-2 rounded-xl text-sm font-black transition-all">부서 관리</button>
</th> </div>
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">상세 사양
</th> <!-- Tab 1: Laptop Models -->
<th class="px-6 py-4 text-right text-xs font-bold text-slate-500 uppercase tracking-wider">관리 <div x-show="currentTab === 'models'" x-transition>
</th> <!-- Bulk Actions -->
</tr> <div x-show="selectedIds.length > 0" x-transition x-cloak
</thead> class="mb-6 bg-slate-900 text-white px-6 py-3 rounded-2xl flex items-center justify-between shadow-xl">
<tbody class="divide-y divide-slate-50"> <div class="flex items-center gap-4">
<template x-for="model in models" :key="model.id"> <div class="flex items-center gap-2">
<tr class="hover:bg-slate-50/80 transition-colors group"> <span class="text-blue-400 font-black" x-text="selectedIds.length"></span>
<td class="px-6 py-4 whitespace-nowrap"> <span class="text-xs font-bold text-slate-400"> 선택됨</span>
<span </div>
class="px-2.5 py-1 bg-slate-100 text-slate-600 rounded-lg text-xs font-black uppercase tracking-widest" <div class="h-4 w-px bg-slate-700"></div>
x-text="model.manufacturer"></span> <div class="flex items-center gap-3">
</td> <span class="text-xs font-bold text-slate-400 uppercase">일괄 변경:</span>
<td class="px-6 py-4 whitespace-nowrap"> <input type="text" x-model="bulkManufacturer" placeholder="제조사 일괄 변경..."
<span class="text-sm font-bold text-slate-900" x-text="model.model_name"></span> class="bg-slate-800 border-none rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-blue-500 w-40 text-white">
</td> <input type="text" x-model="bulkProductName" placeholder="품명 일괄 변경..."
<td class="px-6 py-4"> class="bg-slate-800 border-none rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-blue-500 w-40 text-white">
<span class="text-xs text-slate-500 line-clamp-1" x-text="model.specs || '-'"></span> <button @click="applyBulkUpdate"
</td> class="bg-blue-500 hover:bg-blue-600 px-4 py-1.5 rounded-lg text-xs font-black transition-all">적용</button>
<td class="px-6 py-4 whitespace-nowrap text-right"> </div>
<button @click="editModel(model)" </div>
class="text-xs font-bold text-blue-600 hover:underline px-3 py-1 rounded-lg hover:bg-blue-50 transition-all">수정</button> <button @click="selectedIds = []" class="text-slate-400 hover:text-white transition-colors">
</td> <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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Model List Table -->
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-x-auto no-scrollbar">
<table class="min-w-full divide-y divide-slate-100">
<thead class="bg-slate-50/50">
<tr>
<th class="px-6 py-4 text-left w-10">
<input type="checkbox" @change="toggleSelectAll($event.target.checked)"
class="w-4 h-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500">
</th>
<th
class="px-4 py-4 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider">
취득년도</th>
<th
class="px-4 py-4 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider">
취득일</th>
<th
class="px-4 py-4 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider">
제조사</th>
<th
class="px-6 py-4 text-left text-[10px] font-bold text-slate-500 uppercase tracking-wider">
모델명</th>
<th
class="px-4 py-4 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider font-mono">
CPU</th>
<th
class="px-4 py-4 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider font-mono">
RAM</th>
<th
class="px-4 py-4 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider font-mono text-blue-500">
HDD 0</th>
<th
class="px-4 py-4 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider">
정격입출력</th>
<th
class="px-6 py-4 text-right text-[10px] font-bold text-slate-500 uppercase tracking-wider whitespace-nowrap">
관리</th>
</tr> </tr>
</template> </thead>
</tbody> <tbody class="divide-y divide-slate-50">
</table> <template x-for="model in models" :key="model.id">
<tr class="hover:bg-slate-50/80 transition-colors group">
<td class="px-6 py-4">
<input type="checkbox" :value="model.id" x-model="selectedIds"
class="w-4 h-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500">
</td>
<td class="px-4 py-4 whitespace-nowrap cursor-pointer text-xs font-medium text-slate-500"
@click="editModel(model)">
<span
x-text="model.purchase_date ? model.purchase_date.substring(0,4) : '-'"></span>
</td>
<td class="px-4 py-4 whitespace-nowrap cursor-pointer text-[11px] text-slate-400"
@click="editModel(model)">
<span x-text="model.purchase_date || '-'"></span>
</td>
<td class="px-4 py-4 whitespace-nowrap cursor-pointer" @click="editModel(model)">
<span
class="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-[10px] font-black uppercase tracking-widest"
x-text="model.manufacturer"></span>
</td>
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editModel(model)">
<span class="text-sm font-bold text-slate-800" x-text="model.model_name"></span>
</td>
<td class="px-4 py-4 whitespace-nowrap cursor-pointer text-xs font-mono text-slate-500"
@click="editModel(model)">
<span x-text="model.cpu || '-'"></span>
</td>
<td class="px-4 py-4 whitespace-nowrap cursor-pointer text-xs font-mono text-slate-600 font-bold"
@click="editModel(model)">
<span x-text="model.ram || '-'"></span>
</td>
<td class="px-4 py-4 whitespace-nowrap cursor-pointer text-xs font-mono text-blue-600 font-bold"
@click="editModel(model)">
<span x-text="model.hdd0_capacity || '-'"></span>
</td>
<td class="px-4 py-4 whitespace-nowrap cursor-pointer text-[10px] text-slate-400"
@click="editModel(model)">
<span x-text="model.power_rating || '-'"></span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<button @click="editModel(model)"
class="text-[11px] font-black text-blue-600 hover:underline px-2 py-1 rounded-lg hover:bg-blue-50 transition-all uppercase">EDIT</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- Tab 2: Departments -->
<div x-show="currentTab === 'depts'" x-transition>
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden">
<table class="min-w-full divide-y divide-slate-100">
<thead class="bg-slate-50/50">
<tr>
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">
부서명 (계층구조)</th>
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">
인원수</th>
<th class="px-6 py-4 text-right text-xs font-bold text-slate-500 uppercase tracking-wider">
관리</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50">
<template x-for="dept in depts" :key="dept.id">
<tr class="hover:bg-slate-50/80 transition-colors group">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div :style="'margin-left: ' + (getDeptLevel(dept.path) * 20) + 'px'"
class="flex items-center">
<div class="w-2 h-2 rounded-full mr-3"
:class="getDeptLevel(dept.path) === 0 ? 'bg-blue-600' : 'bg-slate-300'">
</div>
<span class="text-sm font-bold text-slate-900" x-text="dept.name"></span>
<span class="ml-2 text-[10px] text-slate-400 font-medium"
x-show="getDeptLevel(dept.path) > 0" x-text="dept.path"></span>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<button @click="viewDeptMembers(dept)"
class="text-sm font-black text-blue-600 hover:underline">
<span x-text="dept.member_count || 0"></span> 확인
</button>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<button @click="editDept(dept)"
class="text-xs font-bold text-slate-400 hover:text-blue-600 px-3 py-1 rounded-lg hover:bg-blue-50 transition-all">수정</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div> </div>
</main> </main>
<!-- Model Modal --> <!-- Model Modal -->
<div x-show="showModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4"> <div x-show="showModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
<div class="fixed inset-0 modal-bg" @click="showModal = false"></div> <div class="fixed inset-0 modal-bg" @click="showModal = false"></div>
<div class="bg-white rounded-3xl p-8 max-w-md w-full relative z-[111] shadow-2xl"> <div class="bg-white rounded-3xl p-8 max-w-4xl w-full relative z-[111] shadow-2xl max-h-[90vh] overflow-y-auto">
<h3 class="text-2xl font-black mb-6" x-text="isEdit ? '모델 정보 수정' : '신규 모델 등록'"></h3> <h3 class="text-2xl font-black mb-6" x-text="isEdit ? '모델 정보 수정' : '신규 모델 등록'"></h3>
<form @submit.prevent="submitModel" class="space-y-4"> <form @submit.prevent="submitModel" class="space-y-6">
<div> <!-- 기본 정보 -->
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">제조사</label> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<input type="text" x-model="formData.manufacturer" required placeholder="예: Samsung, Apple" <div>
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none"> <label class="block text-xs font-bold text-slate-500 uppercase mb-2">제조사</label>
<input type="text" x-model="formData.manufacturer" required placeholder="예: SAMSUNG"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">모델명</label>
<input type="text" x-model="formData.model_name" required placeholder="예: NT750XEV"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">품명</label>
<input type="text" x-model="formData.product_name" placeholder="예: 노트북"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
</div>
</div> </div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">모델명</label> <!-- 핵심 사양 -->
<input type="text" x-model="formData.model_name" required placeholder="예: Galaxy Book 4 Pro" <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none"> <div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">CPU (프로세서)</label>
<input type="text" x-model="formData.cpu" placeholder="예: i5-1135G7"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">NPU (AI Boost)</label>
<input type="text" x-model="formData.npu" placeholder="예: Intel AI Boost"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">RAM</label>
<input type="text" x-model="formData.ram" placeholder="예: 16GB"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
</div>
</div> </div>
<!-- 저장 장치 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 bg-slate-50 p-4 rounded-2xl">
<div class="space-y-4">
<label class="block text-[10px] font-black text-slate-400 uppercase tracking-widest">Storage 0
(Main)</label>
<div class="grid grid-cols-2 gap-2">
<input type="text" x-model="formData.hdd0_model" placeholder="타입/모델"
class="w-full px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm outline-none">
<input type="text" x-model="formData.hdd0_capacity" placeholder="용량 (예: 512GB)"
class="w-full px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm outline-none">
</div>
</div>
<div class="space-y-4">
<label class="block text-[10px] font-black text-slate-400 uppercase tracking-widest">Storage 1
(Sub)</label>
<div class="grid grid-cols-2 gap-2">
<input type="text" x-model="formData.hdd1_model" placeholder="타입/모델"
class="w-full px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm outline-none">
<input type="text" x-model="formData.hdd1_capacity" placeholder="용량"
class="w-full px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm outline-none">
</div>
</div>
</div>
<!-- 자산 정보 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">노트북 상태</label>
<input type="text" x-model="formData.asset_status" placeholder="예: 정상"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">고정 IP</label>
<input type="text" x-model="formData.fixed_ip" placeholder="192.168.x.x"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">배정 사용자</label>
<input type="text" x-model="formData.assigned_user" placeholder="성함"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2 text-blue-600">취득일</label>
<input type="date" x-model="formData.purchase_date"
class="w-full px-4 py-3 bg-blue-50 border border-blue-100 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
</div>
</div>
<!-- 기타 사양 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">정격입출력</label>
<input type="text" x-model="formData.power_rating" placeholder="예: 20V / 3.25A"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">판매사</label>
<input type="text" x-model="formData.vendor" placeholder="예: 오픈마켓"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">옵션</label>
<input type="text" x-model="formData.options" placeholder="예: 지문인식"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
</div>
</div>
<div> <div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">상세 사양 (CPU, RAM, SSD )</label> <label class="block text-xs font-bold text-slate-500 uppercase mb-2">비고 (특기사항)</label>
<textarea x-model="formData.specs" rows="3" placeholder="예: Core Ultra 7 / 32GB / 1TB" <textarea x-model="formData.remarks" rows="2" placeholder="기타 참고할 사양 정보"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none resize-none"></textarea> class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none resize-none"></textarea>
</div> </div>
<div class="pt-4 flex gap-3"> <div class="pt-4 flex gap-3">
<button type="button" @click="showModal = false" <button type="button" @click="showModal = false"
class="flex-1 py-3 bg-slate-100 rounded-xl font-bold">취소</button> class="flex-1 py-4 bg-slate-100 rounded-2xl font-bold">취소</button>
<button type="submit" class="flex-1 py-3 bg-blue-600 text-white rounded-xl font-bold" <button type="submit"
class="flex-1 py-4 bg-blue-600 text-white rounded-2xl font-bold shadow-lg shadow-blue-200"
x-text="isEdit ? '수정 완료' : '모델 등록'"></button> x-text="isEdit ? '수정 완료' : '모델 등록'"></button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<!-- Dept Modal -->
<div x-show="showDeptModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
<div class="fixed inset-0 modal-bg" @click="showDeptModal = false"></div>
<div class="bg-white rounded-3xl p-8 max-w-md w-full relative z-[111] shadow-2xl">
<h3 class="text-2xl font-black mb-6" x-text="isDeptEdit ? '부서 정보 수정' : '신규 부서 생성'"></h3>
<form @submit.prevent="submitDept" class="space-y-4">
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">부서명</label>
<input type="text" x-model="deptFormData.name" required
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">상위 부서</label>
<select x-model="deptFormData.parent_id"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
<option value="">없음 (최상위)</option>
<template x-for="d in depts" :key="d.id">
<option :value="d.id" x-text="d.path" :disabled="d.id == deptFormData.id"></option>
</template>
</select>
</div>
<div class="pt-4 flex gap-3">
<button type="button" @click="showDeptModal = false"
class="flex-1 py-3 bg-slate-100 rounded-xl font-bold">취소</button>
<button type="submit" class="flex-1 py-3 bg-blue-600 text-white rounded-xl font-bold"
x-text="isDeptEdit ? '수정 완료' : '부서 생성'"></button>
</div>
</form>
</div>
</div>
<!-- Members Modal -->
<div x-show="showMembersModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
<div class="fixed inset-0 modal-bg" @click="showMembersModal = false"></div>
<div
class="bg-white rounded-3xl p-8 max-w-2xl w-full relative z-[111] shadow-2xl overflow-hidden flex flex-col max-h-[80vh]">
<div class="flex justify-between items-center mb-6">
<h3 class="text-2xl font-black text-slate-900" x-text="selectedDept?.name + ' 부서원'"></h3>
<span class="text-xs font-bold text-blue-600 bg-blue-50 px-3 py-1 rounded-full"><span
x-text="members.length"></span></span>
</div>
<div class="flex-1 overflow-y-auto pr-2 no-scrollbar">
<table class="min-w-full divide-y divide-slate-100">
<thead class="bg-slate-50 sticky top-0">
<tr>
<th class="px-4 py-3 text-left text-xs font-bold text-slate-500">이름</th>
<th class="px-4 py-3 text-left text-xs font-bold text-slate-500">사번/직위</th>
<th class="px-4 py-3 text-right text-xs font-bold text-slate-500">이동</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50">
<template x-for="user in members" :key="user.id">
<tr>
<td class="px-4 py-3 text-sm font-bold text-slate-900" x-text="user.name"></td>
<td class="px-4 py-3">
<div class="text-xs font-black text-slate-400" x-text="user.emp_id"></div>
<div class="text-[10px] text-blue-500 font-bold uppercase"
x-text="user.position || '-'"></div>
</td>
<td class="px-4 py-3 text-right">
<select @change="reassignMember(user.id, $event.target.value)"
class="text-xs bg-slate-100 border-none rounded-lg px-2 py-1 focus:ring-2 focus:ring-blue-500">
<option value="">부서 이동...</option>
<template x-for="d in depts" :key="d.id">
<option :value="d.id" x-text="d.name" :disabled="d.id == selectedDept.id">
</option>
</template>
</select>
</td>
</tr>
</template>
</tbody>
</table>
<div x-show="members.length === 0" class="py-10 text-center text-slate-400 italic text-sm">소속된 부서원이
없습니다.</div>
</div>
<div class="mt-6 pt-6 border-t border-slate-100 flex justify-end">
<button @click="showMembersModal = false"
class="px-6 py-2.5 bg-slate-900 text-white rounded-xl font-bold">닫기</button>
</div>
</div>
</div>
<script> <script>
function modelManagement() { function dbManagement() {
return { return {
currentTab: 'models',
// Model Data
models: [], models: [],
showModal: false, showModal: false,
isEdit: false, isEdit: false,
formData: { id: '', manufacturer: '', model_name: '', specs: '' }, selectedIds: [],
init() { this.fetchModels(); }, bulkManufacturer: '',
bulkProductName: '',
formData: {
id: '', manufacturer: '', model_name: '', product_name: '',
cpu: '', npu: '', ram: '',
hdd0_model: '', hdd0_capacity: '',
hdd1_model: '', hdd1_capacity: '',
asset_status: '', fixed_ip: '', assigned_user: '', purchase_date: '',
power_rating: '', vendor: '', options: '', remarks: '', specs: ''
},
// Dept Data
depts: [],
showDeptModal: false,
showMembersModal: false,
isDeptEdit: false,
selectedDept: null,
members: [],
deptFormData: { id: '', name: '', parent_id: '', level: 1 },
init() {
this.fetchModels();
this.fetchDepts();
},
// Model Methods
fetchModels() { fetchModels() {
const scrollPos = window.scrollY; const scrollPos = window.scrollY;
fetch('api.php?action=get_models').then(res => res.json()).then(data => { fetch('api.php?action=get_models').then(res => res.json()).then(data => {
@ -126,9 +475,37 @@
this.$nextTick(() => window.scrollTo(0, scrollPos)); this.$nextTick(() => window.scrollTo(0, scrollPos));
}); });
}, },
toggleSelectAll(checked) {
this.selectedIds = checked ? this.models.map(m => m.id) : [];
},
applyBulkUpdate() {
if (this.selectedIds.length === 0) return;
if (!this.bulkManufacturer && !this.bulkProductName) { alert('변경할 항목을 선택해주세요.'); return; }
fetch('api.php?action=bulk_update_models', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ids: this.selectedIds,
manufacturer: this.bulkManufacturer,
product_name: this.bulkProductName
})
}).then(res => res.json()).then(data => {
if (data.success) {
this.selectedIds = [];
this.bulkManufacturer = '';
this.bulkProductName = '';
this.fetchModels();
}
});
},
openAddModal() { openAddModal() {
this.isEdit = false; this.isEdit = false;
this.formData = { id: '', manufacturer: '', model_name: '', specs: '' }; this.formData = {
id: '', manufacturer: '', model_name: '', product_name: '',
cpu: '', npu: '', ram: '', hdd0_model: '', hdd0_capacity: '', hdd1_model: '', hdd1_capacity: '',
asset_status: '', fixed_ip: '', assigned_user: '', purchase_date: '',
power_rating: '', vendor: '', options: '', remarks: '', specs: ''
};
this.showModal = true; this.showModal = true;
}, },
editModel(model) { editModel(model) {
@ -148,6 +525,66 @@
this.fetchModels(); this.fetchModels();
} }
}); });
},
// Dept Methods
fetchDepts() {
fetch('api.php?action=get_departments').then(res => res.json()).then(data => {
this.depts = data;
});
},
getDeptLevel(path) {
if (!path) return 0;
return (path.match(/>/g) || []).length;
},
openAddDeptModal() {
this.isDeptEdit = false;
this.deptFormData = { id: '', name: '', parent_id: '', level: 1 };
this.showDeptModal = true;
},
editDept(dept) {
this.isDeptEdit = true;
this.deptFormData = { id: dept.id, name: dept.name, parent_id: dept.parent_id || '', level: dept.level };
this.showDeptModal = true;
},
submitDept() {
const action = this.isDeptEdit ? 'update_dept' : 'add_dept';
if (this.deptFormData.parent_id) {
const parent = this.depts.find(d => d.id == this.deptFormData.parent_id);
this.deptFormData.level = this.getDeptLevel(parent.path) + 1;
} else {
this.deptFormData.level = 0;
}
fetch(`api.php?action=${action}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.deptFormData)
}).then(res => res.json()).then(data => {
if (data.success) {
this.showDeptModal = false;
this.fetchDepts();
}
});
},
viewDeptMembers(dept) {
this.selectedDept = dept;
fetch(`api.php?action=get_dept_users&dept_id=${dept.id}`).then(res => res.json()).then(data => {
this.members = data;
this.showMembersModal = true;
});
},
reassignMember(userId, newDeptId) {
if (!newDeptId) return;
fetch('api.php?action=reassign_user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_ids: [userId], new_dept_id: newDeptId })
}).then(res => res.json()).then(data => {
if (data.success) {
this.viewDeptMembers(this.selectedDept);
this.fetchDepts();
}
});
} }
} }
} }

View file

@ -27,8 +27,8 @@ $current_page = basename($_SERVER['PHP_SELF']);
<?php <?php
$menu_items = [ $menu_items = [
['url' => 'index.php', 'name' => '대시보드', 'icon' => 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6'], ['url' => 'index.php', 'name' => '대시보드', 'icon' => 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6'],
['url' => 'rental.php', 'name' => '노트북 임대', 'icon' => 'M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4'],
['url' => 'users.php', 'name' => '직원 관리', 'icon' => 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z'], ['url' => 'users.php', 'name' => '직원 관리', 'icon' => 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z'],
['url' => 'departments.php', 'name' => '부서 관리', 'icon' => 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4'],
['url' => 'laptops.php', 'name' => '노트북 자산', 'icon' => 'M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z'], ['url' => 'laptops.php', 'name' => '노트북 자산', 'icon' => 'M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z'],
['url' => 'cards.php', 'name' => '출입증 현황', 'icon' => 'M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z'], ['url' => 'cards.php', 'name' => '출입증 현황', 'icon' => 'M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z'],
['url' => 'mfp.php', 'name' => '복합기 계정', 'icon' => 'M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z'], ['url' => 'mfp.php', 'name' => '복합기 계정', 'icon' => 'M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z'],

308
rental.php Normal file
View file

@ -0,0 +1,308 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>노트북 임대 관리 | FKI ASSET</title>
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/style.css">
<style>
body {
font-family: 'Pretendard', sans-serif;
}
[x-cloak] {
display: none !important;
}
.modal-bg {
background-color: rgba(15, 23, 42, 0.7);
backdrop-filter: blur(4px);
}
</style>
</head>
<body class="bg-[#f8fafc] text-slate-800" x-data="rentalManagement()">
<?php include 'nav.php'; ?>
<main class="max-w-[1600px] mx-auto p-8 pt-10">
<header class="mb-10 flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">노트북 단기 임대</h2>
<p class="text-slate-500 mt-1">업무용 공용 자산의 대여 반납 기록 관리 (사원별 임대 현황)</p>
</div>
<div class="flex items-center gap-3">
<div class="relative">
<input type="text" x-model="searchQuery" placeholder="자산번호, 모델명 검색..."
class="pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 outline-none w-64 shadow-sm">
<svg class="w-4 h-4 absolute left-3.5 top-3 text-slate-400" 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>
</div>
</header>
<!-- Rental List Table -->
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden">
<table class="min-w-full divide-y divide-slate-100">
<thead class="bg-slate-50/50">
<tr>
<th @click="sortBy('asset_tag')"
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
자산번호 <span x-show="sortKey === 'asset_tag'" x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
</th>
<th @click="sortBy('model_name')"
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
모델 정보 <span x-show="sortKey === 'model_name'"
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
</th>
<th @click="sortBy('rental_user_name')"
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors text-blue-600">
현재 임대사원 <span x-show="sortKey === 'rental_user_name'"
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">
비고(특기사항)</th>
<th class="px-6 py-4 text-right text-xs font-bold text-slate-500 uppercase tracking-wider">액션
</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50">
<template x-for="item in sortedAssets" :key="item.id">
<tr class="hover:bg-slate-50/80 transition-colors group">
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-sm font-black text-slate-900 px-2.5 py-1 bg-slate-100 rounded-lg"
x-text="item.asset_tag"></span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-bold text-slate-800" x-text="item.model_name"></div>
<div class="text-[10px] text-slate-400 font-black uppercase tracking-widest"
x-text="item.manufacturer"></div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<template x-if="item.rental_user_name">
<div class="flex items-center">
<div class="w-2 h-2 bg-emerald-500 rounded-full mr-2"></div>
<span class="text-sm font-black text-slate-900"
x-text="item.rental_user_name"></span>
</div>
</template>
<template x-if="!item.rental_user_name">
<span class="text-xs text-slate-300 italic font-medium">임대 가능</span>
</template>
</td>
<td class="px-6 py-4">
<div class="text-xs text-slate-500 truncate max-w-xs" x-text="item.remarks || '-'">
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<div class="flex justify-end gap-2">
<button @click="openHistoryModal(item)"
class="px-3 py-1.5 text-[11px] font-black text-slate-500 hover:text-slate-900 bg-slate-100 hover:bg-slate-200 rounded-lg transition-all uppercase">History</button>
<button @click="openRentalModal(item)"
:class="item.rental_user_name ? 'bg-amber-100 text-amber-600 hover:bg-amber-200' : 'bg-blue-600 text-white hover:bg-blue-700'"
class="px-4 py-1.5 text-[11px] font-black rounded-lg transition-all uppercase shadow-sm"
x-text="item.rental_user_name ? 'Change/Return' : 'Start Rental'"></button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
<div x-show="filteredAssets.length === 0" class="py-20 text-center">
<div class="text-slate-300 text-5xl mb-4 italic">No Assets Found</div>
<p class="text-slate-400 text-sm font-medium">배정 사용자가 '업무용' 노트북이 없거나 검색 결과가 없습니다.</p>
</div>
</div>
</main>
<!-- Rental Modal -->
<div x-show="showRentalModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
<div class="fixed inset-0 modal-bg" @click="showRentalModal = false"></div>
<div class="bg-white rounded-3xl p-8 max-w-md w-full relative z-[111] shadow-2xl">
<h3 class="text-2xl font-black mb-2" x-text="selectedAsset?.asset_tag + ' 임대 설정'"></h3>
<p class="text-slate-400 text-xs font-bold mb-6" x-text="selectedAsset?.model_name"></p>
<form @submit.prevent="submitRental" class="space-y-4">
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">임대사원 성함</label>
<input type="text" x-model="rentalName" required placeholder="임대받는 사원의 이름을 입력하세요"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
<p class="mt-2 text-[10px] text-slate-400 italic">* 공란으로 입력 적용 반납 처리됩니다.</p>
</div>
<div class="pt-4 flex gap-3">
<button type="button" @click="showRentalModal = false"
class="flex-1 py-3 bg-slate-100 rounded-xl font-bold">취소</button>
<button type="submit"
class="flex-1 py-3 bg-blue-600 text-white rounded-xl font-bold shadow-lg shadow-blue-200">임대
적용</button>
</div>
</form>
</div>
</div>
<!-- History Modal -->
<div x-show="showHistoryModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
<div class="fixed inset-0 modal-bg" @click="showHistoryModal = false"></div>
<div class="bg-white rounded-3xl p-8 max-w-2xl w-full relative z-[111] shadow-2xl 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" x-text="selectedAsset?.asset_tag + ' 이력 로그'"></h3>
<p class="text-slate-400 text-xs font-bold" x-text="selectedAsset?.model_name"></p>
</div>
<button @click="showHistoryModal = false" class="text-slate-400 hover:text-slate-900">
<svg class="w-6 h-6" 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 space-y-8">
<!-- Part 1: Rental Logs -->
<div>
<div class="flex items-center gap-3 mb-4">
<div class="h-px bg-slate-100 flex-1"></div>
<h4 class="text-[10px] font-black text-emerald-500 uppercase tracking-[0.2em]">01. PC 임대 기록 (Max
20)</h4>
<div class="h-px bg-slate-100 flex-1"></div>
</div>
<div class="space-y-2">
<template x-for="log in history.rental" :key="log.id">
<div
class="flex items-center justify-between p-3 bg-emerald-50 rounded-xl border border-emerald-100">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-white rounded-lg flex items-center justify-center text-[10px] font-black text-emerald-500 shadow-sm"
x-text="log.user_name.charAt(0)"></div>
<span class="text-sm font-black text-slate-900" x-text="log.user_name"></span>
</div>
<span class="text-[10px] font-bold text-emerald-600 bg-white px-2 py-1 rounded-md"
x-text="log.action_date"></span>
</div>
</template>
<div x-show="history.rental.length === 0"
class="text-center py-6 text-slate-300 italic text-xs">임대 기록이 없습니다.</div>
</div>
</div>
<!-- Part 2: Assignment Logs -->
<div>
<div class="flex items-center gap-3 mb-4">
<div class="h-px bg-slate-100 flex-1"></div>
<h4 class="text-[10px] font-black text-blue-500 uppercase tracking-[0.2em]">02. PC 배정 기록 (Max
20)</h4>
<div class="h-px bg-slate-100 flex-1"></div>
</div>
<div class="space-y-2">
<template x-for="log in history.assignment" :key="log.id">
<div
class="flex items-center justify-between p-3 bg-blue-50 rounded-xl border border-blue-100">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-white rounded-lg flex items-center justify-center text-[10px] font-black text-blue-500 shadow-sm"
x-text="log.user_name.charAt(0)"></div>
<span class="text-sm font-black text-slate-900" x-text="log.user_name"></span>
</div>
<span class="text-[10px] font-bold text-blue-600 bg-white px-2 py-1 rounded-md"
x-text="log.action_date"></span>
</div>
</template>
<div x-show="history.assignment.length === 0"
class="text-center py-6 text-slate-300 italic text-xs">배정 기록이 없습니다.</div>
</div>
</div>
</div>
</div>
</div>
<script>
function rentalManagement() {
return {
assets: [],
searchQuery: '',
sortKey: 'asset_tag',
sortOrder: 'asc',
showRentalModal: false,
showHistoryModal: false,
selectedAsset: null,
rentalName: '',
history: { rental: [], assignment: [] },
init() {
this.fetchAssets();
},
fetchAssets() {
fetch('api.php?action=get_rental_laptops').then(res => res.json()).then(data => {
this.assets = data;
});
},
get filteredAssets() {
if (!this.searchQuery) return this.assets;
const q = this.searchQuery.toLowerCase();
return this.assets.filter(a =>
a.asset_tag.toLowerCase().includes(q) ||
a.model_name.toLowerCase().includes(q) ||
(a.rental_user_name && a.rental_user_name.toLowerCase().includes(q))
);
},
get sortedAssets() {
return [...this.filteredAssets].sort((a, b) => {
let v1 = a[this.sortKey] || '';
let v2 = b[this.sortKey] || '';
if (this.sortOrder === 'asc') return v1 > v2 ? 1 : -1;
return v1 < v2 ? 1 : -1;
});
},
sortBy(key) {
if (this.sortKey === key) {
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
} else {
this.sortKey = key;
this.sortOrder = 'asc';
}
},
openRentalModal(asset) {
this.selectedAsset = asset;
this.rentalName = asset.rental_user_name || '';
this.showRentalModal = true;
},
submitRental() {
fetch('api.php?action=update_rental', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: this.selectedAsset.id,
rental_user_name: this.rentalName
})
}).then(res => res.json()).then(data => {
if (data.success) {
this.showRentalModal = false;
this.fetchAssets();
}
});
},
openHistoryModal(asset) {
this.selectedAsset = asset;
fetch(`api.php?action=get_asset_history&asset_id=${asset.id}`).then(res => res.json()).then(data => {
this.history = data;
this.showHistoryModal = true;
});
}
}
}
</script>
</body>
</html>