feat(laptops): implement Excel/CSV bulk import with client-side interactive mapping and encoding detection
This commit is contained in:
parent
ab27bfc34d
commit
3486c6dcc3
2 changed files with 506 additions and 0 deletions
135
api.php
135
api.php
|
|
@ -363,6 +363,141 @@ WHERE a.current_user_id = ?";
|
|||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
case 'download_laptop_template':
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="FKI_Laptop_Import_Template.csv"');
|
||||
|
||||
// UTF-8 BOM to prevent Excel encoding issues
|
||||
echo "\xEF\xBB\xBF";
|
||||
|
||||
$headers = ['자산관리번호', '시리얼번호', 'IP주소', '상태', '비고', '취득일', '정격입출력', '옵션', 'CPU', 'NPU', 'RAM', 'HDD0모델', 'HDD0용량', 'HDD1모델', 'HDD1용량', '임시배정명', '노트북모델명', '배정사용자명'];
|
||||
$sample = ['0020010000245', '5CD0221MH9', '192.168.1.100', 'stock', '신규 구매 노트북', '2026-06-01', '20V / 3.25A', '지문인식, 백라이트', 'i5-1335U', '없음', '16GB', 'Imation NVMe M.2 1TB', '1TB', '슬롯없음', '슬롯없음', '업무용(TEMP_44666b)', 'HP 노트북 15S-FQ1005TU', '김건희'];
|
||||
|
||||
echo implode(',', $headers) . "\n";
|
||||
echo implode(',', $sample) . "\n";
|
||||
exit;
|
||||
|
||||
case 'bulk_add_laptop_assets':
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$assets = $data['assets'] ?? [];
|
||||
|
||||
if (empty($assets)) {
|
||||
echo json_encode(['success' => false, 'error' => '등록할 자산 데이터가 없습니다.']);
|
||||
break;
|
||||
}
|
||||
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
$check_stmt = $db->prepare("SELECT id FROM laptop_assets WHERE asset_tag = ?");
|
||||
|
||||
$insert_stmt = $db->prepare("INSERT INTO laptop_assets (
|
||||
asset_tag, model_id, current_user_id, status, serial_number, purchase_date, ip_address,
|
||||
cpu, npu, hdd0_model, hdd0_capacity, hdd1_model, hdd1_capacity, ram,
|
||||
asset_status, assigned_user_name, fixed_ip, options, power_rating, manufacturer, vendor, product_name, remarks
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
|
||||
$log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date, note) VALUES (?, 'assignment', ?, ?, '대량 가져오기로 배정됨')");
|
||||
|
||||
$inserted_count = 0;
|
||||
$duplicates = [];
|
||||
|
||||
foreach ($assets as $asset) {
|
||||
$asset_tag = trim($asset['asset_tag'] ?? '');
|
||||
if (empty($asset_tag)) continue;
|
||||
|
||||
// Check duplicate
|
||||
$check_stmt->execute([$asset_tag]);
|
||||
if ($check_stmt->fetch()) {
|
||||
$duplicates[] = $asset_tag;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get model details if model_id is specified
|
||||
$manufacturer = '';
|
||||
$vendor = '';
|
||||
$product_name = '';
|
||||
$model_id = $asset['model_id'] ?: null;
|
||||
if ($model_id) {
|
||||
$m_stmt = $db->prepare("SELECT manufacturer, vendor, product_name FROM laptop_models WHERE id = ?");
|
||||
$m_stmt->execute([$model_id]);
|
||||
$m_data = $m_stmt->fetch();
|
||||
if ($m_data) {
|
||||
$manufacturer = $m_data['manufacturer'] ?? '';
|
||||
$vendor = $m_data['vendor'] ?? '';
|
||||
$product_name = $m_data['product_name'] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
// Get user name if current_user_id is specified
|
||||
$assigned_user_name = $asset['assigned_user_name'] ?? '';
|
||||
$current_user_id = $asset['current_user_id'] ?: null;
|
||||
if ($current_user_id) {
|
||||
$u_stmt = $db->prepare("SELECT name FROM users WHERE id = ?");
|
||||
$u_stmt->execute([$current_user_id]);
|
||||
$assigned_user_name = $u_stmt->fetchColumn() ?: '';
|
||||
}
|
||||
|
||||
$status = ($asset['status'] ?? '') ?: ($current_user_id ? 'assigned' : 'stock');
|
||||
if ($status === 'stock' && empty($assigned_user_name)) {
|
||||
$assigned_user_name = '업무용(TEMP_44666b)';
|
||||
}
|
||||
|
||||
$insert_stmt->execute([
|
||||
$asset_tag,
|
||||
$model_id,
|
||||
$current_user_id,
|
||||
$status,
|
||||
$asset['serial_number'] ?? '',
|
||||
$asset['purchase_date'] ?? '',
|
||||
$asset['ip_address'] ?? null,
|
||||
$asset['cpu'] ?? '',
|
||||
$asset['npu'] ?? '',
|
||||
$asset['hdd0_model'] ?? '',
|
||||
$asset['hdd0_capacity'] ?? '',
|
||||
$asset['hdd1_model'] ?? '',
|
||||
$asset['hdd1_capacity'] ?? '',
|
||||
$asset['ram'] ?? '',
|
||||
$asset['asset_status'] ?? '',
|
||||
$assigned_user_name,
|
||||
$asset['fixed_ip'] ?? '',
|
||||
$asset['options'] ?? '',
|
||||
$asset['power_rating'] ?? '',
|
||||
$manufacturer,
|
||||
$vendor,
|
||||
$product_name,
|
||||
$asset['remarks'] ?? ''
|
||||
]);
|
||||
|
||||
$new_id = $db->lastInsertId();
|
||||
|
||||
if ($assigned_user_name && $status === 'assigned') {
|
||||
$log_stmt->execute([$new_id, $assigned_user_name, date('Y-m-d')]);
|
||||
}
|
||||
|
||||
$inserted_count++;
|
||||
}
|
||||
|
||||
if ($inserted_count === 0 && !empty($duplicates)) {
|
||||
$db->rollBack();
|
||||
echo json_encode(['success' => false, 'error' => '중복된 자산 번호만 포함되어 있어 등록되지 않았습니다: ' . implode(', ', $duplicates)]);
|
||||
break;
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
|
||||
$msg = "총 {$inserted_count}대의 노트북 자산이 일괄 등록되었습니다.";
|
||||
if (!empty($duplicates)) {
|
||||
$msg .= " (중복 자산 제외: " . implode(', ', $duplicates) . ")";
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'message' => $msg]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
echo json_encode(['success' => false, 'error' => '자산 일괄 등록 중 오류가 발생했습니다: ' . $e->getMessage()]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'update_asset_remarks':
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$stmt = $db->prepare("UPDATE laptop_assets SET remarks = ? WHERE id = ?");
|
||||
|
|
|
|||
371
laptops.php
371
laptops.php
|
|
@ -58,6 +58,14 @@
|
|||
</svg>
|
||||
엑셀 내보내기
|
||||
</button>
|
||||
<button @click="openImportModal()"
|
||||
class="bg-emerald-600 hover:bg-emerald-700 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-emerald-200 transition-all text-sm flex items-center gap-1.5">
|
||||
<svg class="w-4 h-4" 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-8l-4-4m0 0L8 8m4-4v12" />
|
||||
</svg>
|
||||
엑셀/CSV 가져오기
|
||||
</button>
|
||||
<button @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 text-sm">
|
||||
자산 등록
|
||||
|
|
@ -589,6 +597,198 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bulk Import Modal -->
|
||||
<div x-show="showImportModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
||||
<div x-show="showImportModal" @click="showImportModal = false" class="fixed inset-0 modal-bg transition-opacity"></div>
|
||||
<div x-show="showImportModal"
|
||||
class="bg-white rounded-[2rem] p-8 max-w-6xl w-full relative z-[111] shadow-2xl flex flex-col max-h-[90vh]">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-emerald-100 rounded-2xl flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-emerald-600" 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-8l-4-4m0 0L8 8m4-4v12" />
|
||||
</svg>
|
||||
</div>
|
||||
<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">Bulk Laptop Assets Import</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="showImportModal = false" class="text-slate-400 hover:text-slate-600 transition-colors">
|
||||
<svg class="h-6 w-6" 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>
|
||||
|
||||
<!-- Upload & Config Panel -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<div class="bg-blue-50/40 p-5 rounded-2xl border border-blue-100/40 space-y-2 flex flex-col justify-between">
|
||||
<div>
|
||||
<h4 class="text-xs font-black text-blue-600 uppercase tracking-wider mb-1">01. 표준 포맷 다운로드</h4>
|
||||
<p class="text-xs text-slate-500 font-medium">대량 등록용 표준 CSV 한글 양식을 다운로드하여 작성해 주세요.</p>
|
||||
</div>
|
||||
<a href="api.php?action=download_laptop_template"
|
||||
class="px-4 py-2.5 bg-blue-600 text-white rounded-xl text-xs font-black text-center hover:bg-blue-700 transition-all shadow-md shadow-blue-100 inline-block w-fit mt-2">
|
||||
양식 파일 받기 (.CSV)
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-50 p-5 rounded-2xl border border-slate-100 space-y-2 flex flex-col justify-between">
|
||||
<div>
|
||||
<h4 class="text-xs font-black text-slate-600 uppercase tracking-wider mb-1">02. 파일 인코딩 선택</h4>
|
||||
<p class="text-xs text-slate-500 font-medium">업로드한 파일에서 한글이 깨진다면 인코딩 방식을 맞춤 조정해 주세요.</p>
|
||||
</div>
|
||||
<label class="flex items-center gap-2.5 p-2 bg-white rounded-xl border border-slate-200 cursor-pointer hover:bg-slate-100 transition-colors w-fit mt-2">
|
||||
<input type="checkbox" x-model="importEncoding"
|
||||
@change="reParseCSV()"
|
||||
true-value="euc-kr" false-value="utf-8"
|
||||
class="w-4 h-4 rounded border-slate-300 text-emerald-600 focus:ring-emerald-500">
|
||||
<span class="text-xs font-bold text-slate-700">MS Excel 한글 깨짐 방지 (EUC-KR) 적용</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="bg-emerald-50/40 p-5 rounded-2xl border border-emerald-100/40 space-y-2 flex flex-col justify-between">
|
||||
<div>
|
||||
<h4 class="text-xs font-black text-emerald-600 uppercase tracking-wider mb-1">03. CSV 파일 가져오기</h4>
|
||||
<p class="text-xs text-slate-500 font-medium">준비된 노트북 자산 목록 파일을 첨부해 주세요.</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<input type="file" @change="handleCSVFile($event)" accept=".csv" class="hidden" id="csvFileInput">
|
||||
<button type="button" @click="document.getElementById('csvFileInput').click()"
|
||||
class="px-5 py-2.5 bg-emerald-600 text-white rounded-xl text-xs font-black hover:bg-emerald-700 transition-all shadow-md shadow-emerald-100">
|
||||
파일 첨부하기
|
||||
</button>
|
||||
<span class="text-xs font-bold text-slate-400 truncate max-w-[150px]"
|
||||
x-text="importFileName || '선택된 파일 없음'"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Mapping Panel -->
|
||||
<div class="flex-1 flex flex-col overflow-hidden bg-slate-50/50 rounded-3xl border border-slate-100 p-6">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest">가져온 데이터 실시간 매핑 테이블</h4>
|
||||
<div class="text-xs text-slate-400 font-bold">
|
||||
총 <span class="text-blue-600 font-black" x-text="importedAssets.length"></span>대 자산 대기 중
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template x-if="importedAssets.length === 0">
|
||||
<div class="flex-1 flex flex-col items-center justify-center py-20">
|
||||
<svg class="w-12 h-12 text-slate-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 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>
|
||||
<p class="text-xs font-bold text-slate-400 italic">표준 포맷 파일을 업로드하면 이곳에 매핑 화면이 나타납니다.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="importedAssets.length > 0">
|
||||
<div class="flex-1 overflow-auto rounded-2xl border border-slate-200/80 bg-white border-t border-slate-100">
|
||||
<table class="min-w-full divide-y divide-slate-100">
|
||||
<thead class="bg-slate-50 sticky top-0 z-20">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-[10px] font-black text-slate-400 uppercase tracking-wider w-32">자산관리번호</th>
|
||||
<th class="px-4 py-3 text-left text-[10px] font-black text-slate-400 uppercase tracking-wider w-36">기기 정보 (S/N / IP)</th>
|
||||
<th class="px-4 py-3 text-left text-[10px] font-black text-slate-400 uppercase tracking-wider">
|
||||
노트북 모델 연동 (현재값 매핑)
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-[10px] font-black text-slate-400 uppercase tracking-wider">
|
||||
배정 사용자 (현재값 매핑)
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-[10px] font-black text-slate-400 uppercase tracking-wider w-32">임시배정명 (STOCK)</th>
|
||||
<th class="px-4 py-3 text-center text-[10px] font-black text-slate-400 uppercase tracking-wider w-16">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 text-xs font-medium">
|
||||
<template x-for="(asset, index) in importedAssets" :key="asset.temp_id">
|
||||
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||
<td class="px-4 py-3">
|
||||
<input type="text" x-model="asset.asset_tag"
|
||||
class="px-2 py-1.5 border border-slate-200 rounded-lg text-xs font-bold w-full bg-slate-50 focus:bg-white outline-none">
|
||||
</td>
|
||||
<td class="px-4 py-3 space-y-1">
|
||||
<div class="flex gap-1 items-center">
|
||||
<span class="w-8 shrink-0 text-[9px] font-black text-slate-400 uppercase">S/N</span>
|
||||
<input type="text" x-model="asset.serial_number" placeholder="없음"
|
||||
class="px-1.5 py-1 border border-slate-200 rounded-lg text-[10px] w-full outline-none">
|
||||
</div>
|
||||
<div class="flex gap-1 items-center">
|
||||
<span class="w-8 shrink-0 text-[9px] font-black text-slate-400 uppercase">IP</span>
|
||||
<input type="text" x-model="asset.ip_address" placeholder="자동 할당"
|
||||
class="px-1.5 py-1 border border-slate-200 rounded-lg text-[10px] w-full outline-none font-mono">
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="space-y-1">
|
||||
<select x-model="asset.model_id"
|
||||
:class="asset.model_id ? 'border-emerald-200 bg-emerald-50/30 text-emerald-700' : 'border-slate-200 bg-slate-50 text-slate-500'"
|
||||
class="px-2.5 py-2 border rounded-xl text-xs font-black w-full focus:ring-2 focus:ring-blue-500 outline-none transition-all appearance-none cursor-pointer">
|
||||
<option value="">모델 선택 (미지정시 사양 연동 제한)</option>
|
||||
<template x-for="model in models" :key="model.id">
|
||||
<option :value="model.id" x-text="'[' + model.manufacturer + '] ' + model.model_name"></option>
|
||||
</template>
|
||||
</select>
|
||||
<div class="text-[9px] text-slate-400 font-bold px-1" x-show="asset.temp_model_name">
|
||||
파일 기재명: <span class="text-slate-600" x-text="asset.temp_model_name"></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="space-y-1">
|
||||
<select x-model="asset.current_user_id"
|
||||
:class="asset.current_user_id ? 'border-blue-200 bg-blue-50/30 text-blue-700' : 'border-slate-200 bg-slate-50 text-slate-500'"
|
||||
class="px-2.5 py-2 border rounded-xl text-xs font-black w-full focus:ring-2 focus:ring-blue-500 outline-none transition-all appearance-none cursor-pointer">
|
||||
<option value="">미배정 (재고 상태)</option>
|
||||
<template x-for="user in allUsers" :key="user.id">
|
||||
<option :value="user.id" x-text="user.name + ' (' + user.emp_id + ') - ' + (user.dept_name ? user.dept_name.split(' > ').pop() : '미소속')"></option>
|
||||
</template>
|
||||
</select>
|
||||
<div class="text-[9px] text-slate-400 font-bold px-1" x-show="asset.temp_user_name">
|
||||
파일 기재명: <span class="text-slate-600" x-text="asset.temp_user_name"></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<input type="text" x-model="asset.assigned_user_name" placeholder="기본 업무용"
|
||||
class="px-2 py-1.5 border border-slate-200 rounded-lg text-xs font-bold w-full bg-slate-50 focus:bg-white outline-none">
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<button type="button" @click="importedAssets.splice(index, 1)"
|
||||
class="p-2 text-rose-500 hover:bg-rose-50 rounded-xl transition-colors">
|
||||
<svg class="w-4 h-4 mx-auto" 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>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Footer Action Buttons -->
|
||||
<div class="pt-6 flex gap-4">
|
||||
<button type="button" @click="showImportModal = false"
|
||||
class="flex-1 py-4 bg-slate-100 text-slate-600 rounded-2xl font-bold hover:bg-slate-200 transition-all text-sm">
|
||||
취소
|
||||
</button>
|
||||
<button type="button" @click="submitBulkAssets()"
|
||||
:disabled="importedAssets.length === 0"
|
||||
:class="importedAssets.length === 0 ? 'bg-slate-200 text-slate-400 cursor-not-allowed' : 'bg-emerald-600 text-white hover:bg-emerald-700 shadow-lg shadow-emerald-100'"
|
||||
class="flex-1 py-4 rounded-2xl font-bold transition-all text-sm">
|
||||
일괄 등록 완료
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function assetManagement() {
|
||||
return {
|
||||
|
|
@ -620,6 +820,11 @@
|
|||
showUserDropdown: false,
|
||||
disposalSearchQuery: '',
|
||||
showDisposalDropdown: false,
|
||||
showImportModal: false,
|
||||
importedAssets: [],
|
||||
importEncoding: 'utf-8',
|
||||
importFileName: '',
|
||||
importRawText: '',
|
||||
|
||||
get filteredDisposalUsers() {
|
||||
if (!this.disposalSearchQuery) return this.allUsers.slice(0, 50);
|
||||
|
|
@ -977,6 +1182,172 @@
|
|||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
},
|
||||
|
||||
openImportModal() {
|
||||
this.importedAssets = [];
|
||||
this.importFileName = '';
|
||||
this.importRawText = '';
|
||||
this.importEncoding = 'utf-8';
|
||||
const fileInput = document.getElementById('csvFileInput');
|
||||
if (fileInput) fileInput.value = '';
|
||||
this.showImportModal = true;
|
||||
},
|
||||
|
||||
handleCSVFile(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
this.importFileName = file.name;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.importRawText = e.target.result;
|
||||
this.parseCSV(this.importRawText);
|
||||
};
|
||||
reader.readAsText(file, this.importEncoding);
|
||||
},
|
||||
|
||||
reParseCSV() {
|
||||
if (!this.importFileName) return;
|
||||
|
||||
const fileInput = document.getElementById('csvFileInput');
|
||||
if (fileInput && fileInput.files[0]) {
|
||||
const file = fileInput.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.importRawText = e.target.result;
|
||||
this.parseCSV(this.importRawText);
|
||||
};
|
||||
reader.readAsText(file, this.importEncoding);
|
||||
}
|
||||
},
|
||||
|
||||
parseCSV(text) {
|
||||
const lines = [];
|
||||
let row = [""];
|
||||
let inQuotes = false;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
const next = text[i+1];
|
||||
if (c === '"') {
|
||||
if (inQuotes && next === '"') {
|
||||
row[row.length - 1] += '"';
|
||||
i++;
|
||||
} else {
|
||||
inQuotes = !inQuotes;
|
||||
}
|
||||
} else if (c === ',' && !inQuotes) {
|
||||
row.push("");
|
||||
} else if ((c === '\r' || c === '\n') && !inQuotes) {
|
||||
if (c === '\r' && next === '\n') {
|
||||
i++;
|
||||
}
|
||||
if (row.length > 1 || row[0] !== "") {
|
||||
lines.push(row);
|
||||
}
|
||||
row = [""];
|
||||
} else {
|
||||
row[row.length - 1] += c;
|
||||
}
|
||||
}
|
||||
if (row.length > 1 || row[0] !== "") {
|
||||
lines.push(row);
|
||||
}
|
||||
|
||||
if (lines.length < 2) {
|
||||
window.showAlert('유효한 데이터 행이 없습니다.', '가져오기 실패', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = lines[0].map(h => h.trim().replace(/^["']|["']$/g, ''));
|
||||
const dataRows = lines.slice(1);
|
||||
|
||||
this.importedAssets = dataRows.map((line, rowIndex) => {
|
||||
const asset = {
|
||||
asset_tag: '', serial_number: '', ip_address: '', status: 'stock',
|
||||
remarks: '', purchase_date: '', power_rating: '', options: '',
|
||||
cpu: '', npu: '', ram: '', hdd0_model: '', hdd0_capacity: '',
|
||||
hdd1_model: '', hdd1_capacity: '', assigned_user_name: '',
|
||||
temp_model_name: '', temp_user_name: '', model_id: '', current_user_id: ''
|
||||
};
|
||||
|
||||
headers.forEach((header, colIndex) => {
|
||||
const val = (line[colIndex] || "").trim().replace(/^["']|["']$/g, '');
|
||||
if (header === '자산관리번호') asset.asset_tag = val;
|
||||
else if (header === '시리얼번호') asset.serial_number = val;
|
||||
else if (header === 'IP주소') asset.ip_address = val;
|
||||
else if (header === '상태') asset.status = val || 'stock';
|
||||
else if (header === '비고') asset.remarks = val;
|
||||
else if (header === '취득일') asset.purchase_date = val;
|
||||
else if (header === '정격입출력') asset.power_rating = val;
|
||||
else if (header === '옵션') asset.options = val;
|
||||
else if (header === 'CPU') asset.cpu = val;
|
||||
else if (header === 'NPU') asset.npu = val;
|
||||
else if (header === 'RAM') asset.ram = val;
|
||||
else if (header === 'HDD0모델') asset.hdd0_model = val;
|
||||
else if (header === 'HDD0용량') asset.hdd0_capacity = val;
|
||||
else if (header === 'HDD1모델') asset.hdd1_model = val;
|
||||
else if (header === 'HDD1용량') asset.hdd1_capacity = val;
|
||||
else if (header === '임시배정명') asset.assigned_user_name = val;
|
||||
else if (header === '노트북모델명') asset.temp_model_name = val;
|
||||
else if (header === '배정사용자명') asset.temp_user_name = val;
|
||||
});
|
||||
|
||||
// Auto-match Model ID based on model_name
|
||||
asset.model_id = "";
|
||||
if (asset.temp_model_name) {
|
||||
const matchedModel = this.models.find(m => m.model_name.toLowerCase().includes(asset.temp_model_name.toLowerCase()) || asset.temp_model_name.toLowerCase().includes(m.model_name.toLowerCase()));
|
||||
if (matchedModel) {
|
||||
asset.model_id = matchedModel.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-match User ID based on user_name
|
||||
asset.current_user_id = "";
|
||||
if (asset.temp_user_name) {
|
||||
const matchedUser = this.allUsers.find(u => u.name.toLowerCase() === asset.temp_user_name.toLowerCase());
|
||||
if (matchedUser) {
|
||||
asset.current_user_id = matchedUser.id;
|
||||
}
|
||||
}
|
||||
|
||||
asset.temp_id = rowIndex + '_' + Date.now();
|
||||
return asset;
|
||||
}).filter(a => a.asset_tag);
|
||||
},
|
||||
|
||||
submitBulkAssets() {
|
||||
const tags = this.importedAssets.map(a => a.asset_tag.trim());
|
||||
const uniqueTags = new Set(tags);
|
||||
if (tags.length !== uniqueTags.size) {
|
||||
window.showAlert('가져오기 목록에 중복된 자산관리번호가 포함되어 있습니다.', '검증 오류', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.importedAssets.some(a => !a.asset_tag.trim())) {
|
||||
window.showAlert('자산관리번호는 필수 입력 항목입니다.', '검증 오류', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('api.php?action=bulk_add_laptop_assets', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
assets: this.importedAssets
|
||||
})
|
||||
}).then(res => res.json()).then(data => {
|
||||
if (data.success) {
|
||||
window.showAlert(data.message || '일괄 등록이 완료되었습니다.', '일괄 등록 성공', 'success');
|
||||
this.showImportModal = false;
|
||||
this.fetchAssets();
|
||||
} else {
|
||||
window.showAlert(data.error || '자산 일괄 등록 중 오류가 발생했습니다.', '일괄 등록 실패', 'error');
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
window.showAlert('서버와의 통신에 실패했습니다.', '통신 오류', 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue