This commit is contained in:
NAS-Admin 2026-02-09 01:19:02 +09:00
parent 3e1cd3adab
commit cfeea88124
7 changed files with 247 additions and 41 deletions

View file

@ -27,15 +27,29 @@
<?php include 'nav.php'; ?>
<main class="max-w-[1600px] mx-auto p-8 pt-10">
<header class="mb-10 flex justify-between items-center">
<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">보안실 연동 임시 출입증 NFC ID 관리</p>
</div>
<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">신규
카드 등록</button>
<div class="flex items-center gap-3">
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-emerald-500 transition-colors"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input type="text" x-model="searchQuery"
class="block w-64 pl-10 pr-3 py-2.5 border border-slate-200 rounded-xl leading-5 bg-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 transition-all text-sm shadow-sm"
placeholder="카드번호, 종류, NFC ID 검색...">
</div>
<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 shrink-0">신규
카드 등록</button>
</div>
</header>
<!-- Bulk Actions (Fixed Floating Bar) -->
@ -73,7 +87,8 @@
</div>
<!-- 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="!loading && filteredCards.length > 0">
<table class="min-w-full divide-y divide-slate-100">
<thead class="bg-slate-50/50">
<tr>
@ -92,7 +107,7 @@
</tr>
</thead>
<tbody class="divide-y divide-slate-50">
<template x-for="card in cards" :key="card.id">
<template x-for="card in filteredCards" :key="card.id">
<tr class="hover:bg-slate-50/80 transition-colors group">
<td class="px-6 py-4">
<input type="checkbox" :value="card.id" x-model="selectedIds"
@ -119,6 +134,19 @@
</tbody>
</table>
</div>
<div x-show="loading" class="p-20 flex justify-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-emerald-600"></div>
</div>
<div x-show="!loading && filteredCards.length === 0"
class="p-20 text-center bg-white rounded-3xl border border-dashed border-slate-300">
<template x-if="searchQuery">
<p class="text-slate-400 font-medium italic">검색 결과와 일치하는 카드가 없습니다.</p>
</template>
<template x-if="!searchQuery">
<p class="text-slate-400 font-medium italic">등록된 출입증이 없습니다.</p>
</template>
</div>
</main>
<!-- Card Modal -->
@ -154,17 +182,30 @@
showModal: false,
isEdit: false,
selectedIds: [],
loading: true,
searchQuery: '',
bulkStatus: '',
bulkCardType: '',
formData: { id: '', card_number: '', card_type: '', nfc_id: '' },
init() { this.fetchCards(); },
fetchCards() {
const scrollPos = window.scrollY;
this.loading = true;
fetch('api.php?action=get_cards').then(res => res.json()).then(data => {
this.cards = data;
this.loading = false;
this.$nextTick(() => window.scrollTo(0, scrollPos));
});
},
get filteredCards() {
if (!this.searchQuery) return this.cards;
const q = this.searchQuery.toLowerCase();
return this.cards.filter(c =>
(c.card_number && c.card_number.toLowerCase().includes(q)) ||
(c.card_type && c.card_type.toLowerCase().includes(q)) ||
(c.nfc_id && c.nfc_id.toLowerCase().includes(q))
);
},
toggleSelectAll(checked) {
this.selectedIds = checked ? this.cards.map(c => c.id) : [];
},

6
check_admins.php Normal file
View file

@ -0,0 +1,6 @@
<?php
require_once 'config.php';
$stmt = $db->query("SELECT * FROM administrators");
$admins = $stmt->fetchAll();
print_r($admins);
?>

View file

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

View file

