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