- 댓글 라우트 추가 (store, update, destroy) - PostService에 댓글 관리 메서드 추가 - PostController에 댓글 컨트롤러 메서드 추가 - 게시글 상세 페이지에 댓글 섹션 UI 추가 (AlpineJS) - 계층형 댓글 지원 (부모/대댓글) - BoardComment 모델 추가 - HTMLPurifier 패키지 및 설정 추가 - 게시글 목록에 첨부파일/댓글 수 표시 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
255 lines
6.3 KiB
PHP
255 lines
6.3 KiB
PHP
<?php
|
|
|
|
namespace App\Models\Boards;
|
|
|
|
use App\Models\Tenants\Tenant;
|
|
use App\Models\User;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
|
|
/**
|
|
* 게시글 모델
|
|
*
|
|
* @property int $id
|
|
* @property int|null $tenant_id
|
|
* @property int $board_id
|
|
* @property int $user_id
|
|
* @property string $title
|
|
* @property string|null $content
|
|
* @property string $editor_type
|
|
* @property string|null $ip_address
|
|
* @property bool $is_notice
|
|
* @property bool $is_secret
|
|
* @property int $views
|
|
* @property string $status
|
|
*/
|
|
class Post extends Model
|
|
{
|
|
use SoftDeletes;
|
|
|
|
protected $table = 'posts';
|
|
|
|
protected $fillable = [
|
|
'tenant_id',
|
|
'board_id',
|
|
'user_id',
|
|
'title',
|
|
'content',
|
|
'editor_type',
|
|
'ip_address',
|
|
'is_notice',
|
|
'is_secret',
|
|
'views',
|
|
'status',
|
|
'created_by',
|
|
'updated_by',
|
|
'deleted_by',
|
|
];
|
|
|
|
protected $casts = [
|
|
'is_notice' => 'boolean',
|
|
'is_secret' => 'boolean',
|
|
'views' => 'integer',
|
|
];
|
|
|
|
protected $attributes = [
|
|
'is_notice' => false,
|
|
'is_secret' => false,
|
|
'views' => 0,
|
|
'status' => 'published',
|
|
'editor_type' => 'wysiwyg',
|
|
];
|
|
|
|
// =========================================================================
|
|
// Scopes
|
|
// =========================================================================
|
|
|
|
/**
|
|
* 특정 게시판의 글
|
|
*/
|
|
public function scopeOfBoard(Builder $query, int $boardId): Builder
|
|
{
|
|
return $query->where('board_id', $boardId);
|
|
}
|
|
|
|
/**
|
|
* 공지사항만
|
|
*/
|
|
public function scopeNotices(Builder $query): Builder
|
|
{
|
|
return $query->where('is_notice', true);
|
|
}
|
|
|
|
/**
|
|
* 일반 글만 (공지 제외)
|
|
*/
|
|
public function scopeRegular(Builder $query): Builder
|
|
{
|
|
return $query->where('is_notice', false);
|
|
}
|
|
|
|
/**
|
|
* 게시된 글만
|
|
*/
|
|
public function scopePublished(Builder $query): Builder
|
|
{
|
|
return $query->where('status', 'published');
|
|
}
|
|
|
|
/**
|
|
* 비밀글이 아닌 것만
|
|
*/
|
|
public function scopePublic(Builder $query): Builder
|
|
{
|
|
return $query->where('is_secret', false);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Relationships
|
|
// =========================================================================
|
|
|
|
public function board(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Board::class, 'board_id');
|
|
}
|
|
|
|
public function tenant(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Tenant::class, 'tenant_id');
|
|
}
|
|
|
|
public function author(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'user_id');
|
|
}
|
|
|
|
public function creator(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'created_by');
|
|
}
|
|
|
|
public function customFieldValues(): HasMany
|
|
{
|
|
return $this->hasMany(PostCustomFieldValue::class, 'post_id');
|
|
}
|
|
|
|
/**
|
|
* 첨부파일 (document_id + document_type 기반)
|
|
*/
|
|
public function files(): HasMany
|
|
{
|
|
return $this->hasMany(File::class, 'document_id')
|
|
->where('document_type', 'post');
|
|
}
|
|
|
|
/**
|
|
* 댓글 (활성 상태만)
|
|
*/
|
|
public function comments(): HasMany
|
|
{
|
|
return $this->hasMany(BoardComment::class, 'post_id')
|
|
->where('status', 'active');
|
|
}
|
|
|
|
/**
|
|
* 모든 댓글 (상태 무관)
|
|
*/
|
|
public function allComments(): HasMany
|
|
{
|
|
return $this->hasMany(BoardComment::class, 'post_id');
|
|
}
|
|
|
|
// =========================================================================
|
|
// Helper Methods
|
|
// =========================================================================
|
|
|
|
/**
|
|
* 조회수 증가
|
|
*/
|
|
public function incrementViews(): void
|
|
{
|
|
$this->increment('views');
|
|
}
|
|
|
|
/**
|
|
* 커스텀 필드 값 가져오기
|
|
*/
|
|
public function getCustomFieldValue(string $fieldKey): ?string
|
|
{
|
|
$fieldValue = $this->customFieldValues()
|
|
->whereHas('field', fn ($q) => $q->where('field_key', $fieldKey))
|
|
->first();
|
|
|
|
return $fieldValue?->value;
|
|
}
|
|
|
|
/**
|
|
* 커스텀 필드 값들을 배열로 가져오기
|
|
*/
|
|
public function getCustomFieldsArray(): array
|
|
{
|
|
return $this->customFieldValues()
|
|
->with('field')
|
|
->get()
|
|
->mapWithKeys(fn ($v) => [$v->field->field_key => $v->value])
|
|
->toArray();
|
|
}
|
|
|
|
/**
|
|
* 비밀글 접근 가능 여부
|
|
*/
|
|
public function canAccess(?User $user): bool
|
|
{
|
|
// 비밀글이 아니면 모두 접근 가능
|
|
if (! $this->is_secret) {
|
|
return true;
|
|
}
|
|
|
|
// 비로그인이면 접근 불가
|
|
if (! $user) {
|
|
return false;
|
|
}
|
|
|
|
// 작성자 본인이면 접근 가능
|
|
if ($this->user_id === $user->id) {
|
|
return true;
|
|
}
|
|
|
|
// 관리자면 접근 가능
|
|
if ($user->hasRole(['admin', 'super-admin'])) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 안전한 HTML 콘텐츠 반환 (XSS 방지)
|
|
* - 허용된 태그만 남기고 나머지 제거
|
|
* - 위험한 속성 (onclick, onerror 등) 제거
|
|
*/
|
|
public function getSafeHtmlContent(): string
|
|
{
|
|
$content = $this->content ?? '';
|
|
|
|
// 허용할 HTML 태그
|
|
$allowedTags = '<p><br><strong><b><em><i><u><s><del><h1><h2><h3><h4><h5><h6>'
|
|
.'<ul><ol><li><blockquote><pre><code><a><img><span><div><table><thead><tbody><tr><th><td>';
|
|
|
|
// 허용된 태그만 남기기
|
|
$content = strip_tags($content, $allowedTags);
|
|
|
|
// 위험한 이벤트 핸들러 속성 제거 (onclick, onerror, onload 등)
|
|
$content = preg_replace('/\s*on\w+\s*=\s*["\'][^"\']*["\']/i', '', $content);
|
|
$content = preg_replace('/\s*on\w+\s*=\s*[^\s>]*/i', '', $content);
|
|
|
|
// javascript: 프로토콜 제거
|
|
$content = preg_replace('/href\s*=\s*["\']javascript:[^"\']*["\']/i', 'href="#"', $content);
|
|
|
|
return $content;
|
|
}
|
|
}
|