Files
sam-manage/app/Models/Boards/Post.php
kent 358c987cc1 feat(comment): 게시글 댓글 CRUD 기능 추가
- 댓글 라우트 추가 (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>
2025-12-29 12:58:06 +09:00

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