This commit is contained in:
NAS-Admin 2026-03-04 22:21:38 +09:00
parent bd1ea046b1
commit 9ab5065026
16 changed files with 792 additions and 224 deletions

76
FIX_AND_MIGRATE.php Normal file
View file

@ -0,0 +1,76 @@
<?php
/**
* Synology Autonomous Architect - Deep Migration & Repair Tool
* Target: dda/assets.db
* Function: Fix Mojibake/URL-encoding in names & Apply latest schema
*/
header('Content-Type: text/plain; charset=utf-8');
try {
$dbPath = __DIR__ . DIRECTORY_SEPARATOR . 'dda' . DIRECTORY_SEPARATOR . 'assets.db';
if (!file_exists($dbPath)) {
throw new Exception("File not found at $dbPath");
}
$db = new PDO("sqlite:$dbPath");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
echo "--- 🛠 심층 마이그레이션 및 데이터 복구 시작 ---\n";
// 1. 스키마 강제 보정
$newColumns = [
'disposal_recipient' => 'TEXT',
'disposal_date' => 'TEXT',
'real_user' => 'TEXT',
'disposal_user_id' => 'INTEGER'
];
foreach ($newColumns as $col => $type) {
try {
$db->exec("ALTER TABLE laptop_assets ADD COLUMN $col $type");
echo "✅ 컬럼 보정: $col\n";
} catch (PDOException $e) {
// Already exists or other error
}
}
// 2. 인코딩 복구 (URL-Encoded 데이터가 식별되어 수정 시도)
$stmt = $db->query("SELECT id, assigned_user_name, disposal_recipient FROM laptop_assets");
$assets = $stmt->fetchAll(PDO::FETCH_ASSOC);
$fixCount = 0;
$updateStmt = $db->prepare("UPDATE laptop_assets SET assigned_user_name = ?, disposal_recipient = ? WHERE id = ?");
foreach ($assets as $asset) {
$uName = $asset['assigned_user_name'];
$dRec = $asset['disposal_recipient'];
$newUName = $uName;
$newDRec = $dRec;
// URL 인코딩 탐지 및 변환
if ($uName && strpos($uName, '%') !== false) {
$newUName = urldecode($uName);
}
if ($dRec && strpos($dRec, '%') !== false) {
$newDRec = urldecode($dRec);
}
if ($newUName !== $uName || $newDRec !== $dRec) {
$updateStmt->execute([$newUName, $newDRec, $asset['id']]);
$fixCount++;
}
}
echo "✅ Mojibake/URL-Encoding 데이터 복구: {$fixCount}건 수정 완료.\n";
// 3. STOCK 자산 명칙 동기화
$stmtStock = $db->prepare("UPDATE laptop_assets SET assigned_user_name = '업무용(TEMP_44666b)', current_user_id = NULL WHERE status = 'stock'");
$stmtStock->execute();
echo "✅ 재고(STOCK) 명칭 동기화 완료.\n";
echo "--- 🎉 모든 복구 및 마이그레이션이 완료되었습니다. ---";
} catch (Exception $e) {
echo "❌ 오류: " . $e->getMessage() . "\n";
}
?>

89
RECOVER_MIGRATE.php Normal file
View file

@ -0,0 +1,89 @@
<?php
/**
* Synology Autonomous Architect - Database Migration Script (v2.1)
* Target: dda/assets.db (Legacy/Production Recovery Path)
* Date: 2026-03-04
*/
header('Content-Type: text/plain; charset=utf-8');
try {
// 1. DB 연결 (dda 폴더 내부로 경로 수정)
$dbPath = __DIR__ . DIRECTORY_SEPARATOR . 'dda' . DIRECTORY_SEPARATOR . 'assets.db';
if (!file_exists($dbPath)) {
throw new Exception("데이터베이스 파일({$dbPath})을 찾을 수 없습니다. dda/assets.db 확인이 필요합니다.");
}
$db = new PDO("sqlite:$dbPath");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
echo "--- [복구 마이그레이션 시작] 노트북 자산 관리 시스템 v2.1 ---\n\n";
// 2. 백업 생성
$backupPath = $dbPath . '.bak_' . date('Ymd_His');
if (!copy($dbPath, $backupPath)) {
throw new Exception("데이터베이스 백업 생성 실패");
}
echo "🛡️ 데이터 안전: 작업 전 원본 백업이 생성되었습니다. ($backupPath)\n";
// 3. 스키마 확장 (신규 규격 컬럼 추가)
$newColumns = [
'disposal_recipient' => 'TEXT',
'disposal_date' => 'TEXT',
'real_user' => 'TEXT',
'disposal_user_id' => 'INTEGER'
];
foreach ($newColumns as $col => $type) {
try {
$db->exec("ALTER TABLE laptop_assets ADD COLUMN $col $type");
echo "✅ 컬럼 보정 성공: $col\n";
} catch (PDOException $e) {
if (strpos($e->getMessage(), 'duplicate column name') !== false) {
echo " 이미 존재함: $col (스키마 유지)\n";
} else {
throw $e;
}
}
}
// 4. 데이터 정합성 - STOCK 자산 '업무용' 명칭 통합
$stmtStock = $db->prepare("
UPDATE laptop_assets
SET assigned_user_name = '업무용(TEMP_44666b)',
current_user_id = NULL
WHERE status = 'stock'
AND (assigned_user_name != '업무용(TEMP_44666b)' OR assigned_user_name IS NULL)
");
$stmtStock->execute();
$affectedStock = $stmtStock->rowCount();
echo "✅ 데이터 정합성: STOCK 자산 $affectedStock 건의 입력을 '업무용'으로 통일했습니다.\n";
// 5. 데이터 정합성 - 매각 대상자 ID 복구 (직원 DB 대조)
$allDisposed = $db->query("
SELECT id, disposal_recipient
FROM laptop_assets
WHERE status = 'disposed'
AND disposal_recipient IS NOT NULL
AND disposal_user_id IS NULL
")->fetchAll(PDO::FETCH_ASSOC);
$idRestored = 0;
$userFinder = $db->prepare("SELECT id FROM users WHERE name = ? LIMIT 1");
$idUpdater = $db->prepare("UPDATE laptop_assets SET disposal_user_id = ? WHERE id = ?");
foreach ($allDisposed as $row) {
$userFinder->execute([$row['disposal_recipient']]);
$uid = $userFinder->fetchColumn();
if ($uid) {
$idUpdater->execute([$uid, $row['id']]);
$idRestored++;
}
}
echo "✅ 데이터 정합성: DISPOSED 자산 $idRestored 건의 사원 매핑을 완료했습니다.\n\n";
echo "--- [마이그레이션 성공] 복구된 DB가 최신 규격으로 업데이트되었습니다. ---\n";
} catch (Exception $e) {
echo "\n❌ [마이그레이션 실패] 오류 내용: " . $e->getMessage() . "\n";
}

81
api.php
View file

@ -669,34 +669,62 @@ LIMIT 1")->fetchColumn();
} }
$updates = []; $updates = [];
$params = []; $params = [];
$log_msg = "";
if ($newStatus) { if ($newStatus) {
$updates[] = "status = ?"; $updates[] = "status = ?";
$params[] = $newStatus; $params[] = $newStatus;
// 만약 상태를 'stock'(재고)으로 변경하는 경우 '업무용'으로 설정, 'disposed'(매각)인 경우 완전 초기화
if ($newStatus === 'stock') { if ($newStatus === 'stock') {
$updates[] = "current_user_id = NULL"; $updates[] = "current_user_id = NULL";
$updates[] = "assigned_user_name = '업무용(TEMP_44666b)'"; $updates[] = "assigned_user_name = '업무용(TEMP_44666b)'";
$log_msg = "일괄 상태 변경: 재고 (업무용 지정)";
} elseif ($newStatus === 'disposed') { } elseif ($newStatus === 'disposed') {
$updates[] = "current_user_id = NULL"; $updates[] = "current_user_id = NULL";
$updates[] = "assigned_user_name = NULL"; $updates[] = "assigned_user_name = NULL";
$updates[] = "disposal_date = '" . date('Y-m-d') . "'"; $updates[] = "disposal_date = '" . date('Y-m-d') . "'";
$log_msg = "일괄 상태 변경: 매각됨";
} elseif ($newStatus === 'assigned') {
$log_msg = "일괄 상태 변경: 직원배정";
} }
} }
if ($newModelId) { if ($newModelId) {
$updates[] = "model_id = ?"; $updates[] = "model_id = ?";
$params[] = $newModelId; $params[] = $newModelId;
$log_msg .= ($log_msg ? ", " : "") . "일괄 모델 변경(ID: $newModelId)";
} }
if (empty($updates)) { if (empty($updates)) {
echo json_encode(['success' => false, 'error' => 'No updates specified']); echo json_encode(['success' => false, 'error' => 'No updates specified']);
break; break;
} }
$sql = "UPDATE laptop_assets SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(
0, $placeholders = implode(",", array_fill(0, count($ids), "?"));
count($ids), $sql = "UPDATE laptop_assets SET " . implode(", ", $updates) . " WHERE id IN ($placeholders)";
"?"
)) . ")";
$stmt = $db->prepare($sql); $stmt = $db->prepare($sql);
$stmt->execute(array_merge($params, $ids)); $stmt->execute(array_merge($params, $ids));
// 히스토리 일괄 기록
if ($log_msg) {
$history_sql = "INSERT INTO asset_history (asset_id, log_type, user_name, action_date, note) VALUES (?, 'bulk_update', '시스템(일괄)', ?, ?)";
$h_stmt = $db->prepare($history_sql);
foreach ($ids as $id) {
$h_stmt->execute([$id, date('Y-m-d'), $log_msg]);
}
}
echo json_encode(['success' => true]);
break;
case 'bulk_delete_laptops':
$data = json_decode(file_get_contents('php://input'), true);
$ids = $data['ids'];
if (empty($ids)) {
echo json_encode(['success' => false, 'error' => 'No items selected']);
break;
}
$placeholders = implode(",", array_fill(0, count($ids), "?"));
$db->prepare("DELETE FROM laptop_assets WHERE id IN ($placeholders)")->execute($ids);
// 관련 히스토리도 삭제? 보통 자산 삭제시에는 히스토리도 함께 날리거나 남김. 여기선 DB 정합성을 위해 연쇄 삭제는 안하더라도 자산은 사라짐.
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
break; break;
@ -799,6 +827,33 @@ LIMIT 1")->fetchColumn();
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
break; break;
case 'bulk_delete_models':
$data = json_decode(file_get_contents('php://input'), true);
$ids = $data['ids'];
if (empty($ids)) {
echo json_encode(['success' => false, 'error' => 'No items selected']);
break;
}
try {
$db->beginTransaction();
$placeholders = implode(",", array_fill(0, count($ids), "?"));
// 1. 해당 모델을 참조 중인 자산들의 연결 고리 해제 (ID 참조를 NULL로 변경)
$stmt1 = $db->prepare("UPDATE laptop_assets SET model_id = NULL WHERE model_id IN ($placeholders)");
$stmt1->execute($ids);
// 2. 노트북 모델 마스터 정보 삭제
$stmt2 = $db->prepare("DELETE FROM laptop_models WHERE id IN ($placeholders)");
$stmt2->execute($ids);
$db->commit();
echo json_encode(['success' => true]);
} catch (Exception $e) {
$db->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
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'];
@ -1033,6 +1088,20 @@ ORDER BY r.id DESC";
} }
break; break;
case 'get_settings':
$settings = $db->query("SELECT key_name, value FROM system_settings")->fetchAll(PDO::FETCH_KEY_PAIR);
echo json_encode(['success' => true, 'settings' => $settings]);
break;
case 'update_settings':
$data = json_decode(file_get_contents('php://input'), true);
$stmt = $db->prepare("INSERT OR REPLACE INTO system_settings (key_name, value) VALUES (?, ?)");
foreach ($data['settings'] as $key => $value) {
$stmt->execute([$key, (string) $value]);
}
echo json_encode(['success' => true]);
break;
case 'return_general_rental': case 'return_general_rental':
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
$returnDate = date('Y-m-d'); $returnDate = date('Y-m-d');

