StorageInterface::url 의 public 하드코딩을 완화하고 core.storage.filter_url 훅을 신설해, 완전 공개 자산(상품·카테고리·리뷰·에디터 이미지)의 직접 URL 서빙을 옵트인으로 열었다. 미설정 시 기존 스트리밍 100% 보존. 디스크 결정은 확장 개별 설정 > 코어 전역 > 미설정 순이며, 서빙·삭제·이동은 행에 기록된 disk 를 따라 전환 전후 자산이 혼재해도 각자 실제 저장 위치를 향한다. 고아 disk(공급 플러그인 비활성화)는 스트리밍으로 폴백해 500 이 되지 않는다. 공개 이슈 (@lyg-kaban 건의) 대응.
152 lines
3.9 KiB
PHP
152 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace Modules\Sirsoft\Ecommerce\Models;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Modules\Sirsoft\Ecommerce\Enums\ProductImageCollection;
|
|
use Modules\Sirsoft\Ecommerce\Models\Concerns\HasDirectAssetUrl;
|
|
|
|
/**
|
|
* 상품 이미지 모델
|
|
*/
|
|
class ProductImage extends Model
|
|
{
|
|
use HasDirectAssetUrl;
|
|
use SoftDeletes;
|
|
|
|
protected $table = 'ecommerce_product_images';
|
|
|
|
/**
|
|
* 모델 부트 메서드
|
|
*/
|
|
protected static function boot(): void
|
|
{
|
|
parent::boot();
|
|
|
|
// 생성 시 hash 자동 생성 (URL용 고유 키)
|
|
static::creating(function (self $model) {
|
|
if (empty($model->hash)) {
|
|
$model->hash = self::generateHash();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 고유 해시를 생성합니다.
|
|
*
|
|
* @return string 12자리 고유 해시
|
|
*/
|
|
public static function generateHash(): string
|
|
{
|
|
do {
|
|
$hash = substr(bin2hex(random_bytes(6)), 0, 12);
|
|
} while (self::where('hash', $hash)->exists());
|
|
|
|
return $hash;
|
|
}
|
|
|
|
protected $fillable = [
|
|
'product_id',
|
|
'temp_key',
|
|
'hash',
|
|
'original_filename',
|
|
'stored_filename',
|
|
'disk',
|
|
'path',
|
|
'mime_type',
|
|
'file_size',
|
|
'width',
|
|
'height',
|
|
'alt_text',
|
|
'collection',
|
|
'is_thumbnail',
|
|
'sort_order',
|
|
'created_by',
|
|
];
|
|
|
|
protected $casts = [
|
|
'alt_text' => 'array',
|
|
'collection' => ProductImageCollection::class,
|
|
'is_thumbnail' => 'boolean',
|
|
'sort_order' => 'integer',
|
|
'width' => 'integer',
|
|
'height' => 'integer',
|
|
'file_size' => 'integer',
|
|
];
|
|
|
|
/**
|
|
* 이 이미지가 속한 상품 관계 (product_id FK).
|
|
*
|
|
* @return BelongsTo 상품 관계
|
|
*/
|
|
public function product(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Product::class, 'product_id');
|
|
}
|
|
|
|
/**
|
|
* 이 이미지를 업로드한 사용자 관계 (created_by FK).
|
|
*
|
|
* @return BelongsTo 업로더 사용자 관계
|
|
*/
|
|
public function creator(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'created_by');
|
|
}
|
|
|
|
/**
|
|
* 현재 로케일의 대체 텍스트(alt)를 반환합니다 (다국어 fallback chain 적용).
|
|
*
|
|
* @param string|null $locale 반환할 로케일. null 이면 현재 앱 로케일 사용
|
|
* @return string|null 로케일별 alt 텍스트, alt_text 가 비어있으면 null
|
|
*/
|
|
public function getLocalizedAltText(?string $locale = null): ?string
|
|
{
|
|
if (empty($this->alt_text)) {
|
|
return null;
|
|
}
|
|
|
|
$locale = $locale ?? app()->getLocale();
|
|
$altText = $this->alt_text;
|
|
|
|
return $altText[$locale] ?? $altText[config('app.fallback_locale', 'ko')] ?? null;
|
|
}
|
|
|
|
/**
|
|
* 특정 컬렉션(main/gallery/detail 등) 의 이미지만 필터링하는 쿼리 스코프.
|
|
*
|
|
* @param Builder $query
|
|
* @param ProductImageCollection $collection 필터링할 이미지 컬렉션 enum
|
|
* @return Builder
|
|
*/
|
|
public function scopeByCollection($query, ProductImageCollection $collection)
|
|
{
|
|
return $query->where('collection', $collection);
|
|
}
|
|
|
|
/**
|
|
* 대표 이미지만 조회
|
|
*
|
|
* @param Builder $query
|
|
* @return Builder
|
|
*/
|
|
public function scopeThumbnails($query)
|
|
{
|
|
return $query->where('is_thumbnail', true);
|
|
}
|
|
|
|
/**
|
|
* hash 기반 상품 이미지 서빙 API URL (직접 URL 불가 시 폴백).
|
|
*
|
|
* @return string `/api/modules/sirsoft-ecommerce/product-image/{hash}` 형식 URL
|
|
*/
|
|
protected function apiDownloadUrl(): string
|
|
{
|
|
return '/api/modules/sirsoft-ecommerce/product-image/'.$this->hash;
|
|
}
|
|
}
|