- Morph map에 Post, Department 모델 등록 (ClassMorphViolationException 해결) - 파일 저장 방식을 API 스타일로 변경 (document_id + document_type) - 파일 미리보기 라우트 및 메서드 추가 (previewFile) - 게시글 상세 페이지에서 이미지 첨부파일을 본문 상단에 풀 너비로 표시 - 비이미지 첨부파일은 하단에 다운로드 목록으로 분리 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
236 lines
5.5 KiB
PHP
236 lines
5.5 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',
|
|
// New fields (API 방식)
|
|
'document_id',
|
|
'document_type',
|
|
// Legacy fields (하위 호환)
|
|
'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);
|
|
}
|
|
|
|
/**
|
|
* 특정 문서의 파일들
|
|
*/
|
|
public function scopeForDocument($query, int $documentId, string $documentType)
|
|
{
|
|
return $query->where('document_id', $documentId)
|
|
->where('document_type', $documentType);
|
|
}
|
|
}
|