- File 모델 추가 (Polymorphic 관계) - Post 모델에 files() MorphMany 관계 추가 - PostService 파일 업로드/삭제/다운로드 메서드 추가 - PostController 파일 관련 액션 추가 - 게시글 작성/수정 폼에 드래그앤드롭 파일 업로드 UI - 게시글 상세에 첨부파일 목록 표시 - tenant 디스크 설정 (공유 스토리지) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
223 lines
5.1 KiB
PHP
223 lines
5.1 KiB
PHP
<?php
|
|
|
|
namespace App\Models\Boards;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
/**
|
|
* 파일 모델 (Polymorphic)
|
|
*
|
|
* @property int $id
|
|
* @property int|null $tenant_id
|
|
* @property int|null $folder_id
|
|
* @property bool $is_temp
|
|
* @property string $file_path
|
|
* @property string|null $display_name
|
|
* @property string|null $stored_name
|
|
* @property string|null $original_name
|
|
* @property int $file_size
|
|
* @property string|null $mime_type
|
|
* @property string|null $file_type
|
|
* @property int|null $fileable_id
|
|
* @property string|null $fileable_type
|
|
* @property int|null $uploaded_by
|
|
*/
|
|
class File extends Model
|
|
{
|
|
use SoftDeletes;
|
|
|
|
protected $table = 'files';
|
|
|
|
protected $fillable = [
|
|
'tenant_id',
|
|
'folder_id',
|
|
'is_temp',
|
|
'file_path',
|
|
'display_name',
|
|
'stored_name',
|
|
'original_name',
|
|
'file_name',
|
|
'file_size',
|
|
'mime_type',
|
|
'file_type',
|
|
'fileable_id',
|
|
'fileable_type',
|
|
'description',
|
|
'uploaded_by',
|
|
'deleted_by',
|
|
'created_by',
|
|
'updated_by',
|
|
];
|
|
|
|
protected $casts = [
|
|
'is_temp' => 'boolean',
|
|
'file_size' => 'integer',
|
|
];
|
|
|
|
// =========================================================================
|
|
// Relationships
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Polymorphic 관계 - 연결된 모델 (Post, Product 등)
|
|
*/
|
|
public function fileable(): MorphTo
|
|
{
|
|
return $this->morphTo();
|
|
}
|
|
|
|
/**
|
|
* 업로더
|
|
*/
|
|
public function uploader(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'uploaded_by');
|
|
}
|
|
|
|
/**
|
|
* 생성자
|
|
*/
|
|
public function creator(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'created_by');
|
|
}
|
|
|
|
// =========================================================================
|
|
// Helper Methods
|
|
// =========================================================================
|
|
|
|
/**
|
|
* 스토리지 전체 경로
|
|
*/
|
|
public function getStoragePath(): string
|
|
{
|
|
return Storage::disk('tenant')->path($this->file_path);
|
|
}
|
|
|
|
/**
|
|
* 파일 존재 여부
|
|
*/
|
|
public function existsInStorage(): bool
|
|
{
|
|
return Storage::disk('tenant')->exists($this->file_path);
|
|
}
|
|
|
|
/**
|
|
* 다운로드 응답
|
|
*/
|
|
public function download()
|
|
{
|
|
if (! $this->existsInStorage()) {
|
|
abort(404, '파일을 찾을 수 없습니다.');
|
|
}
|
|
|
|
$fileName = $this->display_name ?? $this->original_name ?? $this->file_name;
|
|
|
|
return response()->download($this->getStoragePath(), $fileName);
|
|
}
|
|
|
|
/**
|
|
* 파일 URL (public 접근용)
|
|
*/
|
|
public function getUrl(): ?string
|
|
{
|
|
if (! $this->existsInStorage()) {
|
|
return null;
|
|
}
|
|
|
|
return Storage::disk('tenant')->url($this->file_path);
|
|
}
|
|
|
|
/**
|
|
* 파일 크기 포맷팅
|
|
*/
|
|
public function getFormattedSize(): string
|
|
{
|
|
$bytes = $this->file_size;
|
|
|
|
if ($bytes >= 1073741824) {
|
|
return number_format($bytes / 1073741824, 2).' GB';
|
|
} elseif ($bytes >= 1048576) {
|
|
return number_format($bytes / 1048576, 2).' MB';
|
|
} elseif ($bytes >= 1024) {
|
|
return number_format($bytes / 1024, 2).' KB';
|
|
}
|
|
|
|
return $bytes.' bytes';
|
|
}
|
|
|
|
/**
|
|
* 파일 확장자
|
|
*/
|
|
public function getExtension(): string
|
|
{
|
|
$fileName = $this->stored_name ?? $this->file_name ?? $this->original_name;
|
|
|
|
return pathinfo($fileName, PATHINFO_EXTENSION);
|
|
}
|
|
|
|
/**
|
|
* 이미지 여부
|
|
*/
|
|
public function isImage(): bool
|
|
{
|
|
return str_starts_with($this->mime_type ?? '', 'image/');
|
|
}
|
|
|
|
/**
|
|
* Soft Delete with user tracking
|
|
*/
|
|
public function softDeleteFile(int $userId): void
|
|
{
|
|
$this->deleted_by = $userId;
|
|
$this->save();
|
|
$this->delete();
|
|
}
|
|
|
|
/**
|
|
* 완전 삭제 (물리 파일 포함)
|
|
*/
|
|
public function permanentDelete(): void
|
|
{
|
|
if ($this->existsInStorage()) {
|
|
Storage::disk('tenant')->delete($this->file_path);
|
|
}
|
|
|
|
$this->forceDelete();
|
|
}
|
|
|
|
// =========================================================================
|
|
// Scopes
|
|
// =========================================================================
|
|
|
|
/**
|
|
* 특정 모델의 파일들
|
|
*/
|
|
public function scopeForModel($query, string $type, int $id)
|
|
{
|
|
return $query->where('fileable_type', $type)
|
|
->where('fileable_id', $id);
|
|
}
|
|
|
|
/**
|
|
* 임시 파일만
|
|
*/
|
|
public function scopeTemp($query)
|
|
{
|
|
return $query->where('is_temp', true);
|
|
}
|
|
|
|
/**
|
|
* 임시 파일 제외
|
|
*/
|
|
public function scopeNonTemp($query)
|
|
{
|
|
return $query->where('is_temp', false);
|
|
}
|
|
}
|