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]);
|
||||
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':
|
||||
$query = "SELECT r.*, GROUP_CONCAT(i.item_name, ', ') as items
|
||||
FROM general_rentals r
|
||||
|
|
|
|||
141
users.php
141
users.php
|
|
@ -88,6 +88,14 @@
|
|||
</svg>
|
||||
불러오기
|
||||
</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>
|
||||
</header>
|
||||
|
|
@ -152,13 +160,13 @@
|
|||
|
||||
<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">
|
||||
<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="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>
|
||||
내보내기
|
||||
CSV 다운로드
|
||||
</button>
|
||||
<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">
|
||||
|
|
@ -623,6 +631,72 @@
|
|||
</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>
|
||||
function userManagement() {
|
||||
return {
|
||||
|
|
@ -643,8 +717,22 @@
|
|||
bulkPosition: '',
|
||||
showRentalListModal: false,
|
||||
showRestoreModal: false,
|
||||
showExportModal: false,
|
||||
userRentals: [],
|
||||
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: '' },
|
||||
|
||||
init() {
|
||||
|
|
@ -917,6 +1005,53 @@
|
|||
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