giftcon_dev/app/Services/Admin/Product/AdminMediaService.php
sungro815 b0545ab5b9 관리자 상품관리 완료
웹사이트 상품리스트 상세보기 작업중
2026-02-20 18:11:03 +09:00

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' => '삭제 처리 중 오류가 발생했습니다.'];
}
}
}