gogo
This commit is contained in:
parent
cfeea88124
commit
980483278a
2 changed files with 205 additions and 3 deletions
67
api.php
67
api.php
|
|
@ -891,6 +891,73 @@ LIMIT 1")->fetchColumn();
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'export_users_csv':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$userIds = $data['user_ids'] ?? [];
|
||||||
|
$columns = $data['columns'] ?? []; // ['name' => '이름', 'emp_id' => '사번', ...]
|
||||||
|
|
||||||
|
if (empty($columns)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => '내보낼 컬럼을 선택해주세요.']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전체 유저 정보 조회 (기본 쿼리 재사용)
|
||||||
|
$query = "WITH RECURSIVE dept_path(id, path) AS (
|
||||||
|
SELECT id, name FROM departments WHERE parent_id IS NULL
|
||||||
|
UNION ALL
|
||||||
|
SELECT d.id, dp.path || ' > ' || d.name
|
||||||
|
FROM departments d JOIN dept_path dp ON d.parent_id = dp.id
|
||||||
|
)
|
||||||
|
SELECT u.*, dp.path as dept_name,
|
||||||
|
(SELECT asset_tag FROM laptop_assets WHERE current_user_id = u.id ORDER BY id DESC LIMIT 1) as laptop_tag
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN dept_path dp ON u.department_id = dp.id";
|
||||||
|
|
||||||
|
if (!empty($userIds)) {
|
||||||
|
$placeholders = implode(',', array_fill(0, count($userIds), '?'));
|
||||||
|
$query .= " WHERE u.id IN ($placeholders)";
|
||||||
|
$stmt = $db->prepare($query);
|
||||||
|
$stmt->execute($userIds);
|
||||||
|
} else {
|
||||||
|
$stmt = $db->query($query);
|
||||||
|
}
|
||||||
|
|
||||||
|
$users = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// CSV 스트림 생성
|
||||||
|
header('Content-Type: text/csv; charset=utf-8');
|
||||||
|
header('Content-Disposition: attachment; filename=직원_마스터_리스트_' . date('Ymd_His') . '.csv');
|
||||||
|
|
||||||
|
$output = fopen('php://output', 'w');
|
||||||
|
|
||||||
|
// Zero-Mojibake: BOM (Byte Order Mark) 삽입
|
||||||
|
fwrite($output, "\xEF\xBB\xBF");
|
||||||
|
|
||||||
|
// 헤더 작성
|
||||||
|
fputcsv($output, array_values($columns));
|
||||||
|
|
||||||
|
// 데이터 작성
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$row = [];
|
||||||
|
foreach (array_keys($columns) as $key) {
|
||||||
|
$val = $user[$key] ?? '';
|
||||||
|
// 상태 값 한글 변환
|
||||||
|
if ($key === 'status') {
|
||||||
|
$val = match ($val) {
|
||||||
|
'active' => '재직',
|
||||||
|
'retired' => '퇴사',
|
||||||
|
'on_leave' => '휴직',
|
||||||
|
'classification' => '분류',
|
||||||
|
default => $val
|
||||||
|
};
|
||||||
|
}
|
||||||
|
$row[] = $val;
|
||||||
|
}
|
||||||
|
fputcsv($output, $row);
|
||||||
|
}
|
||||||
|
fclose($output);
|
||||||
|
exit;
|
||||||
|
|
||||||
case 'get_general_rentals':
|
case 'get_general_rentals':
|
||||||
$query = "SELECT r.*, GROUP_CONCAT(i.item_name, ', ') as items
|
$query = "SELECT r.*, GROUP_CONCAT(i.item_name, ', ') as items
|
||||||
FROM general_rentals r
|
FROM general_rentals r
|
||||||
|
|
|
||||||
141
users.php
141
users.php
|
|
@ -88,6 +88,14 @@
|
||||||
</svg>
|
</svg>
|
||||||
불러오기
|
불러오기
|
||||||
</button>
|
</button>
|
||||||
|
<button @click="openExportModal()"
|
||||||
|
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 flex items-center shrink-0">
|
||||||
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
CSV 내보내기
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -152,13 +160,13 @@
|
||||||
|
|
||||||
<div class="h-8 w-px bg-white/10 mx-1"></div>
|
<div class="h-8 w-px bg-white/10 mx-1"></div>
|
||||||
|
|
||||||
<button @click="archiveUsers"
|
<button @click="openExportModal"
|
||||||
class="bg-emerald-600 hover:bg-emerald-700 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-emerald-500/20 active:scale-95 flex items-center gap-2">
|
class="bg-emerald-600 hover:bg-emerald-700 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-emerald-500/20 active:scale-95 flex items-center gap-2">
|
||||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
|
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
내보내기
|
CSV 다운로드
|
||||||
</button>
|
</button>
|
||||||
<button @click="bulkDelete"
|
<button @click="bulkDelete"
|
||||||
class="bg-rose-600 hover:bg-rose-700 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-rose-500/20 active:scale-95 flex items-center gap-2">
|
class="bg-rose-600 hover:bg-rose-700 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-rose-500/20 active:scale-95 flex items-center gap-2">
|
||||||
|
|
@ -623,6 +631,72 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- CSV Export Modal -->
|
||||||
|
<div x-show="showExportModal" x-cloak class="fixed inset-0 z-[130] flex items-center justify-center p-4">
|
||||||
|
<div class="fixed inset-0 modal-bg" @click="showExportModal = false"></div>
|
||||||
|
<div class="bg-white rounded-[2.5rem] p-8 max-w-lg w-full relative z-[131] shadow-2xl overflow-hidden">
|
||||||
|
<div class="flex justify-between items-start mb-6">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-2xl font-black text-slate-900">CSV 내보내기 설정</h3>
|
||||||
|
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mt-1">Select Columns to Export
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button @click="showExportModal = false" class="text-slate-300 hover:text-slate-900 transition-colors">
|
||||||
|
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<span class="text-sm font-bold text-slate-600">내보낼 항목 선택</span>
|
||||||
|
<button @click="toggleAllColumns" class="text-xs font-bold text-blue-600 hover:underline">전체
|
||||||
|
선택/해제</button>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<template x-for="(label, key) in columnOptions" :key="key">
|
||||||
|
<label
|
||||||
|
class="flex items-center p-3 bg-slate-50 rounded-xl cursor-pointer hover:bg-blue-50 transition-colors border border-transparent"
|
||||||
|
:class="selectedColumns.includes(key) ? 'border-blue-200 bg-blue-50/50' : ''">
|
||||||
|
<input type="checkbox" :value="key" x-model="selectedColumns"
|
||||||
|
class="w-4 h-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500 mr-3">
|
||||||
|
<span class="text-sm font-medium text-slate-700" x-text="label"></span>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="p-4 bg-amber-50 rounded-2xl border border-amber-100 mb-2">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<svg class="w-5 h-5 text-amber-500 shrink-0" 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>
|
||||||
|
<p class="text-[11px] font-bold text-amber-700 leading-normal">
|
||||||
|
<span x-show="selectedIds.length > 0"><span class="text-rose-600"
|
||||||
|
x-text="selectedIds.length"></span>명의 선택된 직원 정보를 내보냅니다.</span>
|
||||||
|
<span x-show="selectedIds.length === 0">전체 <span class="text-blue-600"
|
||||||
|
x-text="users.length"></span>명의 직원 정보를 내보냅니다.</span>
|
||||||
|
<br>UTF-8 BOM이 포함되어 엑셀에서 바로 열어도 한글이 깨지지 않습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button @click="downloadCSV" :disabled="selectedColumns.length === 0"
|
||||||
|
class="w-full py-4 bg-blue-600 text-white rounded-2xl font-black hover:bg-blue-700 transition-all shadow-lg shadow-blue-200 disabled:opacity-50 disabled:shadow-none flex items-center justify-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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||||
|
</svg>
|
||||||
|
CSV 다운로드 시작
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function userManagement() {
|
function userManagement() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -643,8 +717,22 @@
|
||||||
bulkPosition: '',
|
bulkPosition: '',
|
||||||
showRentalListModal: false,
|
showRentalListModal: false,
|
||||||
showRestoreModal: false,
|
showRestoreModal: false,
|
||||||
|
showExportModal: false,
|
||||||
userRentals: [],
|
userRentals: [],
|
||||||
archivedUsers: [],
|
archivedUsers: [],
|
||||||
|
selectedColumns: ['name', 'emp_id', 'dept_name', 'position', 'email', 'mobile', 'status', 'laptop_tag'],
|
||||||
|
columnOptions: {
|
||||||
|
'name': '이름',
|
||||||
|
'emp_id': '사번',
|
||||||
|
'dept_name': '부서',
|
||||||
|
'position': '직위',
|
||||||
|
'email': '이메일',
|
||||||
|
'mobile': '휴대폰',
|
||||||
|
'phone': '사내전화',
|
||||||
|
'accounting_type': '회계구분',
|
||||||
|
'status': '상태',
|
||||||
|
'laptop_tag': '노트북 자산번호'
|
||||||
|
},
|
||||||
formData: { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active', phone: '' },
|
formData: { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active', phone: '' },
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|
@ -917,6 +1005,53 @@
|
||||||
this.fetchUsers();
|
this.fetchUsers();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
openExportModal() {
|
||||||
|
this.showExportModal = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleAllColumns() {
|
||||||
|
if (this.selectedColumns.length === Object.keys(this.columnOptions).length) {
|
||||||
|
this.selectedColumns = [];
|
||||||
|
} else {
|
||||||
|
this.selectedColumns = Object.keys(this.columnOptions);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async downloadCSV() {
|
||||||
|
const columns = {};
|
||||||
|
this.selectedColumns.forEach(key => {
|
||||||
|
columns[key] = this.columnOptions[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('api.php?action=export_users_csv', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_ids: this.selectedIds,
|
||||||
|
columns: columns
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Download failed');
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `직원목록_${new Date().toISOString().slice(0, 10)}.csv`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
this.showExportModal = false;
|
||||||
|
|
||||||
|
window.showAlert('CSV 파일 다운로드가 시작되었습니다.', '다운로드 성공', 'success');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
window.showAlert('다운로드 중 오류가 발생했습니다.', '오류', 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue