기능업데이트

This commit is contained in:
NAS-Admin 2026-03-14 21:51:23 +09:00
parent 9ab5065026
commit acc2969dbe
5 changed files with 186 additions and 18 deletions

32
api.php
View file

@ -94,13 +94,41 @@ try {
$users_general = $db->query("SELECT COUNT(*) FROM users WHERE accounting_type = '일반회계'")->fetchColumn();
$users_special = $db->query("SELECT COUNT(*) FROM users WHERE accounting_type = '특별회계'")->fetchColumn();
// Asset Replacement Analysis (4 years)
$current_year = (int)date('Y');
// 4년 이상 경과 (매각 대상)
$replacement_targets = $db->query("SELECT a.*, m.model_name, m.manufacturer
FROM laptop_assets a
LEFT JOIN laptop_models m ON a.model_id = m.id
WHERE a.status != 'disposed'
AND a.purchase_date IS NOT NULL
AND CAST(SUBSTR(a.purchase_date, 1, 4) AS INTEGER) <= " . ($current_year - 4))
->fetchAll();
// 4년차 도래 (올해 매각 예정 대상)
// 예: 2022년 구매 -> 2026년 매각대상 (만 4년이 되는 해)
$upcoming_replacements = $db->query("SELECT a.*, m.model_name, m.manufacturer
FROM laptop_assets a
LEFT JOIN laptop_models m ON a.model_id = m.id
WHERE a.status != 'disposed'
AND a.purchase_date IS NOT NULL
AND CAST(SUBSTR(a.purchase_date, 1, 4) AS INTEGER) = " . ($current_year - 3))
->fetchAll();
echo json_encode([
'laptops' => ['total' => (int) $laptop_total, 'assigned' => (int) $laptop_assigned, 'disposed' => (int) $laptop_disposed],
'cards' => ['total' => (int) $card_total, 'assigned' => (int) $card_assigned],
'mfp' => ['total' => (int) $mfp_total, 'active' => (int) $mfp_active],
'users' => ['general' => (int) $users_general, 'special' => (int) $users_special]
'users' => ['general' => (int) $users_general, 'special' => (int) $users_special],
'replacements' => [
'targets' => $replacement_targets,
'upcoming' => $upcoming_replacements,
'current_year' => $current_year
]
]);
break;
break;
case 'get_users':
$query = "WITH RECURSIVE dept_path(id, path) AS (
@ -226,7 +254,7 @@ LEFT JOIN users u ON a.current_user_id = u.id";
$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 '%업무용%' OR a.current_user_id IS NULL)";
WHERE (a.assigned_user_name LIKE '%업무용%' OR a.current_user_id IS NULL) AND a.status != 'disposed'";
echo json_encode($db->query($query)->fetchAll());
break;

Binary file not shown.

121
index.php
View file

@ -38,7 +38,18 @@
</header>
<!-- Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-8">
<?php
$visible_count = 2; // 노트북, 조직현황은 상시 노출
if (($settings['menu_cards_visible'] ?? '1') === '1') $visible_count++;
if (($settings['menu_mfp_visible'] ?? '1') === '1') $visible_count++;
$grid_cols_class = match ($visible_count) {
2 => 'xl:grid-cols-2 max-w-[800px]',
3 => 'xl:grid-cols-3 max-w-[1200px]',
default => 'xl:grid-cols-4'
};
?>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 transition-all duration-500 mx-auto <?php echo $grid_cols_class; ?>">
<div
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">
@ -124,7 +135,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg> 네비게이션</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-<?php echo $visible_count; ?> gap-6">
<a href="users.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">
<div class="bg-slate-50 p-3 rounded-xl group-hover:bg-blue-50 transition-colors"><svg
@ -184,6 +195,112 @@
<?php endif; ?>
</div>
</section>
<!-- 자산 교체/매각 대상 리스트 -->
<section class="mt-16 mb-20" x-show="stats.replacements?.targets.length > 0 || stats.replacements?.upcoming.length > 0">
<div class="flex items-center justify-between mb-8">
<div>
<h3 class="text-2xl font-black text-slate-900 flex items-center gap-3">
<div class="w-10 h-10 bg-rose-100 rounded-2xl flex items-center justify-center">
<svg class="w-6 h-6 text-rose-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</div>
자산 교체 매각 관리
</h3>
<p class="text-slate-500 mt-2 text-sm font-medium">취득 4년이 경과하거나 도래한 자산 리스트입니다. (정책: 4 단위 교체)</p>
</div>
<div class="flex gap-2">
<span class="px-4 py-2 bg-rose-50 text-rose-600 rounded-xl text-xs font-black border border-rose-100 uppercase tracking-tighter" x-text="'Current: ' + stats.replacements?.current_year"></span>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- 01. 매각 완료 대상 (4 이상) -->
<div class="bg-white rounded-[2.5rem] border border-slate-200 shadow-sm overflow-hidden flex flex-col">
<div class="p-6 border-b border-slate-100 bg-slate-50/50 flex justify-between items-center">
<div class="flex items-center gap-3">
<span class="w-8 h-8 bg-rose-600 text-white rounded-xl flex items-center justify-center text-xs font-black shadow-lg shadow-rose-200">01</span>
<h4 class="font-black text-slate-800">매각 대상 자산 <span class="text-rose-600 ml-1" x-text="'(' + stats.replacements?.targets.length + ')'"></span></h4>
</div>
<span class="text-[10px] font-black text-rose-500 bg-rose-50 px-2.5 py-1 rounded-lg border border-rose-100 uppercase">Immediate Action</span>
</div>
<div class="flex-1 overflow-y-auto max-h-[500px] no-scrollbar">
<table class="w-full text-left border-collapse">
<thead class="sticky top-0 bg-white/95 backdrop-blur-md z-10">
<tr>
<th class="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">Asset TAG</th>
<th class="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">Model & Spec</th>
<th class="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest text-right">Purchase Year</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50">
<template x-for="item in stats.replacements?.targets" :key="item.id">
<tr class="hover:bg-rose-50/30 transition-colors group">
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-sm font-black text-slate-900 group-hover:text-rose-600 transition-colors" x-text="item.asset_tag"></span>
</td>
<td class="px-6 py-4">
<div class="text-xs font-bold text-slate-700" x-text="item.model_name"></div>
<div class="text-[10px] text-slate-400 font-bold" x-text="item.manufacturer"></div>
</td>
<td class="px-6 py-4 text-right">
<div class="inline-flex flex-col items-end">
<span class="text-xs font-black text-slate-900" x-text="item.purchase_date.substring(0,4) + '년'"></span>
<span class="text-[9px] font-black text-rose-500 bg-rose-50 px-1.5 py-0.5 rounded mt-1" x-text="(stats.replacements.current_year - parseInt(item.purchase_date.substring(0,4))) + '년 경과'"></span>
</div>
</td>
</tr>
</template>
</tbody>
</table>
<div x-show="stats.replacements?.targets.length === 0" class="py-20 text-center text-slate-300 italic text-sm font-medium">관리 대상 자산이 없습니다.</div>
</div>
</div>
<!-- 02. 올해 매각 예정 (4년차 도래) -->
<div class="bg-white rounded-[2.5rem] border border-slate-200 shadow-sm overflow-hidden flex flex-col">
<div class="p-6 border-b border-slate-100 bg-slate-50/50 flex justify-between items-center">
<div class="flex items-center gap-3">
<span class="w-8 h-8 bg-amber-500 text-white rounded-xl flex items-center justify-center text-xs font-black shadow-lg shadow-amber-200">02</span>
<h4 class="font-black text-slate-800">교체/매각 준비 <span class="text-amber-600 ml-1" x-text="'(' + stats.replacements?.upcoming.length + ')'"></span></h4>
</div>
<span class="text-[10px] font-black text-amber-500 bg-amber-50 px-2.5 py-1 rounded-lg border border-amber-100 uppercase" x-text="stats.replacements?.current_year + ' Replacement'"></span>
</div>
<div class="flex-1 overflow-y-auto max-h-[500px] no-scrollbar">
<table class="w-full text-left border-collapse">
<thead class="sticky top-0 bg-white/95 backdrop-blur-md z-10">
<tr>
<th class="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">Asset TAG</th>
<th class="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">Model & Spec</th>
<th class="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest text-right">Target Year</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50">
<template x-for="item in stats.replacements?.upcoming" :key="item.id">
<tr class="hover:bg-amber-50/30 transition-colors group">
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-sm font-black text-slate-900 group-hover:text-amber-600 transition-colors" x-text="item.asset_tag"></span>
</td>
<td class="px-6 py-4">
<div class="text-xs font-bold text-slate-700" x-text="item.model_name"></div>
<div class="text-[10px] text-slate-400 font-bold" x-text="item.manufacturer"></div>
</td>
<td class="px-6 py-4 text-right">
<div class="inline-flex flex-col items-end">
<span class="text-xs font-black text-slate-900" x-text="(parseInt(item.purchase_date.substring(0,4)) + 4) + '년 매각 대상'"></span>
<span class="text-[9px] font-black text-blue-500 bg-blue-50 px-1.5 py-0.5 rounded mt-1" x-text="'취득: ' + item.purchase_date.substring(0,4) + '년'"></span>
</div>
</td>
</tr>
</template>
</tbody>
</table>
<div x-show="stats.replacements?.upcoming.length === 0" class="py-20 text-center text-slate-300 italic text-sm font-medium">관리 대상 자산이 없습니다.</div>
</div>
</div>
</div>
</section>
</main>
<script>

View file

@ -324,6 +324,17 @@
</div>
</div>
<!-- 설정 적용 버튼 추가 -->
<div class="mt-8 flex justify-end">
<button @click="saveSettings()"
class="px-8 py-4 bg-slate-900 text-white rounded-2xl font-black shadow-xl hover:bg-slate-800 transition-all active:scale-95 flex items-center gap-2 group">
<svg class="w-5 h-5 text-blue-400 group-hover:rotate-12 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
메뉴 설정 적용하기
</button>
</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">
@ -828,17 +839,26 @@
fetch('api.php?action=get_settings')
.then(res => res.json())
.then(data => {
if (data.success) this.settings = data.settings;
if (data.success) {
// DB에 설정이 없는 경우를 대비해 초기 기본값과 병합
this.settings = { ...this.settings, ...data.settings };
}
});
},
toggleMenu(key) {
const finalValue = this.settings[key] === '1' ? '0' : '1';
this.settings[key] = finalValue;
this.settings[key] = this.settings[key] === '1' ? '0' : '1';
},
saveSettings() {
fetch('api.php?action=update_settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ settings: { [key]: finalValue } })
body: JSON.stringify({ settings: this.settings })
}).then(res => res.json()).then(data => {
if (data.success) {
window.showAlert('메뉴 설정이 저장되었습니다. 시스템에 즉시 반영됩니다.', '설정 완료', 'success');
// 페이지 새로고침하여 nav.php의 반영사항 확인
setTimeout(() => location.reload(), 1500);
}
});
}
}

21
nav.php
View file

@ -5,6 +5,18 @@
*/
require_once 'config.php';
$current_page = basename($_SERVER['PHP_SELF']);
// 시스템 설정 로드 (최상단으로 이동하여 하위 include 페이지에서 즉시 사용 가능하도록 함)
$settings = [];
try {
$settings = $db->query("SELECT key_name, value FROM system_settings")->fetchAll(PDO::FETCH_KEY_PAIR);
} catch (Exception $e) {
// 테이블이 없거나 에러 발생 시 기본값
$settings = [];
}
// 기본값 보정
if (!isset($settings['menu_cards_visible'])) $settings['menu_cards_visible'] = '1';
if (!isset($settings['menu_mfp_visible'])) $settings['menu_mfp_visible'] = '1';
?>
<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">
<div class="max-w-[1600px] mx-auto h-20 flex items-center justify-between">
@ -26,15 +38,6 @@ $current_page = basename($_SERVER['PHP_SELF']);
<!-- 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">
<?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 = [
['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'],