반영보자

This commit is contained in:
NAS-Admin 2026-02-08 21:28:22 +09:00
parent e1372061e1
commit e333930c09
3 changed files with 93 additions and 30 deletions

33
api.php
View file

@ -387,7 +387,7 @@ try {
echo json_encode(['success' => true, 'merged' => true]);
} else {
// Check if a department with the same name exists under the same parent (excluding itself)
$check_stmt = $db->prepare("SELECT id FROM departments WHERE name = ? AND (parent_id <=> ?) AND id != ?");
$check_stmt = $db->prepare("SELECT id FROM departments WHERE name = ? AND parent_id IS ? AND id != ?");
$check_stmt->execute([$new_name, $parent_id, $dept_id]);
$existing = $check_stmt->fetch();
@ -403,18 +403,32 @@ try {
case 'delete_dept':
$dept_id = $_GET['id'];
// 1. Find the '미소속' department ID
$miso_id = $db->query("SELECT id FROM departments WHERE name = '미소속'")->fetchColumn();
// Find FKI Root
$fki_root = $db->query("SELECT id FROM departments WHERE name = '한국경제인협회' AND parent_id IS NULL LIMIT 1")->fetch();
$fki_root_id = $fki_root ? $fki_root['id'] : null;
// 1. Find the '미소속' department under FKI Root
$miso_id = $db->query("SELECT id FROM departments WHERE name = '미소속' AND parent_id IS " . ($fki_root_id ?: 'NULL') . " LIMIT 1")->fetchColumn();
if (!$miso_id) {
// Fallback creation if not exists
$db->exec("INSERT INTO departments (name, level) VALUES ('미소속', 0)");
$db->prepare("INSERT INTO departments (name, parent_id, level) VALUES ('미소속', ?, 1)")->execute([$fki_root_id]);
$miso_id = $db->lastInsertId();
}
// 2. Move members to '미소속'
// 2. Get current dept info for re-parenting children
$current_dept = $db->prepare("SELECT parent_id FROM departments WHERE id = ?");
$current_dept->execute([$dept_id]);
$parent_info = $current_dept->fetch();
$target_new_parent = $parent_info ? $parent_info['parent_id'] : null;
// 3. Move members to '미소속'
$db->prepare("UPDATE users SET department_id = ? WHERE department_id = ?")->execute([$miso_id, $dept_id]);
// 3. Delete the department
// 4. Move child departments to its grandparent (re-parenting) to avoid FK constraint violation
$db->prepare("UPDATE departments SET parent_id = ? WHERE parent_id = ?")->execute([$target_new_parent, $dept_id]);
// 5. Delete the department
$db->prepare("DELETE FROM departments WHERE id = ?")->execute([$dept_id]);
echo json_encode(['success' => true]);
@ -582,6 +596,11 @@ try {
$updates[] = "department_id = ?";
$params[] = $newDeptId;
}
$newPosition = $data['position'] ?? null;
if ($newPosition) {
$updates[] = "position = ?";
$params[] = $newPosition;
}
if (empty($updates)) {
echo json_encode(['success' => false, 'error' => 'No updates specified']);

View file

@ -88,10 +88,6 @@
<div class="bg-white rounded-3xl p-8 max-w-md w-full relative z-[111] shadow-2xl">
<div class="flex justify-between items-center mb-6">
<h3 class="text-2xl font-black text-slate-900" x-text="isEdit ? '부서 정보 수정' : '신규 부서 생성'"></h3>
<button x-show="isEdit" type="button" @click="deleteDept()"
class="text-xs font-bold text-rose-500 hover:text-rose-700 bg-rose-50 px-3 py-1.5 rounded-lg border border-rose-100 transition-colors">
부서 제거
</button>
</div>
<form @submit.prevent="submitDept" class="space-y-4">
<div>
@ -115,6 +111,13 @@
<button type="submit" class="flex-1 py-3 bg-blue-600 text-white rounded-xl font-bold"
x-text="isEdit ? '수정 완료' : '부서 생성'"></button>
</div>
<div x-show="isEdit" class="pt-4 mt-4 border-t border-slate-100">
<button type="button" @click="deleteDept()"
class="w-full py-3 text-sm font-bold text-rose-500 hover:text-white hover:bg-rose-500 border border-rose-200 rounded-xl transition-all">
부서 삭제하기
</button>
<p class="text-[10px] text-slate-400 mt-2 text-center"> 삭제 소속 인원은 '미소속'으로 이동됩니다.</p>
</div>
</form>
</div>
</div>
@ -245,12 +248,22 @@
deleteDept() {
if (confirm(`정말로 '${this.formData.name}' 부서를 제거하시겠습니까?\n이 명령을 수행하면 소속된 부서원들은 자동으로 '[한국경제인협회] - 미소속' 부서로 이동됩니다.\n이 작업은 되돌릴 수 없습니다.`)) {
fetch(`api.php?action=delete_dept&id=${this.formData.id}`)
.then(res => res.json())
.then(res => {
if (!res.ok) throw new Error('네트워크 응답이 올바르지 않습니다.');
return res.json();
})
.then(data => {
if (data.success) {
alert('부서가 성공적으로 제거되었습니다.');
this.showModal = false;
this.fetchDepts();
} else {
alert('부서 제거 중 오류가 발생했습니다: ' + (data.error || '알 수 없는 오류'));
}
})
.catch(err => {
console.error(err);
alert('부서 제거 요청 중 오류가 발생했습니다.');
});
}
},

View file

@ -102,7 +102,8 @@
<div x-show="selectedIds.length > 0" x-transition x-cloak
class="fixed bottom-10 left-10 z-[200] bg-slate-900 text-white px-6 py-4 rounded-3xl flex items-center gap-6 shadow-2xl border border-white/10 animate-in fade-in slide-in-from-left-4 duration-300">
<div class="flex items-center gap-2">
<div class="w-10 h-10 bg-blue-600 rounded-full flex items-center justify-center font-black text-sm shadow-lg shadow-blue-500/30" x-text="selectedIds.length"></div>
<div class="w-10 h-10 bg-blue-600 rounded-full flex items-center justify-center font-black text-sm shadow-lg shadow-blue-500/30"
x-text="selectedIds.length"></div>
<div class="flex flex-col">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-tighter">Selected</span>
<span class="text-xs font-bold">인원 선택됨</span>
@ -116,6 +117,7 @@
<option value="active">재직</option>
<option value="retired">퇴사</option>
<option value="on_leave">휴직</option>
<option value="classification">분류</option>
</select>
<select x-model="bulkDeptId"
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]">
@ -124,11 +126,16 @@
<option :value="dept.id" x-text="dept.name"></option>
</template>
</select>
<input type="text" x-model="bulkPosition" placeholder="직위 변경..."
class="bg-slate-800 border-none rounded-xl text-xs font-bold px-4 py-2 focus:ring-2 focus:ring-blue-500 shadow-inner w-32">
<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">일괄 적용</button>
<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="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-blue-500/20 active:scale-95">일괄
적용</button>
<button @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">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
@ -160,8 +167,8 @@
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
</div>
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">연락처
</th>
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">연락처 /
사내전화</th>
<th @click="toggleSort('accounting_type')"
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:text-blue-600 transition-colors">
<div class="flex items-center">
@ -206,7 +213,16 @@
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-slate-600 font-medium" x-text="user.email || '-'"></div>
<div class="text-xs text-slate-400" x-text="user.mobile || user.phone || '-'"></div>
<div class="flex flex-col gap-0.5 mt-1">
<div class="text-[10px] text-slate-400 flex items-center">
<span class="w-12 font-bold uppercase tracking-tighter">Mobile</span>
<span class="text-slate-600 font-semibold" x-text="user.mobile || '-'"></span>
</div>
<div class="text-[10px] text-slate-400 flex items-center">
<span class="w-12 font-bold uppercase tracking-tighter">사내전화</span>
<span class="text-slate-600 font-semibold" x-text="user.phone || '-'"></span>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
@ -227,6 +243,10 @@
<span
class="px-3 py-1 text-xs font-bold rounded-full bg-amber-50 text-amber-600 border border-amber-100">휴직</span>
</template>
<template x-if="user.status === 'classification'">
<span
class="px-3 py-1 text-xs font-bold rounded-full bg-indigo-50 text-indigo-600 border border-indigo-100">분류</span>
</template>
</td>
</tr>
</template>
@ -285,13 +305,6 @@
<input type="email" x-model="formData.email"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">휴대폰</label>
<input type="text" x-model="formData.mobile"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
</div>
</div>
<div class="grid grid-cols-2 gap-6">
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">회계구분</label>
<select x-model="formData.accounting_type"
@ -300,6 +313,20 @@
<option value="특별회계">특별회계</option>
</select>
</div>
</div>
<div class="grid grid-cols-2 gap-6">
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">휴대폰</label>
<input type="text" x-model="formData.mobile"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">사내전화</label>
<input type="text" x-model="formData.phone"
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
</div>
</div>
<div class="grid grid-cols-2 gap-6">
<div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">상태</label>
<select x-model="formData.status"
@ -307,6 +334,7 @@
<option value="active">재직</option>
<option value="retired">퇴사</option>
<option value="on_leave">휴직</option>
<option value="classification">분류</option>
</select>
</div>
</div>
@ -337,7 +365,8 @@
selectedIds: [],
bulkStatus: '',
bulkDeptId: '',
formData: { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active' },
bulkPosition: '',
formData: { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active', phone: '' },
init() {
this.fetchUsers();
@ -355,7 +384,7 @@
result = result.filter(u => u.name.toLowerCase().includes(q) || u.emp_id.toLowerCase().includes(q) || (u.email && u.email.toLowerCase().includes(q)));
}
if (!this.filterRetired) result = result.filter(u => u.status !== 'retired');
if (!this.filterOnLeave) result = result.filter(u => u.status !== 'on_leave');
if (!this.filterOnLeave) result = result.filter(u => u.status !== 'on_leave' && u.status !== 'classification');
if (this.sortKey) {
result.sort((a, b) => {
@ -426,20 +455,22 @@
},
applyBulkUpdate() {
if (!this.bulkStatus && !this.bulkDeptId) return;
if (this.selectedIds.length === 0) return;
fetch('api.php?action=bulk_update_users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_ids: this.selectedIds,
status: this.bulkStatus,
department_id: this.bulkDeptId
department_id: this.bulkDeptId,
position: this.bulkPosition
})
}).then(res => res.json()).then(data => {
if (data.success) {
this.selectedIds = [];
this.bulkStatus = '';
this.bulkDeptId = '';
this.bulkPosition = '';
this.fetchUsers();
}
});