Files
sam-manage/app/Models/Boards/File.php
kent da159cc46e feat(boards): 게시글 파일 시스템 개선 및 이미지 미리보기 추가
- 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>
2025-12-27 17:08:53 +09:00

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);
}
}