Files
Gnuboard7/modules/_bundled/sirsoft-board/src/Models/Board.php
T
HeuJung 6505d85959 feat(board,ecommerce,template): 관리자↔유저 크로스 이동 링크 + 상품코드·게시판 슬러그 라우팅 전환
관리자·유저 화면 왕복 이동 수단을 추가하고, 상품/게시판 URL 식별자를
사람이 읽을 수 있는 값(상품코드/슬러그)으로 전환한다.

크로스 이동 링크 (전부 새 탭):
- 관리자 사이드바 "사이트 보기" + 모바일 드로어 상단 잘림 수정
- 관리자 게시물 조회 ↔ 유저 게시판, 유저 상품 상세 ↔ 관리자 상품 수정
- 유저 화면의 관리자 진입은 is_admin 이 아니라 세부 권한 플래그로 게이트
 (게시판 can_access_admin, 상품 can_update)

상품 라우팅 id → product_code:
- Product::resolveRouteBinding(code/id) + 공개 라우트 정규식 확장
- 모든 상품 링크(목록·최근본·검색·장바구니·주문내역·문의내역·관리자) 전환
- 상품 검색을 상품코드·번호 양쪽 조회 가능하게 확장(관리자+통합검색)

게시판 라우팅 board id → slug:
- Board::resolveRouteBinding(slug/id) + 관리자 라우트 slug 전환
- 생성 후 리다이렉트·토글·삭제·수정·add-to-menu·편집기 스코프 전수 정정
- BoardController::addToAdminMenu 를 Board 바인딩으로 정합

검증: 프론트 레이아웃 Vitest 36건 green, 백엔드 회귀 테스트 추가,
g7.dev 라이브 실측(생성/삭제/메뉴추가 slug, 문의내역·검색 상품코드 URL).
2026-07-07 10:40:22 +09:00

354 lines
11 KiB
PHP