View file

@ -1,4 +1,19 @@
<?php <?php
$db = new PDO('sqlite:d:\Docker\jasan\assets.db'); try {
$cols = $db->query("PRAGMA table_info(laptop_assets)")->fetchAll(PDO::FETCH_ASSOC); $db = new PDO('sqlite:d:/Docker/jasan/dda/assets.db');
echo json_encode($cols, JSON_PRETTY_PRINT); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
echo "--- LAPTOP_ASSETS SCHEMA ---\n";
$res = $db->query('PRAGMA table_info(laptop_assets)')->fetchAll();
foreach ($res as $col) {
echo "{$col['name']} ({$col['type']})\n";
}
echo "\n--- LAPTOP_MODELS SCHEMA ---\n";
$res = $db->query('PRAGMA table_info(laptop_models)')->fetchAll();
foreach ($res as $col) {
echo "{$col['name']} ({$col['type']})\n";
}
} catch (Exception $e) {
echo $e->getMessage();
}

9
check_tables.php Normal file
View file

@ -0,0 +1,9 @@
<?php
try {
$db = new PDO('sqlite:d:/Docker/jasan/dda/assets.db');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$tables = $db->query("SELECT name FROM sqlite_master WHERE type='table'")->fetchAll(PDO::FETCH_COLUMN);
print_r($tables);
} catch (Exception $e) {
echo $e->getMessage();
}

Binary file not shown.

BIN
dda/assets.db.back Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

5
debug_db.php Normal file
View file

