190 lines
7.3 KiB
PHP
190 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Admin\Product;
|
|
|
|
use App\Repositories\Admin\Product\AdminMediaRepository;
|
|
use App\Services\Admin\AdminAuditService;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
// 이미지 처리 라이브러리 (Intervention Image v3)
|
|
use Intervention\Image\ImageManager;
|
|
use Intervention\Image\Drivers\Gd\Driver;
|
|
|
|
final class AdminMediaService
|
|
{
|
|
private ImageManager $manager;
|
|
|
|
public function __construct(
|
|
private readonly AdminMediaRepository $repo,
|
|
private readonly AdminAuditService $audit,
|
|
) {
|
|
// GD 드라이버 사용 (대부분의 호스팅/서버 환경에서 기본 지원)
|
|
$this->manager = new ImageManager(new Driver());
|
|
}
|
|
|
|
/**
|
|
* 미디어 목록 조회 (필터 및 페이징)
|
|
*/
|
|
public function list(array $filters): array
|
|
{
|
|
return [
|
|
'page' => $this->repo->paginateMedia($filters),
|
|
'folders' => $this->repo->getDistinctFolders(),
|
|
'currentFolder' => $filters['folder'] ?? '',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 이미지 업로드 및 WebP 변환 저장
|
|
* * @param array $input 폴더명 등 입력값
|
|
* @param UploadedFile $file 업로드된 파일 객체
|
|
* @param string|null $customName 관리자가 지정한 이미지 이름 (없으면 원본명 사용)
|
|
* @param int $fileIndex 다중 업로드 시 순번 (이름 뒤에 _1, _2 붙이기 위함)
|
|
*/
|
|
public function uploadImage(array $input, UploadedFile $file, ?string $customName, int $fileIndex, int $actorAdminId, string $ip, string $ua): array
|
|
{
|
|
try {
|
|
// 1. 폴더명 보안 처리 (영문, 숫자, 언더바, 하이픈만 허용)
|
|
$folderName = trim($input['folder_name'] ?? 'default');
|
|
$folderName = preg_replace('/[^a-zA-Z0-9_\-]/', '', $folderName);
|
|
if (empty($folderName)) $folderName = 'default';
|
|
|
|
$originalName = $file->getClientOriginalName();
|
|
|
|
// 2. 이미지 처리 (WebP 변환)
|
|
// Intervention Image를 통해 파일을 읽고 WebP 포맷(퀄리티 80)으로 인코딩
|
|
$image = $this->manager->read($file);
|
|
$encoded = $image->toWebp(quality: 80);
|
|
|
|
// 3. 저장할 파일명 생성 (랜덤 해시 + .webp)
|
|
$hashName = Str::random(40) . '.webp';
|
|
$savePath = "product/{$folderName}/{$hashName}";
|
|
|
|
// 4. 스토리지 저장 (public 디스크)
|
|
// put() 메소드를 사용하여 변환된 바이너리 데이터를 저장
|
|
$isSaved = Storage::disk('public')->put($savePath, (string) $encoded);
|
|
|
|
if (!$isSaved) {
|
|
return ['ok' => false, 'message' => '스토리지 파일 저장에 실패했습니다.'];
|
|
}
|
|
|
|
// 5. 관리용 이름(Name) 결정
|
|
if (!empty($customName)) {
|
|
// 다중 파일인 경우 뒤에 순번을 붙임 (예: 문화상품권_1)
|
|
$finalName = $fileIndex > 0 ? $customName . '_' . $fileIndex : $customName;
|
|
} else {
|
|
// 이름 지정 안 함 -> 원본 파일명에서 확장자 제거하고 사용
|
|
$finalName = pathinfo($originalName, PATHINFO_FILENAME);
|
|
}
|
|
|
|
// 6. DB 저장 데이터 준비
|
|
$data = [
|
|
'folder_name' => $folderName,
|
|
'name' => $finalName,
|
|
'original_name' => $originalName,
|
|
'file_name' => $hashName,
|
|
'file_path' => '/storage/' . $savePath, // 웹 접근 경로
|
|
'file_size' => strlen((string) $encoded), // 변환된 파일 크기
|
|
'file_ext' => 'webp', // 확장자는 무조건 webp
|
|
];
|
|
|
|
// 7. 트랜잭션 DB 저장 및 로그
|
|
DB::transaction(function () use ($data, $actorAdminId, $ip, $ua) {
|
|
$newId = $this->repo->insertMedia($data);
|
|
|
|
$this->audit->log(
|
|
actorAdminId: $actorAdminId,
|
|
action: 'admin.media.upload',
|
|
targetType: 'media_library',
|
|
targetId: $newId,
|
|
before: null,
|
|
after: $data,
|
|
ip: $ip,
|
|
ua: $ua
|
|
);
|
|
});
|
|
|
|
return ['ok' => true, 'message' => '업로드 성공'];
|
|
|
|
} catch (\Throwable $e) {
|
|
// 이미지 변환 실패(엑셀 파일 등) 혹은 기타 오류 처리
|
|
return ['ok' => false, 'message' => '이미지 처리 중 오류 발생: ' . $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 이미지 이름(관리용) 수정
|
|
*/
|
|
public function updateMediaName(int $id, string $newName, int $actorAdminId, string $ip, string $ua): array
|
|
{
|
|
$before = $this->repo->findMedia($id);
|
|
if (!$before) return ['ok' => false, 'message' => '이미지를 찾을 수 없습니다.'];
|
|
|
|
try {
|
|
DB::transaction(function () use ($id, $newName, $before, $actorAdminId, $ip, $ua) {
|
|
$this->repo->updateMediaName($id, trim($newName));
|
|
|
|
$after = (array)$before;
|
|
$after['name'] = trim($newName);
|
|
|
|
$this->audit->log(
|
|
actorAdminId: $actorAdminId,
|
|
action: 'admin.media.rename',
|
|
targetType: 'media_library',
|
|
targetId: $id,
|
|
before: (array)$before,
|
|
after: $after,
|
|
ip: $ip,
|
|
ua: $ua
|
|
);
|
|
});
|
|
|
|
return ['ok' => true, 'message' => '이름이 변경되었습니다.'];
|
|
} catch (\Throwable $e) {
|
|
return ['ok' => false, 'message' => '이름 변경 중 오류가 발생했습니다.'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 이미지 삭제 (DB + 물리 파일)
|
|
*/
|
|
public function deleteImage(int $id, int $actorAdminId, string $ip, string $ua): array
|
|
{
|
|
$media = $this->repo->findMedia($id);
|
|
if (!$media) return ['ok' => false, 'message' => '이미지를 찾을 수 없습니다.'];
|
|
|
|
try {
|
|
// 1. DB 삭제 및 로그 (트랜잭션)
|
|
DB::transaction(function () use ($id, $media, $actorAdminId, $ip, $ua) {
|
|
$this->repo->deleteMedia($id);
|
|
|
|
$this->audit->log(
|
|
actorAdminId: $actorAdminId,
|
|
action: 'admin.media.delete',
|
|
targetType: 'media_library',
|
|
targetId: $id,
|
|
before: (array)$media,
|
|
after: null,
|
|
ip: $ip,
|
|
ua: $ua
|
|
);
|
|
});
|
|
|
|
// 2. 물리 파일 삭제 (DB 삭제 성공 시 실행)
|
|
// /storage/product/... -> product/... 로 변환하여 삭제
|
|
$storagePath = str_replace('/storage/', '', $media->file_path);
|
|
|
|
if (Storage::disk('public')->exists($storagePath)) {
|
|
Storage::disk('public')->delete($storagePath);
|
|
}
|
|
|
|
return ['ok' => true, 'message' => '이미지가 삭제되었습니다.'];
|
|
|
|
} catch (\Throwable $e) {
|
|
return ['ok' => false, 'message' => '삭제 처리 중 오류가 발생했습니다.'];
|
|
}
|
|
}
|
|
}
|