<?php
namespace Modules\Sirsoft\Board\Models;
use App\Casts\AsUnicodeJson;
use App\Models\User;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Modules\Sirsoft\Board\Database\Factories\BoardFactory;
use Modules\Sirsoft\Board\Enums\BoardOrderBy;
use Modules\Sirsoft\Board\Enums\OrderDirection;
use Modules\Sirsoft\Board\Enums\SecretMode;
class Board extends Model
{
use HasFactory;
/** @var array<string, array> 활동 로그 추적 필드 */
public static array $activityLogFields = [
'is_active' => ['label_key' => 'sirsoft-board::activity_log.fields.is_active', 'type' => 'boolean'],
'per_page' => ['label_key' => 'sirsoft-board::activity_log.fields.per_page', 'type' => 'number'],
'per_page_mobile' => ['label_key' => 'sirsoft-board::activity_log.fields.per_page_mobile', 'type' => 'number'],
'order_by' => ['label_key' => 'sirsoft-board::activity_log.fields.order_by', 'type' => 'enum', 'enum' => \Modules\Sirsoft\Board\Enums\BoardOrderBy::class],
'order_direction' => ['label_key' => 'sirsoft-board::activity_log.fields.order_direction', 'type' => 'enum', 'enum' => \Modules\Sirsoft\Board\Enums\OrderDirection::class],
'type' => ['label_key' => 'sirsoft-board::activity_log.fields.type', 'type' => 'text'],
'show_view_count' => ['label_key' => 'sirsoft-board::activity_log.fields.show_view_count', 'type' => 'boolean'],
'secret_mode' => ['label_key' => 'sirsoft-board::activity_log.fields.secret_mode', 'type' => 'enum', 'enum' => \Modules\Sirsoft\Board\Enums\SecretMode::class],
'use_comment' => ['label_key' => 'sirsoft-board::activity_log.fields.use_comment', 'type' => 'boolean'],
'use_reply' => ['label_key' => 'sirsoft-board::activity_log.fields.use_reply', 'type' => 'boolean'],
'max_reply_depth' => ['label_key' => 'sirsoft-board::activity_log.fields.max_reply_depth', 'type' => 'number'],
'use_report' => ['label_key' => 'sirsoft-board::activity_log.fields.use_report', 'type' => 'boolean'],
'use_file_upload' => ['label_key' => 'sirsoft-board::activity_log.fields.use_file_upload', 'type' => 'boolean'],
'max_file_size' => ['label_key' => 'sirsoft-board::activity_log.fields.max_file_size', 'type' => 'number'],
'max_file_count' => ['label_key' => 'sirsoft-board::activity_log.fields.max_file_count', 'type' => 'number'],
];
/**
* 라우트 모델 바인딩 시 슬러그(slug) 또는 숫자 ID로 게시판을 조회합니다.
*
* 관리자 게시판 URL 은 slug 기준(/admin/boards/:slug/edit)이며,
* 숫자 ID 도 하위호환으로 허용합니다.
*
* @param mixed $value 라우트 파라미터 값 (slug 또는 ID)
* @param string|null $field 검색할 필드명
* @return Model|null
*/
public function resolveRouteBinding($value, $field = null)
{
if ($field) {
return $this->where($field, $value)->firstOrFail();
}
if (is_numeric($value)) {
return $this->where('id', $value)->firstOrFail();
}
return $this->where('slug', $value)->firstOrFail();
}
/**
* 팩토리 생성
*/
protected static function newFactory(): BoardFactory
{
return BoardFactory::new();
}
/**
* 테이블명
*/
protected $table = 'boards';
/**
* 기본키
*/
protected $primaryKey = 'id';
/**
* 타임스탬프 사용 여부
*/
public $timestamps = true;
/**
* 모델 부트
*/
protected static function boot(): void
{
parent::boot();
// slug 필드 변경 방지 (생성 후 수정 불가)
static::updating(function ($board) {
if ($board->isDirty('slug')) {
$board->slug = $board->getOriginal('slug');
}
});
}
/**
* 대량 할당 가능한 속성
*/
protected $fillable = [
// 기본 정보
'name',
'slug',
'is_active',
'description',
// 게시글 설정
'per_page',
'per_page_mobile',
'order_by',
'order_direction',
// 타입 설정
'type',
// 기능 설정
'categories',
'show_view_count',
'secret_mode',
'use_comment',
'use_reply',
'max_reply_depth',
'use_report',
'new_display_hours',
// 입력 제한 설정
'min_title_length',
'max_title_length',
'min_content_length',
'max_content_length',
'min_comment_length',
'max_comment_length',
// 파일 업로드 설정
'use_file_upload',
'max_file_size',
'max_file_count',
'allowed_extensions',
// 댓글 설정
'comment_order',
'max_comment_depth',
// 알림 설정
'notify_author',
'notify_admin_on_post',
// 기타 설정
'blocked_keywords',
// 카운트 컬럼
'posts_count',
'comments_count',
// 생성자/수정자
'created_by',
'updated_by',
];
/**
* 속성 캐스팅
*/
protected function casts(): array
{
return [
'name' => AsUnicodeJson::class,
'description' => AsUnicodeJson::class,
'categories' => 'array',
'allowed_extensions' => 'array',
'blocked_keywords' => 'array',
'is_active' => 'boolean',
'show_view_count' => 'boolean',
'use_comment' => 'boolean',
'use_reply' => 'boolean',
'max_reply_depth' => 'integer',
'use_report' => 'boolean',
'use_file_upload' => 'boolean',
'notify_author' => 'boolean',
'notify_admin_on_post' => 'boolean',
'secret_mode' => SecretMode::class,
'order_by' => BoardOrderBy::class,
'order_direction' => OrderDirection::class,
'max_comment_depth' => 'integer',
'comment_order' => OrderDirection::class,
'posts_count' => 'integer',
'comments_count' => 'integer',
];
}
/**
* 생성자와의 관계를 정의합니다.
*/
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
/**
* 수정자와의 관계를 정의합니다.
*/
public function updater(): BelongsTo
{
return $this->belongsTo(User::class, 'updated_by');
}
/**
* 게시글 목록과의 관계를 정의합니다.
*/
public function posts(): HasMany
{
return $this->hasMany(Post::class, 'board_id');
}
/**
* 댓글 목록과의 관계를 정의합니다.
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class, 'board_id');
}
/**
* 첨부파일 목록과의 관계를 정의합니다.
*/
public function attachments(): HasMany
{
return $this->hasMany(Attachment::class, 'board_id');
}
/**
* 신고 목록과의 관계를 정의합니다.
*/
public function reports(): HasMany
{
return $this->hasMany(Report::class, 'board_id');
}
/**
* 지정된 로케일의 게시판명 반환
*/
public function getLocalizedName(?string $locale = null): string
{
$locale = $locale ?? app()->getLocale();
if (! is_array($this->name)) {
return (string) $this->name;
}
return $this->name[$locale]
?? $this->name[config('app.fallback_locale')]
?? (! empty($this->name) ? array_values($this->name)[0] : '')
?? '';
}
/**
* 현재 로케일의 게시판명 반환
*/
protected function localizedName(): Attribute
{
return Attribute::make(
get: fn () => $this->getLocalizedName()
);
}
/**
* 지정된 로케일의 게시판 설명 반환
*
* @param string|null $locale 로케일 (null이면 현재 로케일)
* @return string 지정된 로케일의 게시판 설명
*/
public function getLocalizedDescription(?string $locale = null): string
{
$locale = $locale ?? app()->getLocale();
if (! is_array($this->description)) {
return (string) $this->description;
}
return $this->description[$locale]
?? $this->description[config('app.fallback_locale')]
?? (! empty($this->description) ? array_values($this->description)[0] : '')
?? '';
}
/**
* 현재 로케일의 게시판 설명 반환
*/
protected function localizedDescription(): Attribute
{
return Attribute::make(
get: fn () => $this->getLocalizedDescription()
);
}
/**
* 게시판에서 분류(카테고리)를 사용하는지 확인합니다.
*
* @return bool 분류 사용 여부
*/
public function hasCategories(): bool
{
return is_array($this->categories) && ! empty($this->categories);
}
/**
* 게시판의 권한 정보를 반환합니다.
*
* g7_permissions 및 role_permissions 테이블에서 권한 정보를 조회하여
* 각 권한별로 할당된 역할 identifier 배열을 반환합니다.
*
* @return array 권한 정보 (키: permission_key, 값: [role_identifiers] or null)
*/
protected function permissions(): Attribute
{
return Attribute::make(
get: function () {
$permissionDefinitions = config('sirsoft-board.board_permission_definitions', []);
$permissions = [];
foreach (array_keys($permissionDefinitions) as $key) {
$identifier = "sirsoft-board.{$this->slug}.{$key}";
// 권한 조회
$permission = \App\Models\Permission::where('identifier', $identifier)->first();
if (! $permission) {
// 권한이 없으면 null (전체 허용)
$permissions[$key] = null;
continue;
}
// 할당된 역할들의 identifier 조회
$roleIdentifiers = $permission->roles()->pluck('identifier')->toArray();
if (empty($roleIdentifiers)) {
// 역할이 없으면 null (전체 허용)
$permissions[$key] = null;
} else {
// 역할 identifier 배열
$permissions[$key] = $roleIdentifiers;
}
}
return $permissions;
}
);
}
}