feat(laptops): implement Excel/CSV bulk import with client-side interactive mapping and encoding detection

This commit is contained in:
NAS-Admin 2026-06-01 10:23:05 +09:00
parent ab27bfc34d
commit 3486c6dcc3
2 changed files with 506 additions and 0 deletions

135
api.php
View file

@ -363,6 +363,141 @@ WHERE a.current_user_id = ?";
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
break; 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': case 'update_asset_remarks':
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
$stmt = $db->prepare("UPDATE laptop_assets SET remarks = ? WHERE id = ?"); $stmt = $db->prepare("UPDATE laptop_assets SET remarks = ? WHERE id = ?");

View file

@ -58,6 +58,14 @@
</svg> </svg>
엑셀 내보내기 엑셀 내보내기
</button> </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()" <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"> 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>
</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> <script>
function assetManagement() { function assetManagement() {
return { return {
@ -620,6 +820,11 @@
showUserDropdown: false, showUserDropdown: false,
disposalSearchQuery: '', disposalSearchQuery: '',
showDisposalDropdown: false, showDisposalDropdown: false,
showImportModal: false,
importedAssets: [],
importEncoding: 'utf-8',
importFileName: '',
importRawText: '',
get filteredDisposalUsers() { get filteredDisposalUsers() {
if (!this.disposalSearchQuery) return this.allUsers.slice(0, 50); if (!this.disposalSearchQuery) return this.allUsers.slice(0, 50);
@ -977,6 +1182,172 @@
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); 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');
});
} }
} }
} }