@ -0,0 +1,5 @@
<?php
$db = new PDO('sqlite:d:\Docker\jasan\dda\assets.db');
$rows = $db->query("SELECT id, asset_tag, status, assigned_user_name FROM laptop_assets LIMIT 50")->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($rows, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
?>

View file

@ -67,102 +67,118 @@
</div> </div>
</header> </header>
<!-- Rental Cards Grid --> <!-- Table List -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6" <div class="bg-white rounded-[2.5rem] shadow-sm border border-slate-200 overflow-hidden"
x-show="!loading && filteredRentals.length > 0"> x-show="!loading && sortedRentals.length > 0">
<template x-for="rental in filteredRentals" :key="rental.id"> <table class="min-w-full divide-y divide-slate-100">
<div class="bg-white rounded-[2rem] p-6 shadow-sm border border-slate-100 hover:shadow-xl transition-all relative overflow-hidden flex flex-col h-full" <thead class="bg-slate-50/50">
:class="rental.status === 'returned' ? 'opacity-75' : ''"> <tr>
<th @click="sortBy('user_name')"
<!-- Status Ribbon for Returned --> class="px-6 py-4 text-left text-[11px] font-black text-slate-400 uppercase tracking-widest cursor-pointer hover:bg-slate-100 transition-colors">
<template x-if="rental.status === 'returned'"> <div class="flex items-center gap-1">
<div class="absolute top-0 right-0"> 사원명
<div <svg class="w-3 h-3 transition-transform"
class="bg-slate-500 p-1 px-4 text-[10px] font-black text-white uppercase transform rotate-45 translate-x-3 translate-y-1 shadow-sm"> :class="sortKey === 'user_name' ? (sortOrder === 'asc' ? 'rotate-180' : '') : 'opacity-20'"
Returned fill="none" stroke="currentColor" viewBox="0 0 24 24">
</div> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3"
</div> d="M19 9l-7 7-7-7" />
</template>
<div class="flex items-center gap-4 mb-5">
<div class="w-12 h-12 rounded-2xl flex items-center justify-center shadow-inner"
:class="rental.status === 'returned' ? 'bg-slate-100 text-slate-400' : 'bg-emerald-50 text-emerald-500'">
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg> </svg>
</div> </div>
<div> </th>
<h4 class="text-lg font-black text-slate-900" x-text="rental.user_name"></h4> <th class="px-6 py-4 text-left text-[11px] font-black text-slate-400 uppercase tracking-widest">
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Rental Employee 임대 품목</th>
</p> <th @click="sortBy('rental_start_date')"
class="px-6 py-4 text-left text-[11px] font-black text-slate-400 uppercase tracking-widest cursor-pointer hover:bg-slate-100 transition-colors">
<div class="flex items-center gap-1">
임대일
<svg class="w-3 h-3 transition-transform"
:class="sortKey === 'rental_start_date' ? (sortOrder === 'asc' ? 'rotate-180' : '') : 'opacity-20'"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3"
d="M19 9l-7 7-7-7" />
</svg>
</div> </div>
</th>
<th @click="sortBy('status')"
class="px-6 py-4 text-left text-[11px] font-black text-slate-400 uppercase tracking-widest cursor-pointer hover:bg-slate-100 transition-colors">
<div class="flex items-center gap-1">
상태/경과
<svg class="w-3 h-3 transition-transform"
:class="sortKey === 'status' ? (sortOrder === 'asc' ? 'rotate-180' : '') : 'opacity-20'"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3"
d="M19 9l-7 7-7-7" />
</svg>
</div> </div>
</th>
<div class="flex-1 space-y-4"> <th class="px-6 py-4 text-left text-[11px] font-black text-slate-400 uppercase tracking-widest">
<!-- Item List --> 사유</th>
<div> <th
<div class="px-6 py-4 text-right text-[11px] font-black text-slate-400 uppercase tracking-widest">
class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 flex items-center gap-2"> 관리</th>
<div class="w-1 h-3 bg-blue-500 rounded-full"></div> </tr>
Rental Items </thead>
<tbody class="divide-y divide-slate-50">
<template x-for="rental in sortedRentals" :key="rental.id">
<tr class="hover:bg-slate-50/80 transition-colors group"
:class="rental.status === 'returned' ? 'opacity-60 bg-slate-50/30' : ''">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-lg flex items-center justify-center text-xs font-black"
:class="rental.status === 'returned' ? 'bg-slate-100 text-slate-400' : 'bg-emerald-50 text-emerald-600'">
<span x-text="rental.user_name.charAt(0)"></span>
</div> </div>
<div class="flex flex-wrap gap-1.5"> <span class="text-sm font-black text-slate-900" x-text="rental.user_name"></span>
<template x-for="item in rental.items.split(', ')" :key="item"> </div>
</td>
<td class="px-6 py-4">
<div class="flex flex-wrap gap-1">
<template x-for="item in (rental.items ? rental.items.split(', ') : [])"
:key="item">
<span <span
class="px-2 py-1 bg-blue-50 text-blue-600 text-[11px] font-black rounded-lg border border-blue-100" class="px-2 py-0.5 bg-blue-50 text-blue-600 text-[10px] font-black rounded-md border border-blue-100"
x-text="item"></span> x-text="item"></span>
</template> </template>
</div> </div>
</div> </td>
<td class="px-6 py-4 whitespace-nowrap">
<!-- Date Info --> <span class="text-xs font-bold text-slate-600" x-text="rental.rental_start_date"></span>
<div class="grid grid-cols-2 gap-4 pt-2 border-t border-slate-50"> </td>
<div> <td class="px-6 py-4 whitespace-nowrap">
<div class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Start
Date</div>
<div class="text-sm font-bold text-slate-700" x-text="rental.rental_start_date"></div>
</div>
<div>
<div class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Status
</div>
<template x-if="rental.status === 'rented'"> <template x-if="rental.status === 'rented'">
<div class="text-xs font-black text-blue-600 flex items-center gap-1.5"> <div
<span class="w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse"></span> class="inline-flex items-center gap-1.5 px-2.5 py-1 bg-blue-50 text-blue-600 rounded-full">
<span x-text="calculateElapsed(rental.rental_start_date) + '일째'"></span> <span class="w-1 h-1 bg-blue-500 rounded-full animate-pulse"></span>
<span class="text-[10px] font-black"
x-text="calculateElapsed(rental.rental_start_date) + '일째'"></span>
</div> </div>
</template> </template>
<template x-if="rental.status === 'returned'"> <template x-if="rental.status === 'returned'">
<div class="text-xs font-black text-slate-400" <div class="inline-flex items-center bg-slate-100 px-2.5 py-1 rounded-full">
x-text="'반납 (' + rental.rental_return_date + ')'"></div> <span class="text-[10px] font-black text-slate-500"
</template> x-text="'반납 (' + rental.rental_return_date + ')'"></span>
</div>
</div>
<!-- Reason -->
<template x-if="rental.rental_reason">
<div class="bg-slate-50 p-3 rounded-xl border border-slate-100 mt-2">
<div class="text-[9px] font-black text-slate-400 uppercase mb-1 tracking-wider">Reason
</div>
<div class="text-[11px] text-slate-600 font-bold whitespace-pre-wrap leading-relaxed"
x-text="rental.rental_reason"></div>
</div> </div>
</template> </template>
</div> </td>
<td class="px-6 py-4">
<!-- Return Button --> <p class="text-[11px] text-slate-500 font-medium truncate max-w-[200px]"
:title="rental.rental_reason" x-text="rental.rental_reason || '-'"></p>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<template x-if="rental.status === 'rented'"> <template x-if="rental.status === 'rented'">
<button @click="returnRental(rental)" <button @click="returnRental(rental)"
class="mt-6 w-full py-3.5 bg-slate-900 text-white rounded-2xl font-black text-sm shadow-xl hover:bg-slate-800 transition-all active:scale-95 flex items-center justify-center gap-2"> class="px-4 py-1.5 bg-slate-900 text-white rounded-xl font-black text-[10px] hover:bg-blue-600 transition-all active:scale-95">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 16l-4-4m0 0l4-4m-4 4h18" />
</svg>
반납 처리 반납 처리
</button> </button>
</template> </template>
</div> <template x-if="rental.status === 'returned'">
<span class="text-[10px] font-bold text-slate-300">처리완료</span>
</template> </template>
</td>
</tr>
</template>
</tbody>
</table>
</div> </div>
<div x-show="loading" class="p-20 flex justify-center"> <div x-show="loading" class="p-20 flex justify-center">
@ -315,6 +331,8 @@
rental_reason: '', rental_reason: '',
items: [''] items: ['']
}, },
sortKey: 'rental_start_date',
sortOrder: 'desc',
userSearchQuery: '', userSearchQuery: '',
showUserDropdown: false, showUserDropdown: false,
@ -361,10 +379,29 @@
const q = this.searchQuery.toLowerCase(); const q = this.searchQuery.toLowerCase();
return this.rentals.filter(r => return this.rentals.filter(r =>
(r.user_name && r.user_name.toLowerCase().includes(q)) || (r.user_name && r.user_name.toLowerCase().includes(q)) ||
(r.items && r.items.toLowerCase().includes(q)) (r.items && r.items.toLowerCase().includes(q)) ||
(r.rental_reason && r.rental_reason.toLowerCase().includes(q))
); );
}, },
get sortedRentals() {
return [...this.filteredRentals].sort((a, b) => {
let modifier = this.sortOrder === 'asc' ? 1 : -1;
if (a[this.sortKey] < b[this.sortKey]) return -1 * modifier;
if (a[this.sortKey] > b[this.sortKey]) return 1 * modifier;
return 0;
});
},
sortBy(key) {
if (this.sortKey === key) {
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
} else {
this.sortKey = key;
this.sortOrder = 'asc';
}
},
fetchUsers() { fetchUsers() {
fetch('api.php?action=get_users').then(res => res.json()).then(data => { fetch('api.php?action=get_users').then(res => res.json()).then(data => {
this.users = data; this.users = data;

View file

@ -60,6 +60,7 @@
</div> </div>
</div> </div>
<?php if (($settings['menu_cards_visible'] ?? '1') === '1'): ?>
<div <div
class="bg-white p-7 rounded-3xl shadow-sm border border-slate-100 hover:shadow-xl hover:-translate-y-1 transition-all"> class="bg-white p-7 rounded-3xl shadow-sm border border-slate-100 hover:shadow-xl hover:-translate-y-1 transition-all">
<div class="flex justify-between mb-6"> <div class="flex justify-between mb-6">
@ -76,7 +77,9 @@
class="text-slate-400 font-medium tracking-tighter">/ <span class="text-slate-400 font-medium tracking-tighter">/ <span
x-text="stats.cards.total || 0">0</span></span></div> x-text="stats.cards.total || 0">0</span></span></div>
</div> </div>
<?php endif; ?>
<?php if (($settings['menu_mfp_visible'] ?? '1') === '1'): ?>
<div <div
class="bg-white p-7 rounded-3xl shadow-sm border border-slate-100 hover:shadow-xl hover:-translate-y-1 transition-all"> class="bg-white p-7 rounded-3xl shadow-sm border border-slate-100 hover:shadow-xl hover:-translate-y-1 transition-all">
<div class="flex justify-between mb-6"> <div class="flex justify-between mb-6">
@ -93,6 +96,7 @@
class="text-slate-400 font-medium tracking-tighter">/ <span class="text-slate-400 font-medium tracking-tighter">/ <span
x-text="stats.mfp.total || 0">0</span></span></div> x-text="stats.mfp.total || 0">0</span></span></div>
</div> </div>
<?php endif; ?>
<div <div
class="bg-white p-7 rounded-3xl shadow-sm border border-slate-100 hover:shadow-xl hover:-translate-y-1 transition-all"> class="bg-white p-7 rounded-3xl shadow-sm border border-slate-100 hover:shadow-xl hover:-translate-y-1 transition-all">
@ -147,6 +151,7 @@
<p class="text-xs text-slate-400">자산태그/배정현황</p> <p class="text-xs text-slate-400">자산태그/배정현황</p>
</div> </div>
</a> </a>
<?php if (($settings['menu_cards_visible'] ?? '1') === '1'): ?>
<a href="cards.php" <a href="cards.php"
class="group bg-white p-6 rounded-2xl border border-slate-200 hover:border-blue-500 transition-all flex items-center space-x-4"> class="group bg-white p-6 rounded-2xl border border-slate-200 hover:border-blue-500 transition-all flex items-center space-x-4">
<div class="bg-slate-50 p-3 rounded-xl group-hover:bg-emerald-50 transition-colors"><svg <div class="bg-slate-50 p-3 rounded-xl group-hover:bg-emerald-50 transition-colors"><svg
@ -160,6 +165,9 @@
<p class="text-xs text-slate-400">보안실 연동 데이터</p> <p class="text-xs text-slate-400">보안실 연동 데이터</p>
</div> </div>
</a> </a>
<?php endif; ?>
<?php if (($settings['menu_mfp_visible'] ?? '1') === '1'): ?>
<a href="mfp.php" <a href="mfp.php"
class="group bg-white p-6 rounded-2xl border border-slate-200 hover:border-blue-500 transition-all flex items-center space-x-4"> class="group bg-white p-6 rounded-2xl border border-slate-200 hover:border-blue-500 transition-all flex items-center space-x-4">
<div class="bg-slate-50 p-3 rounded-xl group-hover:bg-amber-50 transition-colors"><svg <div class="bg-slate-50 p-3 rounded-xl group-hover:bg-amber-50 transition-colors"><svg
@ -173,6 +181,7 @@
<p class="text-xs text-slate-400">임시 계정/pw 관리</p> <p class="text-xs text-slate-400">임시 계정/pw 관리</p>
</div> </div>
</a> </a>
<?php endif; ?>
</div> </div>
</section> </section>
</main> </main>

17
init_settings.php Normal file
View file

@ -0,0 +1,17 @@
<?php
require_once 'config.php';
try {
$db->exec("CREATE TABLE IF NOT EXISTS system_settings (
key_name TEXT PRIMARY KEY,
value TEXT
)");
// 기본값 설정 (이미 있으면 무시)
$stmt = $db->prepare("INSERT OR IGNORE INTO system_settings (key_name, value) VALUES (?, ?)");
$stmt->execute(['menu_cards_visible', '1']);
$stmt->execute(['menu_mfp_visible', '1']);
echo "✅ system_settings table created and initialized.";
} catch (Exception $e) {
echo "❌ Error: " . $e->getMessage();
}

View file

@ -68,29 +68,29 @@
<!-- Tabs for filtering --> <!-- Tabs for filtering -->
<div class="mb-6 flex items-center justify-between border-b border-slate-200"> <div class="mb-6 flex items-center justify-between border-b border-slate-200">
<div class="flex gap-8"> <div class="flex gap-8">
<button @click="currentTab = 'all'" <button @click="currentTab = 'all'" class="pb-4 text-sm font-black transition-all relative"
class="pb-4 text-sm font-black transition-all relative"
:class="currentTab === 'all' ? 'text-blue-600' : 'text-slate-400 hover:text-slate-600'"> :class="currentTab === 'all' ? 'text-blue-600' : 'text-slate-400 hover:text-slate-600'">
전체 자산 전체 자산
<div x-show="currentTab === 'all'" x-transition class="absolute bottom-0 left-0 w-full h-1 bg-blue-600 rounded-t-full"></div> <div x-show="currentTab === 'all'" x-transition
class="absolute bottom-0 left-0 w-full h-1 bg-blue-600 rounded-t-full"></div>
</button> </button>
<button @click="currentTab = 'stock'" <button @click="currentTab = 'stock'" class="pb-4 text-sm font-black transition-all relative"
class="pb-4 text-sm font-black transition-all relative"
:class="currentTab === 'stock' ? 'text-blue-600' : 'text-slate-400 hover:text-slate-600'"> :class="currentTab === 'stock' ? 'text-blue-600' : 'text-slate-400 hover:text-slate-600'">
재고 현황 재고 현황
<div x-show="currentTab === 'stock'" x-transition class="absolute bottom-0 left-0 w-full h-1 bg-blue-600 rounded-t-full"></div> <div x-show="currentTab === 'stock'" x-transition
class="absolute bottom-0 left-0 w-full h-1 bg-blue-600 rounded-t-full"></div>
</button> </button>
<button @click="currentTab = 'assigned'" <button @click="currentTab = 'assigned'" class="pb-4 text-sm font-black transition-all relative"
class="pb-4 text-sm font-black transition-all relative"
:class="currentTab === 'assigned' ? 'text-blue-600' : 'text-slate-400 hover:text-slate-600'"> :class="currentTab === 'assigned' ? 'text-blue-600' : 'text-slate-400 hover:text-slate-600'">
지급 완료 지급 완료
<div x-show="currentTab === 'assigned'" x-transition class="absolute bottom-0 left-0 w-full h-1 bg-blue-600 rounded-t-full"></div> <div x-show="currentTab === 'assigned'" x-transition
class="absolute bottom-0 left-0 w-full h-1 bg-blue-600 rounded-t-full"></div>
</button> </button>
<button @click="currentTab = 'disposed'" <button @click="currentTab = 'disposed'" class="pb-4 text-sm font-black transition-all relative"
class="pb-4 text-sm font-black transition-all relative"
:class="currentTab === 'disposed' ? 'text-rose-600' : 'text-slate-400 hover:text-slate-600'"> :class="currentTab === 'disposed' ? 'text-rose-600' : 'text-slate-400 hover:text-slate-600'">
매각 자산 매각 자산
<div x-show="currentTab === 'disposed'" x-transition class="absolute bottom-0 left-0 w-full h-1 bg-rose-600 rounded-t-full"></div> <div x-show="currentTab === 'disposed'" x-transition
class="absolute bottom-0 left-0 w-full h-1 bg-rose-600 rounded-t-full"></div>
</button> </button>
</div> </div>
@ -113,22 +113,37 @@
<div class="h-8 w-px bg-white/10 mx-2"></div> <div class="h-8 w-px bg-white/10 mx-2"></div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<select x-model="bulkStatus" <select x-model="bulkStatus"
class="bg-slate-800 border-none rounded-xl text-xs font-bold px-4 py-2 focus:ring-2 focus:ring-blue-500 appearance-none shadow-inner min-w-[150px]"> class="bg-slate-800 border-none rounded-xl text-xs font-bold px-4 py-2 focus:ring-2 focus:ring-blue-500 appearance-none shadow-inner min-w-[120px]">
<option value="">상태 변경...</option> <option value="">상태 변경...</option>
<option value="assigned">지급됨(assigned)</option> <option value="assigned">지급됨</option>
<option value="stock">재고(stock)</option> <option value="stock">재고</option>
<option value="disposed">매각됨(disposed)</option>
</select> </select>
<select x-model="bulkModelId" <select x-model="bulkModelId"
class="bg-slate-800 border-none rounded-xl text-xs font-bold px-4 py-2 focus:ring-2 focus:ring-blue-500 appearance-none shadow-inner min-w-[200px]"> class="bg-slate-800 border-none rounded-xl text-xs font-bold px-4 py-2 focus:ring-2 focus:ring-blue-500 appearance-none shadow-inner min-w-[150px]">
<option value="">모델 변경...</option> <option value="">모델 변경...</option>
<template x-for="model in models" :key="model.id"> <template x-for="model in models" :key="model.id">
<option :value="model.id" x-text="'[' + model.manufacturer + '] ' + model.model_name"></option> <option :value="model.id" x-text="'[' + model.manufacturer + '] ' + model.model_name"></option>
</template> </template>
</select> </select>
<button @click="applyBulkUpdate" <button @click="applyBulkUpdate"
class="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-blue-500/20 active:scale-95">일괄 class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-blue-500/20 active:scale-95">
적용</button> 변경 적용
</button>
<div class="h-6 w-px bg-white/10 mx-1"></div>
<button @click="applyBulkDisposal"
class="bg-rose-600 hover:bg-rose-700 px-4 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-rose-500/20 active:scale-95 flex items-center gap-1.5">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
매각 완료
</button>
<button @click="bulkDelete"
class="bg-slate-700 hover:bg-rose-600 px-4 py-2 rounded-xl text-xs font-black transition-all shadow-lg active:scale-95">
삭제
</button>
<button @click="selectedIds = []" <button @click="selectedIds = []"
class="ml-2 w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 text-slate-400 hover:text-white transition-all"> class="ml-2 w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 text-slate-400 hover:text-white transition-all">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -236,7 +251,8 @@
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<template x-if="asset.status === 'disposed'"> <template x-if="asset.status === 'disposed'">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="px-3 py-1 rounded-full text-[10px] font-black border uppercase tracking-tighter bg-rose-50 text-rose-600 border-rose-100 w-fit">매각됨</span> <span
class="px-3 py-1 rounded-full text-[10px] font-black border uppercase tracking-tighter bg-rose-50 text-rose-600 border-rose-100 w-fit">매각됨</span>
<div class="text-[10px] font-bold text-slate-400 group-hover:text-slate-600"> <div class="text-[10px] font-bold text-slate-400 group-hover:text-slate-600">
매각: <span x-text="asset.disposal_recipient"></span><br> 매각: <span x-text="asset.disposal_recipient"></span><br>
실사용: <span x-text="asset.real_user || '-'"></span><br> 실사용: <span x-text="asset.real_user || '-'"></span><br>
@ -291,19 +307,22 @@
<button type="button" @click="setStatus('stock')" <button type="button" @click="setStatus('stock')"
:class="formData.status === 'stock' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'" :class="formData.status === 'stock' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'"
class="px-4 py-2 rounded-xl text-xs font-black transition-all flex items-center gap-2"> class="px-4 py-2 rounded-xl text-xs font-black transition-all flex items-center gap-2">
<div class="w-1.5 h-1.5 rounded-full" :class="formData.status === 'stock' ? 'bg-blue-600' : 'bg-slate-300'"></div> <div class="w-1.5 h-1.5 rounded-full"
:class="formData.status === 'stock' ? 'bg-blue-600' : 'bg-slate-300'"></div>
재고 재고
</button> </button>
<button type="button" @click="setStatus('assigned')" <button type="button" @click="setStatus('assigned')"
:class="formData.status === 'assigned' ? 'bg-white text-indigo-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'" :class="formData.status === 'assigned' ? 'bg-white text-indigo-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'"
class="px-4 py-2 rounded-xl text-xs font-black transition-all flex items-center gap-2"> class="px-4 py-2 rounded-xl text-xs font-black transition-all flex items-center gap-2">
<div class="w-1.5 h-1.5 rounded-full" :class="formData.status === 'assigned' ? 'bg-indigo-600' : 'bg-slate-300'"></div> <div class="w-1.5 h-1.5 rounded-full"
:class="formData.status === 'assigned' ? 'bg-indigo-600' : 'bg-slate-300'"></div>
직원배정 직원배정
</button> </button>
<button type="button" @click="setStatus('disposed')" <button type="button" @click="setStatus('disposed')"
:class="formData.status === 'disposed' ? 'bg-white text-rose-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'" :class="formData.status === 'disposed' ? 'bg-white text-rose-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'"
class="px-4 py-2 rounded-xl text-xs font-black transition-all flex items-center gap-2"> class="px-4 py-2 rounded-xl text-xs font-black transition-all flex items-center gap-2">
<div class="w-1.5 h-1.5 rounded-full" :class="formData.status === 'disposed' ? 'bg-rose-600' : 'bg-slate-300'"></div> <div class="w-1.5 h-1.5 rounded-full"
:class="formData.status === 'disposed' ? 'bg-rose-600' : 'bg-slate-300'"></div>
매각 매각
</button> </button>
</div> </div>
@ -325,7 +344,8 @@
:class="isAssetTagDuplicate ? 'border-rose-500 ring-2 ring-rose-200' : 'border-slate-200'" :class="isAssetTagDuplicate ? 'border-rose-500 ring-2 ring-rose-200' : 'border-slate-200'"
class="w-full px-4 py-3 bg-slate-50 border rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all font-bold"> class="w-full px-4 py-3 bg-slate-50 border rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all font-bold">
<template x-if="isAssetTagDuplicate"> <template x-if="isAssetTagDuplicate">
<p class="text-[10px] text-rose-500 font-bold mt-1.5 ml-1 animate-pulse italic">이미 등록된 자산번호입니다.</p> <p class="text-[10px] text-rose-500 font-bold mt-1.5 ml-1 animate-pulse italic">이미 등록된
자산번호입니다.</p>
</template> </template>
</div> </div>
<div> <div>
@ -334,7 +354,8 @@
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 font-bold text-sm text-slate-700"> 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 font-bold text-sm text-slate-700">
<option value="">모델 선택</option> <option value="">모델 선택</option>
<template x-for="model in models" :key="model.id"> <template x-for="model in models" :key="model.id">
<option :value="model.id" x-text="'[' + model.manufacturer + '] ' + model.model_name"></option> <option :value="model.id" x-text="'[' + model.manufacturer + '] ' + model.model_name">
</option>
</template> </template>
</select> </select>
</div> </div>
@ -350,7 +371,8 @@
class="bg-rose-50 p-6 rounded-3xl border border-rose-100 space-y-4"> class="bg-rose-50 p-6 rounded-3xl border border-rose-100 space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="relative" x-on:click.outside="showDisposalDropdown = false"> <div class="relative" x-on:click.outside="showDisposalDropdown = false">
<label class="block text-[10px] font-black text-rose-400 uppercase mb-2 ml-1">매각 대상자 (사내 직원)</label> <label class="block text-[10px] font-black text-rose-400 uppercase mb-2 ml-1">매각 대상자 (사내
직원)</label>
<div class="relative"> <div class="relative">
<input type="text" x-model="disposalSearchQuery" <input type="text" x-model="disposalSearchQuery"
@focus="showDisposalDropdown = true; disposalSearchQuery = ''" @focus="showDisposalDropdown = true; disposalSearchQuery = ''"
@ -358,8 +380,10 @@
class="w-full px-4 py-3 bg-white border border-rose-200 rounded-xl focus:ring-2 focus:ring-rose-500 outline-none text-sm font-bold text-slate-700"> class="w-full px-4 py-3 bg-white border border-rose-200 rounded-xl focus:ring-2 focus:ring-rose-500 outline-none text-sm font-bold text-slate-700">
<div class="absolute right-3 top-3.5"> <div class="absolute right-3 top-3.5">
<svg class="w-4 h-4 text-rose-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-rose-300" fill="none" stroke="currentColor"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 9l-7 7-7-7" />
</svg> </svg>
</div> </div>
</div> </div>
@ -367,12 +391,16 @@
<div x-show="showDisposalDropdown" x-transition <div x-show="showDisposalDropdown" x-transition
class="absolute z-[120] mt-2 w-full bg-white rounded-2xl shadow-2xl border border-slate-100 max-h-60 overflow-y-auto overflow-x-hidden no-scrollbar"> class="absolute z-[120] mt-2 w-full bg-white rounded-2xl shadow-2xl border border-slate-100 max-h-60 overflow-y-auto overflow-x-hidden no-scrollbar">
<template x-for="user in filteredDisposalUsers" :key="user.id"> <template x-for="user in filteredDisposalUsers" :key="user.id">
<div @click="selectDisposalUser(user)" class="px-4 py-3 hover:bg-rose-50 cursor-pointer flex items-center justify-between transition-colors group"> <div @click="selectDisposalUser(user)"
class="px-4 py-3 hover:bg-rose-50 cursor-pointer flex items-center justify-between transition-colors group">
<div> <div>
<div class="text-sm font-bold text-slate-700 group-hover:text-rose-600" x-text="user.name"></div> <div class="text-sm font-bold text-slate-700 group-hover:text-rose-600"
<div class="text-[10px] text-slate-400 font-medium" x-text="user.dept_name || '부서 정보 없음'"></div> x-text="user.name"></div>
<div class="text-[10px] text-slate-400 font-medium"
x-text="user.dept_name || '부서 정보 없음'"></div>
</div> </div>
<div class="text-[10px] font-black text-slate-300 group-hover:text-rose-400" x-text="user.emp_id"></div> <div class="text-[10px] font-black text-slate-300 group-hover:text-rose-400"
x-text="user.emp_id"></div>
</div> </div>
</template> </template>
</div> </div>
@ -394,13 +422,15 @@
<div class="bg-blue-50/50 p-6 rounded-[2rem] border border-blue-100/50 space-y-4"> <div class="bg-blue-50/50 p-6 rounded-[2rem] border border-blue-100/50 space-y-4">
<div class="flex items-center gap-2 mb-2"> <div class="flex items-center gap-2 mb-2">
<div class="w-1.5 h-1.5 rounded-full bg-blue-500"></div> <div class="w-1.5 h-1.5 rounded-full bg-blue-500"></div>
<h4 class="text-xs font-black text-blue-600 uppercase tracking-widest">Individual Asset Info</h4> <h4 class="text-xs font-black text-blue-600 uppercase tracking-widest">Individual Asset Info
</h4>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="relative" x-on:click.outside="showUserDropdown = false"> <div class="relative" x-on:click.outside="showUserDropdown = false">
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1"> <label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">
배정 사용자 배정 사용자
<span x-show="formData.status === 'assigned' && !formData.current_user_id" class="text-rose-500 ml-2 animate-pulse font-bold">[값이 없음]</span> <span x-show="formData.status === 'assigned' && !formData.current_user_id"
class="text-rose-500 ml-2 animate-pulse font-bold">[값이 없음]</span>
</label> </label>
<div class="relative"> <div class="relative">
<input type="text" x-model="userSearchQuery" <input type="text" x-model="userSearchQuery"
@ -411,13 +441,17 @@
class="w-full px-4 py-3 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700"> class="w-full px-4 py-3 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700">
<div class="absolute right-3 top-3.5 flex items-center gap-2"> <div class="absolute right-3 top-3.5 flex items-center gap-2">
<button type="button" x-show="userSearchQuery && formData.status === 'assigned'" @click="clearUser()" class="text-slate-400 hover:text-slate-600"> <button type="button" x-show="userSearchQuery && formData.status === 'assigned'"
@click="clearUser()" class="text-slate-400 hover:text-slate-600">
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 9l-7 7-7-7" />
</svg> </svg>
</div> </div>
</div> </div>
@ -425,24 +459,30 @@
<div x-show="showUserDropdown" x-transition <div x-show="showUserDropdown" x-transition
class="absolute z-[120] mt-2 w-full bg-white rounded-2xl shadow-2xl border border-slate-100 max-h-60 overflow-y-auto overflow-x-hidden no-scrollbar"> class="absolute z-[120] mt-2 w-full bg-white rounded-2xl shadow-2xl border border-slate-100 max-h-60 overflow-y-auto overflow-x-hidden no-scrollbar">
<template x-for="user in filteredUsers" :key="user.id"> <template x-for="user in filteredUsers" :key="user.id">
<div @click="selectUser(user)" class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between transition-colors group"> <div @click="selectUser(user)"
class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between transition-colors group">
<div> <div>
<div class="text-sm font-bold text-slate-700 group-hover:text-blue-600" x-text="user.name"></div> <div class="text-sm font-bold text-slate-700 group-hover:text-blue-600"
<div class="text-[10px] text-slate-400 font-medium" x-text="user.dept_name || '부서 정보 없음'"></div> x-text="user.name"></div>
<div class="text-[10px] text-slate-400 font-medium"
x-text="user.dept_name || '부서 정보 없음'"></div>
</div> </div>
<div class="text-[10px] font-black text-slate-300 group-hover:text-blue-400" x-text="user.emp_id"></div> <div class="text-[10px] font-black text-slate-300 group-hover:text-blue-400"
x-text="user.emp_id"></div>
</div> </div>
</template> </template>
</div> </div>
</div> </div>
<div> <div>
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">고정 IP 주소</label> <label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">고정 IP
주소</label>
<input type="text" x-model="formData.ip_address" placeholder="192.168.x.x" <input type="text" x-model="formData.ip_address" placeholder="192.168.x.x"
@focus="if(!formData.ip_address) formData.ip_address = '192.168.'" @focus="if(!formData.ip_address) formData.ip_address = '192.168.'"
class="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700"> class="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700">
</div> </div>
<div> <div>
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">노트북 상태 (비고)</label> <label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">노트북 상태
(비고)</label>
<input type="text" x-model="formData.asset_status" placeholder="예: 정상" <input type="text" x-model="formData.asset_status" placeholder="예: 정상"
class="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700"> class="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700">
</div> </div>
@ -463,15 +503,18 @@
<div class="grid grid-cols-1 md:grid-cols-3 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">CPU</label> <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 font-medium"> <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 font-medium">
</div> </div>
<div> <div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">NPU</label> <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 font-medium"> <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 font-medium">
</div> </div>
<div> <div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">RAM</label> <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 font-medium"> <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 font-medium">
</div> </div>
</div> </div>
@ -479,15 +522,19 @@
<div class="bg-slate-50 p-4 rounded-xl space-y-3"> <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> <label class="block text-[10px] font-black text-slate-400 uppercase">Storage 0</label>
<div class="grid grid-cols-2 gap-2"> <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_model" placeholder="모델명/타입"
<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"> 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="bg-slate-50 p-4 rounded-xl space-y-3"> <div class="bg-slate-50 p-4 rounded-xl space-y-3">
<label class="block text-[10px] font-black text-slate-400 uppercase">Storage 1</label> <label class="block text-[10px] font-black text-slate-400 uppercase">Storage 1</label>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<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_model" placeholder="모델명/타입"
<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"> 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>
</div> </div>
@ -496,7 +543,8 @@
<div class="bg-slate-50 p-6 rounded-[2rem] border border-slate-100 space-y-4"> <div class="bg-slate-50 p-6 rounded-[2rem] border border-slate-100 space-y-4">
<div class="flex items-center gap-2 mb-2"> <div class="flex items-center gap-2 mb-2">
<div class="w-1.5 h-1.5 rounded-full bg-slate-400"></div> <div class="w-1.5 h-1.5 rounded-full bg-slate-400"></div>
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest">Model Base Info (Read Only)</h4> <h4 class="text-xs font-black text-slate-400 uppercase tracking-widest">Model Base Info (Read
Only)</h4>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div> <div>
@ -524,8 +572,11 @@
</div> </div>
<div class="pt-4 flex gap-4"> <div class="pt-4 flex gap-4">
<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> <button type="button" @click="showModal = false"
<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" x-text="isEdit ? '수정 완료' : '자산 등록하기'"></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"
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>
</div> </div>
</form> </form>
</div> </div>
@ -656,7 +707,7 @@
}) })
}).then(res => res.json()).then(data => { }).then(res => res.json()).then(data => {
if (data.success) { if (data.success) {
window.showAlert(`${this.selectedIds.length}개의 자산 정보가 일괄 변경되었습니다.`, '변경 성공', 'success'); window.showAlert(`${this.selectedIds.length}개의 자산 속성이 변경되었습니다.`, '변경 성공', 'success');
this.selectedIds = []; this.selectedIds = [];
this.bulkStatus = ''; this.bulkStatus = '';
this.bulkModelId = ''; this.bulkModelId = '';
@ -667,6 +718,50 @@
}); });
}, },
applyBulkDisposal() {
if (this.selectedIds.length === 0) return;
if (!confirm(`선택한 ${this.selectedIds.length}개의 자산을 '매각 완료' 처리하시겠습니까?\n이 작업은 되돌릴 수 없으며 자산이 매각 탭으로 이동합니다.`)) return;
fetch('api.php?action=bulk_update_laptops', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ids: this.selectedIds,
status: 'disposed'
})
}).then(res => res.json()).then(data => {
if (data.success) {
window.showAlert(`${this.selectedIds.length}개의 자산이 매각 처리되었습니다.`, '매각 완료', 'success');
this.selectedIds = [];
this.fetchAssets();
this.currentTab = 'disposed'; // 매각 탭으로 보냄
} else {
window.showAlert('매각 처리에 실패했습니다.', '오류', 'error');
}
});
},
bulkDelete() {
if (this.selectedIds.length === 0) return;
if (!confirm(`선택한 ${this.selectedIds.length}개의 자산을 시스템에서 영구적으로 삭제하시겠습니까?`)) return;
fetch('api.php?action=bulk_delete_laptops', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ids: this.selectedIds
})
}).then(res => res.json()).then(data => {
if (data.success) {
window.showAlert(`${this.selectedIds.length}개의 자산이 삭제되었습니다.`, '삭제 완료', 'success');
this.selectedIds = [];
this.fetchAssets();
} else {
window.showAlert('자산 삭제에 실패했습니다.', '오류', 'error');
}
});
},
fetchAssets() { fetchAssets() {
const scrollPos = window.scrollY; const scrollPos = window.scrollY;
this.loading = true; this.loading = true;

View file

@ -5,7 +5,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>관리자페이지 | 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">
@ -33,8 +33,8 @@
<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">시스템 관리</h2>
<p class="text-slate-500 mt-1">시스템 운영을 위한 기초 자산 모델 조직 정보를 관리합니다.</p> <p class="text-slate-500 mt-1">시스템 운영을 위한 기초 자산 모델, 조직 정보 메뉴 노출 설정을 관리합니다.</p>
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<!-- Search Bar for Models --> <!-- Search Bar for Models -->
@ -82,6 +82,9 @@
<button @click="currentTab = 'depts'" <button @click="currentTab = 'depts'"
:class="currentTab === 'depts' ? 'bg-white shadow-sm text-blue-600' : 'text-slate-500 hover:text-slate-700'" :class="currentTab === 'depts' ? 'bg-white shadow-sm text-blue-600' : 'text-slate-500 hover:text-slate-700'"
class="px-6 py-2 rounded-xl text-sm font-black transition-all">부서 관리</button> class="px-6 py-2 rounded-xl text-sm font-black transition-all">부서 관리</button>
<button @click="currentTab = 'menus'"
:class="currentTab === 'menus' ? 'bg-white shadow-sm text-blue-600' : 'text-slate-500 hover:text-slate-700'"
class="px-6 py-2 rounded-xl text-sm font-black transition-all">메뉴 관리</button>
</div> </div>
<!-- Tab 1: Laptop Models --> <!-- Tab 1: Laptop Models -->
@ -103,6 +106,9 @@
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"> 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">
<button @click="applyBulkUpdate" <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> class="bg-blue-500 hover:bg-blue-600 px-4 py-1.5 rounded-lg text-xs font-black transition-all">적용</button>
<div class="h-4 w-px bg-slate-700 mx-1"></div>
<button @click="bulkDelete"
class="bg-slate-700 hover:bg-rose-600 px-4 py-1.5 rounded-lg text-xs font-black transition-all">삭제</button>
</div> </div>
</div> </div>
<button @click="selectedIds = []" class="text-slate-400 hover:text-white transition-colors"> <button @click="selectedIds = []" class="text-slate-400 hover:text-white transition-colors">
@ -200,7 +206,8 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div x-show="filteredModels.length === 0" class="p-20 text-center bg-white rounded-3xl border border-dashed border-slate-300 mt-6"> <div x-show="filteredModels.length === 0"
class="p-20 text-center bg-white rounded-3xl border border-dashed border-slate-300 mt-6">
<p class="text-slate-400 font-medium italic">일치하는 모델 정보가 없습니다.</p> <p class="text-slate-400 font-medium italic">일치하는 모델 정보가 없습니다.</p>
</div> </div>
</div> </div>
@ -250,10 +257,87 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div x-show="filteredDepts.length === 0" class="p-20 text-center bg-white rounded-3xl border border-dashed border-slate-300 mt-6"> <div x-show="filteredDepts.length === 0"
class="p-20 text-center bg-white rounded-3xl border border-dashed border-slate-300 mt-6">
<p class="text-slate-400 font-medium italic">일치하는 부서 정보가 없습니다.</p> <p class="text-slate-400 font-medium italic">일치하는 부서 정보가 없습니다.</p>
</div> </div>
</div> </div>
<!-- Tab 3: Menu Management -->
<div x-show="currentTab === 'menus'" x-transition>
<div class="bg-white rounded-[2.5rem] shadow-sm border border-slate-200 overflow-hidden p-10">
<div class="max-w-2xl mx-auto">
<h3 class="text-2xl font-black text-slate-900 mb-2">상단 메뉴 노출 관리</h3>
<p class="text-slate-500 mb-10 font-medium">네비게이션 바에서 특정 메뉴의 활성화 여부를 실시간으로 제어합니다.</p>
<div class="space-y-4">
<!-- 출입증 현황 -->
<div
class="flex items-center justify-between p-6 bg-slate-50 rounded-3xl border border-slate-100 transition-all hover:bg-white hover:shadow-xl hover:border-blue-100 group">
<div class="flex items-center gap-4">
<div
class="w-12 h-12 bg-white rounded-2xl flex items-center justify-center shadow-sm group-hover:bg-blue-50 transition-colors">
<svg class="w-6 h-6 text-slate-400 group-hover:text-blue-500 transition-colors"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="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" />
</svg>
</div>
<div>
<h4 class="font-black text-slate-800">출입증 현황</h4>
<p class="text-xs text-slate-500 font-bold uppercase tracking-wider">Access Card
Status</p>
</div>
</div>
<button @click="toggleMenu('menu_cards_visible')"
:class="settings.menu_cards_visible === '1' ? 'bg-blue-600' : 'bg-slate-300'"
class="relative inline-flex h-7 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
<span :class="settings.menu_cards_visible === '1' ? 'translate-x-5' : 'translate-x-0'"
class="pointer-events-none inline-block h-6 w-6 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
</button>
</div>
<!-- 복합기 계정 -->
<div
class="flex items-center justify-between p-6 bg-slate-50 rounded-3xl border border-slate-100 transition-all hover:bg-white hover:shadow-xl hover:border-blue-100 group">
<div class="flex items-center gap-4">
<div
class="w-12 h-12 bg-white rounded-2xl flex items-center justify-center shadow-sm group-hover:bg-blue-50 transition-colors">
<svg class="w-6 h-6 text-slate-400 group-hover:text-blue-500 transition-colors"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="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" />
</svg>
</div>
<div>
<h4 class="font-black text-slate-800">복합기 계정</h4>
<p class="text-xs text-slate-500 font-bold uppercase tracking-wider">MFP Account
Management</p>
</div>
</div>
<button @click="toggleMenu('menu_mfp_visible')"
:class="settings.menu_mfp_visible === '1' ? 'bg-blue-600' : 'bg-slate-300'"
class="relative inline-flex h-7 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
<span :class="settings.menu_mfp_visible === '1' ? 'translate-x-5' : 'translate-x-0'"
class="pointer-events-none inline-block h-6 w-6 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
</button>
</div>
</div>
<div class="mt-12 p-6 bg-blue-50 rounded-3xl border border-blue-100 flex items-start gap-4">
<div class="w-10 h-10 bg-blue-600 rounded-2xl flex items-center justify-center shrink-0">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p class="text-sm font-bold text-blue-800 leading-relaxed">
메뉴 설정을 변경하면 즉시 시스템에 반영됩니다. 특정 기능을 한시적으로 제한하거나 보안 정책에 따라 메뉴를 숨기고 싶을 활용하세요.
</p>
</div>
</div>
</div>
</div>
</main> </main>
<!-- Model Modal --> <!-- Model Modal -->
@ -474,6 +558,9 @@
return { return {
currentTab: 'models', currentTab: 'models',
// Settings Data
settings: { menu_cards_visible: '1', menu_mfp_visible: '1' },
// Model Data // Model Data
models: [], models: [],
showModal: false, showModal: false,
@ -522,6 +609,7 @@
init() { init() {
this.fetchModels(); this.fetchModels();
this.fetchDepts(); this.fetchDepts();
this.fetchSettings();
}, },
// Model Methods // Model Methods
@ -551,6 +639,7 @@
}) })
}).then(res => res.json()).then(data => { }).then(res => res.json()).then(data => {
if (data.success) { if (data.success) {
window.showAlert(`${this.selectedIds.length}개의 모델 정보가 일괄 변경되었습니다.`, '성공', 'success');
this.selectedIds = []; this.selectedIds = [];
this.bulkManufacturer = ''; this.bulkManufacturer = '';
this.bulkProductName = ''; this.bulkProductName = '';
@ -558,6 +647,30 @@
} }
}); });
}, },
bulkDelete() {
if (this.selectedIds.length === 0) return;
window.showConfirm(
`선택한 ${this.selectedIds.length}개의 모델 정보를 시스템에서 영구적으로 삭제하시겠습니까?`,
() => {
fetch('api.php?action=bulk_delete_models', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ids: this.selectedIds
})
}).then(res => res.json()).then(data => {
if (data.success) {
window.showAlert(`${this.selectedIds.length}개의 모델 정보가 삭제되었습니다.`, '삭제 완료', 'success');
this.selectedIds = [];
this.fetchModels();
} else {
window.showAlert(data.error || '모델 삭제에 실패했습니다.', '오류', 'error');
}
});
},
'모델 영구 삭제'
);
},
openAddModal() { openAddModal() {
this.isEdit = false; this.isEdit = false;
this.formData = { this.formData = {
@ -710,6 +823,23 @@
}, },
'부서 영구 삭제' '부서 영구 삭제'
); );
},
fetchSettings() {
fetch('api.php?action=get_settings')
.then(res => res.json())
.then(data => {
if (data.success) this.settings = data.settings;
});
},
toggleMenu(key) {
const finalValue = this.settings[key] === '1' ? '0' : '1';
this.settings[key] = finalValue;
fetch('api.php?action=update_settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ settings: { [key]: finalValue } })
});
} }
} }
} }