@ -44,18 +44,33 @@
<h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">일반 물품 임대</h2>
<p class="text-slate-500 mt-1">사원별 각종 물품(어댑터, 주변기기 ) 단기 임대 반납 기록</p>
</div>
<button @click="openAddModal()"
class="px-6 py-3 bg-blue-600 text-white rounded-2xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all flex items-center gap-2">
<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="M12 4v16m8-8H4" />
</svg>
임대 등록
</button>
<div class="flex items-center gap-3">
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-blue-500 transition-colors"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input type="text" x-model="searchQuery"
class="block w-64 pl-10 pr-3 py-2.5 border border-slate-200 rounded-xl leading-5 bg-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all text-sm shadow-sm"
placeholder="사원명, 품목 검색...">
</div>
<button @click="openAddModal()"
class="px-6 py-3 bg-blue-600 text-white rounded-2xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all flex items-center gap-2 shrink-0">
<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="M12 4v16m8-8H4" />
</svg>
임대 등록
</button>
</div>
</header>
<!-- Rental Cards Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<template x-for="rental in rentals" :key="rental.id">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
x-show="!loading && filteredRentals.length > 0">
<template x-for="rental in filteredRentals" :key="rental.id">
<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"
:class="rental.status === 'returned' ? 'opacity-75' : ''">
@ -149,6 +164,19 @@
</div>
</template>
</div>
<div x-show="loading" class="p-20 flex justify-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
<div x-show="!loading && filteredRentals.length === 0"
class="p-20 text-center bg-white rounded-3xl border border-dashed border-slate-300">
<template x-if="searchQuery">
<p class="text-slate-400 font-medium italic">검색 결과와 일치하는 임대 기록이 없습니다.</p>
</template>
<template x-if="!searchQuery">
<p class="text-slate-400 font-medium italic">등록된 임대 기록이 없습니다.</p>
</template>
</div>
</main>
<!-- Add Rental Modal -->
@ -238,6 +266,8 @@
return {
rentals: [],
users: [],
loading: true,
searchQuery: '',
showAddModal: false,
formData: {
user_id: '',
@ -253,11 +283,24 @@
},
fetchRentals() {
const scrollPos = window.scrollY;
this.loading = true;
fetch('api.php?action=get_general_rentals').then(res => res.json()).then(data => {
this.rentals = data;
this.loading = false;
this.$nextTick(() => window.scrollTo(0, scrollPos));
});
},
get filteredRentals() {
if (!this.searchQuery) return this.rentals;
const q = this.searchQuery.toLowerCase();
return this.rentals.filter(r =>
(r.user_name && r.user_name.toLowerCase().includes(q)) ||
(r.items && r.items.toLowerCase().includes(q))
);
},
fetchUsers() {
fetch('api.php?action=get_users').then(res => res.json()).then(data => {
this.users = data;

54
mfp.php
View file

@ -27,15 +27,29 @@
<?php include 'nav.php'; ?>
<main class="max-w-[1600px] mx-auto p-8 pt-10">
<header class="mb-10 flex justify-between items-center">
<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>
<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">신규
계정 등록</button>
<div class="flex items-center gap-3">
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-amber-500 transition-colors"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input type="text" x-model="searchQuery"
class="block w-64 pl-10 pr-3 py-2.5 border border-slate-200 rounded-xl leading-5 bg-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-amber-500 transition-all text-sm shadow-sm"
placeholder="계정 ID, 사용 목적 검색...">
</div>
<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 shrink-0">신규
계정 등록</button>
</div>
</header>
<!-- Bulk Actions (Fixed Floating Bar) -->
@ -73,7 +87,8 @@
</div>
<!-- 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="!loading && filteredMfp.length > 0">
<table class="min-w-full divide-y divide-slate-100">
<thead class="bg-slate-50/50">
<tr>
@ -92,7 +107,7 @@
</tr>
</thead>
<tbody class="divide-y divide-slate-50">
<template x-for="mfp in mfpList" :key="mfp.id">
<template x-for="mfp in filteredMfp" :key="mfp.id">
<tr class="hover:bg-slate-50/80 transition-colors group">
<td class="px-6 py-4">
<input type="checkbox" :value="mfp.id" x-model="selectedIds"
@ -127,6 +142,19 @@
</tbody>
</table>
</div>
<div x-show="loading" class="p-20 flex justify-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-amber-600"></div>
</div>
<div x-show="!loading && filteredMfp.length === 0"
class="p-20 text-center bg-white rounded-3xl border border-dashed border-slate-300">
<template x-if="searchQuery">
<p class="text-slate-400 font-medium italic">검색 결과와 일치하는 계정이 없습니다.</p>
</template>
<template x-if="!searchQuery">
<p class="text-slate-400 font-medium italic">등록된 복합기 계정이 없습니다.</p>
</template>
</div>
</main>
<!-- MFP Modal -->
@ -161,17 +189,29 @@
showModal: false,
isEdit: false,
selectedIds: [],
loading: true,
searchQuery: '',
bulkStatus: '',
bulkPurpose: '',
formData: { id: '', account_id: '', account_pw: '', purpose: '' },
init() { this.fetchMfp(); },
fetchMfp() {
const scrollPos = window.scrollY;
this.loading = true;
fetch('api.php?action=get_mfp').then(res => res.json()).then(data => {
this.mfpList = data;
this.loading = false;
this.$nextTick(() => window.scrollTo(0, scrollPos));
});
},
get filteredMfp() {
if (!this.searchQuery) return this.mfpList;
const q = this.searchQuery.toLowerCase();
return this.mfpList.filter(m =>
(m.account_id && m.account_id.toLowerCase().includes(q)) ||
(m.purpose && m.purpose.toLowerCase().includes(q))
);
},
toggleSelectAll(checked) {
this.selectedIds = checked ? this.mfpList.map(m => m.id) : [];
},

View file

@ -36,13 +36,41 @@
<h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">DB 마스터 관리</h2>
<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 class="flex items-center gap-3">
<!-- Search Bar for Models -->
<div x-show="currentTab === 'models'" class="relative group">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-blue-500 transition-colors"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input type="text" x-model="searchQuery"
class="block w-64 pl-10 pr-3 py-2.5 border border-slate-200 rounded-xl leading-5 bg-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all text-sm shadow-sm"
placeholder="모델명, 제조사, CPU 검색...">
</div>
<!-- Search Bar for Departments -->
<div x-show="currentTab === 'depts'" class="relative group">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-blue-500 transition-colors"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input type="text" x-model="deptSearchQuery"
class="block w-64 pl-10 pr-3 py-2.5 border border-slate-200 rounded-xl leading-5 bg-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all text-sm shadow-sm"
placeholder="부서명 검색...">
</div>
<div class="flex gap-2">
<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 shrink-0">신규
모델 등록</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 shrink-0">신규
부서 생성</button>
</div>
</div>
</header>
@ -124,7 +152,7 @@
</tr>
</thead>
<tbody class="divide-y divide-slate-50">
<template x-for="model in models" :key="model.id">
<template x-for="model in filteredModels" :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"
@ -172,6 +200,9 @@
</tbody>
</table>
</div>
<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>
</div>
</div>
<!-- Tab 2: Departments -->
@ -189,7 +220,7 @@
</tr>
</thead>
<tbody class="divide-y divide-slate-50">
<template x-for="dept in depts" :key="dept.id">
<template x-for="dept in filteredDepts" :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">
@ -219,6 +250,9 @@
</tbody>
</table>
</div>
<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>
</div>
</div>
</main>
@ -445,6 +479,7 @@
showModal: false,
isEdit: false,
selectedIds: [],
searchQuery: '',
bulkManufacturer: '',
bulkProductName: '',
formData: {
@ -455,15 +490,34 @@
asset_status: '', fixed_ip: '', assigned_user: '', purchase_date: '',
power_rating: '', vendor: '', options: '', remarks: '', specs: ''
},
get filteredModels() {
if (!this.searchQuery) return this.models;
const q = this.searchQuery.toLowerCase();
return this.models.filter(m =>
(m.model_name && m.model_name.toLowerCase().includes(q)) ||
(m.manufacturer && m.manufacturer.toLowerCase().includes(q)) ||
(m.cpu && m.cpu.toLowerCase().includes(q)) ||
(m.product_name && m.product_name.toLowerCase().includes(q))
);
},
// Dept Data
depts: [],
deptSearchQuery: '',
showDeptModal: false,
showMembersModal: false,
isDeptEdit: false,
selectedDept: null,
members: [],
deptFormData: { id: '', name: '', parent_id: '', level: 1 },
get filteredDepts() {
if (!this.deptSearchQuery) return this.depts;
const q = this.deptSearchQuery.toLowerCase();
return this.depts.filter(d =>
(d.name && d.name.toLowerCase().includes(q)) ||
(d.path && d.path.toLowerCase().includes(q))
);
},
init() {
this.fetchModels();

31
reset_admin.php Normal file
View file

@ -0,0 +1,31 @@
<?php
require_once 'config.php';
// administrators 테이블이 없으면 생성
$db->exec("CREATE TABLE IF NOT EXISTS administrators (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login_id TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
admin_name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)");
$login_id = 'admin';
$password = 'admin1234';
$admin_name = 'Admin';
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $db->prepare("SELECT id FROM administrators WHERE login_id = ?");
$stmt->execute([$login_id]);
if ($stmt->fetch()) {
$stmt = $db->prepare("UPDATE administrators SET password_hash = ?, admin_name = ? WHERE login_id = ?");
$stmt->execute([$hash, $admin_name, $login_id]);
echo "기존 admin 계정의 비밀번호를 admin1234로 초기화했습니다.<br>";
} else {
$stmt = $db->prepare("INSERT INTO administrators (login_id, password_hash, admin_name) VALUES (?, ?, ?)");
$stmt->execute([$login_id, $hash, $admin_name]);
echo "신규 admin 계정(PW: admin1234)을 생성했습니다.<br>";
}
echo "<br><a href='login.php'>로그인 페이지로 이동</a>";
?>