Files
sam-manage/app/Services/EquipmentImportService.php
김보곤 7f1327bfea feat: [equipment] 사진 멀티 업로드(GCS) + 엑셀 Import 기능 추가
- EquipmentPhotoService: GCS 기반 사진 업로드/삭제/조회 (최대 10장)
- EquipmentImportService: 엑셀 파싱 → 설비 일괄 등록 (한글 헤더 자동 매핑)
- API: 사진 업로드/목록/삭제, Import 미리보기/실행 엔드포인트
- 뷰: create/edit에 드래그앤드롭 사진 업로드, show에 갤러리 표시
- import.blade.php: 3단계 Import UI (파일선택 → 미리보기 → 결과)
- phpoffice/phpspreadsheet 패키지 추가
2026-02-25 20:15:06 +09:00

277 lines
8.1 KiB
PHP

<?php
namespace App\Services;
use App\Models\Equipment\Equipment;
use Illuminate\Support\Facades\Log;
use PhpOffice\PhpSpreadsheet\IOFactory;
class EquipmentImportService
{
private const HEADER_MAP = [
'설비코드' => 'equipment_code',
'설비명' => 'name',
'설비유형' => 'equipment_type',
'규격' => 'specification',
'제조사' => 'manufacturer',
'모델명' => 'model_name',
'제조번호' => 'serial_no',
'위치' => 'location',
'생산라인' => 'production_line',
'구입일' => 'purchase_date',
'설치일' => 'install_date',
'구입가격' => 'purchase_price',
'내용연수' => 'useful_life',
'상태' => 'status',
'담당자' => 'manager_name',
'비고' => 'memo',
// 영문 매핑 (호환)
'equipment_code' => 'equipment_code',
'name' => 'name',
'equipment_type' => 'equipment_type',
'specification' => 'specification',
'manufacturer' => 'manufacturer',
'model_name' => 'model_name',
'serial_no' => 'serial_no',
'location' => 'location',
'production_line' => 'production_line',
'purchase_date' => 'purchase_date',
'install_date' => 'install_date',
'purchase_price' => 'purchase_price',
'useful_life' => 'useful_life',
'status' => 'status',
'manager_name' => 'manager_name',
'memo' => 'memo',
];
private const STATUS_MAP = [
'가동' => 'active',
'사용' => 'active',
'정상' => 'active',
'유휴' => 'idle',
'대기' => 'idle',
'폐기' => 'disposed',
'처분' => 'disposed',
'active' => 'active',
'idle' => 'idle',
'disposed' => 'disposed',
];
/**
* 엑셀 파일 미리보기 (헤더 + 첫 10행)
*/
public function preview(string $filePath): array
{
$spreadsheet = IOFactory::load($filePath);
$sheet = $spreadsheet->getActiveSheet();
$rows = $sheet->toArray(null, true, true, true);
if (empty($rows)) {
throw new \RuntimeException('빈 파일입니다.');
}
// 첫 행 = 헤더
$headerRow = array_shift($rows);
$headers = $this->mapHeaders($headerRow);
if (empty($headers)) {
throw new \RuntimeException('인식 가능한 헤더가 없습니다. 설비코드, 설비명 등의 헤더가 필요합니다.');
}
// 미리보기 10행
$previewRows = [];
$count = 0;
foreach ($rows as $row) {
if ($count >= 10) {
break;
}
$mapped = $this->mapRow($row, $headers);
if ($mapped) {
$previewRows[] = $mapped;
$count++;
}
}
return [
'headers' => array_values($headers),
'original_headers' => array_values(array_filter($headerRow)),
'preview' => $previewRows,
'total_rows' => count($rows),
'mapped_columns' => count($headers),
];
}
/**
* 엑셀 데이터 일괄 등록
*/
public function import(string $filePath, array $options = []): array
{
$duplicateAction = $options['duplicate_action'] ?? 'skip';
$tenantId = session('selected_tenant_id', 1);
$spreadsheet = IOFactory::load($filePath);
$sheet = $spreadsheet->getActiveSheet();
$rows = $sheet->toArray(null, true, true, true);
if (empty($rows)) {
throw new \RuntimeException('빈 파일입니다.');
}
$headerRow = array_shift($rows);
$headers = $this->mapHeaders($headerRow);
$result = ['success' => 0, 'failed' => 0, 'skipped' => 0, 'errors' => []];
foreach ($rows as $rowNum => $row) {
$mapped = $this->mapRow($row, $headers);
if (! $mapped || empty($mapped['equipment_code'])) {
continue;
}
try {
$existing = Equipment::where('tenant_id', $tenantId)
->where('equipment_code', $mapped['equipment_code'])
->first();
if ($existing) {
if ($duplicateAction === 'overwrite') {
$this->updateEquipment($existing, $mapped);
$result['success']++;
} else {
$result['skipped']++;
}
continue;
}
$this->createEquipment($mapped, $tenantId);
$result['success']++;
} catch (\Exception $e) {
$result['failed']++;
$result['errors'][] = "".($rowNum + 2).": {$e->getMessage()}";
Log::warning('EquipmentImport: 행 처리 실패', [
'row' => $rowNum + 2,
'error' => $e->getMessage(),
]);
}
}
return $result;
}
private function mapHeaders(array $headerRow): array
{
$mapped = [];
foreach ($headerRow as $col => $value) {
$value = trim((string) $value);
if (isset(self::HEADER_MAP[$value])) {
$mapped[$col] = self::HEADER_MAP[$value];
}
}
return $mapped;
}
private function mapRow(array $row, array $headers): ?array
{
$mapped = [];
$hasData = false;
foreach ($headers as $col => $field) {
$value = isset($row[$col]) ? trim((string) $row[$col]) : '';
if ($field === 'status') {
$value = self::STATUS_MAP[mb_strtolower($value)] ?? 'active';
}
if ($field === 'purchase_date' || $field === 'install_date') {
$value = $this->parseDate($value);
}
if ($field === 'purchase_price') {
$value = $value !== '' ? (float) preg_replace('/[^0-9.]/', '', $value) : null;
}
if ($field === 'useful_life') {
$value = $value !== '' ? (int) preg_replace('/[^0-9]/', '', $value) : null;
}
if ($value !== '' && $value !== null) {
$hasData = true;
}
$mapped[$field] = $value;
}
return $hasData ? $mapped : null;
}
private function parseDate(string $value): ?string
{
if (empty($value)) {
return null;
}
// 엑셀 날짜 시리얼 번호
if (is_numeric($value)) {
try {
$date = \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject((int) $value);
return $date->format('Y-m-d');
} catch (\Exception) {
return null;
}
}
// 일반 날짜 문자열
foreach (['Y-m-d', 'Y/m/d', 'Y.m.d', 'Ymd'] as $format) {
$date = \DateTime::createFromFormat($format, $value);
if ($date) {
return $date->format('Y-m-d');
}
}
return null;
}
private function createEquipment(array $data, int $tenantId): Equipment
{
$managerName = $data['manager_name'] ?? null;
unset($data['manager_name']);
$data['tenant_id'] = $tenantId;
$data['created_by'] = auth()->id();
if (! isset($data['status'])) {
$data['status'] = 'active';
}
if ($managerName) {
$user = \App\Models\User::where('name', $managerName)->first();
if ($user) {
$data['manager_id'] = $user->id;
}
}
return Equipment::create($data);
}
private function updateEquipment(Equipment $equipment, array $data): void
{
$managerName = $data['manager_name'] ?? null;
unset($data['manager_name']);
$data['updated_by'] = auth()->id();
if ($managerName) {
$user = \App\Models\User::where('name', $managerName)->first();
if ($user) {
$data['manager_id'] = $user->id;
}
}
$equipment->update($data);
}
}