25
nav.php
View file

@ -3,6 +3,7 @@
* nav.php * nav.php
* 공통 네비게이션 컴포넌트 (최상단 고정형 리본 메뉴) * 공통 네비게이션 컴포넌트 (최상단 고정형 리본 메뉴)
*/ */
require_once 'config.php';
$current_page = basename($_SERVER['PHP_SELF']); $current_page = basename($_SERVER['PHP_SELF']);
?> ?>
<nav class="fixed top-0 left-0 right-0 bg-[#0f172a] text-white z-[100] shadow-2xl border-b border-blue-500/20 px-6"> <nav class="fixed top-0 left-0 right-0 bg-[#0f172a] text-white z-[100] shadow-2xl border-b border-blue-500/20 px-6">
@ -25,18 +26,34 @@ $current_page = basename($_SERVER['PHP_SELF']);
<!-- Horizontal Menu --> <!-- Horizontal Menu -->
<div class="flex-1 flex items-center justify-center space-x-1 lg:space-x-4 overflow-x-auto no-scrollbar py-2"> <div class="flex-1 flex items-center justify-center space-x-1 lg:space-x-4 overflow-x-auto no-scrollbar py-2">
<?php <?php
// 시스템 설정 로드
$settings = [];
try {
$settings = $db->query("SELECT key_name, value FROM system_settings")->fetchAll(PDO::FETCH_KEY_PAIR);
} catch (Exception $e) {
// 테이블이 없거나 에러 발생 시 기본값
$settings = ['menu_cards_visible' => '1', 'menu_mfp_visible' => '1'];
}
$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' => 'rental.php', 'name' => '노트북 임대', 'icon' => 'M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4'],
['url' => 'general_rental.php', 'name' => '임대', 'icon' => 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4'], ['url' => 'general_rental.php', 'name' => '임대', 'icon' => 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 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' => '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' => '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' => 'models.php', 'name' => 'DB 관리', 'icon' => 'M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4'],
['url' => 'admins.php', 'name' => 'ADMIN', 'icon' => 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z']
]; ];
if (($settings['menu_cards_visible'] ?? '1') === '1') {
$menu_items[] = ['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'];
}
if (($settings['menu_mfp_visible'] ?? '1') === '1') {
$menu_items[] = ['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'];
}
$menu_items[] = ['url' => 'models.php', 'name' => '관리자페이지', 'icon' => 'M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4'];
$menu_items[] = ['url' => 'admins.php', 'name' => 'ADMIN', 'icon' => 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z'];
foreach ($menu_items as $item): foreach ($menu_items as $item):
$is_active = ($current_page == $item['url']); $is_active = ($current_page == $item['url']);
?> ?>