- Auditable 트레이트 신규 생성 (bootAuditable 패턴) - creating: created_by/updated_by 자동 채우기 - updating: updated_by 자동 채우기 - deleting: deleted_by 채우기 + saveQuietly() - created/updated/deleted: audit_logs 자동 기록 - 기존 AuditLogger 패턴과 동일한 try/catch 조용한 실패 - 변경된 필드만 before/after 기록 (updated 이벤트) - auditExclude 프로퍼티로 모델별 제외 필드 설정 가능 - 제외 대상: Attendance, StockTransaction, TodayIssue 등 고빈도/시스템 모델 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
250 lines
6.7 KiB
PHP
250 lines
6.7 KiB
PHP
<?php
|
|
|
|
namespace App\Models\ItemMaster;
|
|
|
|
use App\Traits\Auditable;
|
|
use App\Traits\BelongsToTenant;
|
|
use App\Traits\ModelTrait;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
|
|
/**
|
|
* EntityRelationship - 엔티티 간 관계를 관리하는 링크 테이블 모델
|
|
*
|
|
* 지원하는 관계 유형:
|
|
* - page-section: 페이지와 섹션 연결
|
|
* - page-field: 페이지와 필드 직접 연결
|
|
* - section-field: 섹션과 필드 연결
|
|
* - section-bom: 섹션과 BOM 항목 연결
|
|
*/
|
|
class EntityRelationship extends Model
|
|
{
|
|
use Auditable, BelongsToTenant, ModelTrait;
|
|
|
|
protected $fillable = [
|
|
'tenant_id',
|
|
'group_id',
|
|
'parent_type',
|
|
'parent_id',
|
|
'child_type',
|
|
'child_id',
|
|
'order_no',
|
|
'metadata',
|
|
'is_locked',
|
|
'locked_by',
|
|
'locked_at',
|
|
'created_by',
|
|
'updated_by',
|
|
];
|
|
|
|
protected $casts = [
|
|
'group_id' => 'integer',
|
|
'parent_id' => 'integer',
|
|
'child_id' => 'integer',
|
|
'order_no' => 'integer',
|
|
'metadata' => 'array',
|
|
'is_locked' => 'boolean',
|
|
'locked_at' => 'datetime',
|
|
'created_at' => 'datetime',
|
|
'updated_at' => 'datetime',
|
|
];
|
|
|
|
// 엔티티 타입 상수
|
|
public const TYPE_PAGE = 'page';
|
|
|
|
public const TYPE_SECTION = 'section';
|
|
|
|
public const TYPE_FIELD = 'field';
|
|
|
|
public const TYPE_BOM = 'bom';
|
|
|
|
// 그룹 ID 상수
|
|
public const GROUP_ITEM_MASTER = 1;
|
|
|
|
/**
|
|
* 부모 엔티티 조회 (Polymorphic)
|
|
*/
|
|
public function parent()
|
|
{
|
|
return $this->morphTo('parent', 'parent_type', 'parent_id');
|
|
}
|
|
|
|
/**
|
|
* 자식 엔티티 조회 (Polymorphic)
|
|
*/
|
|
public function child()
|
|
{
|
|
return $this->morphTo('child', 'child_type', 'child_id');
|
|
}
|
|
|
|
/**
|
|
* 부모 타입에 따른 모델 클래스 반환
|
|
*/
|
|
public static function getModelClass(string $type): ?string
|
|
{
|
|
return match ($type) {
|
|
self::TYPE_PAGE => ItemPage::class,
|
|
self::TYPE_SECTION => ItemSection::class,
|
|
self::TYPE_FIELD => ItemField::class,
|
|
self::TYPE_BOM => ItemBomItem::class,
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 특정 부모의 자식 관계 조회
|
|
*/
|
|
public static function getChildren(string $parentType, int $parentId, ?string $childType = null)
|
|
{
|
|
$query = self::where('parent_type', $parentType)
|
|
->where('parent_id', $parentId)
|
|
->orderBy('order_no');
|
|
|
|
if ($childType) {
|
|
$query->where('child_type', $childType);
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* 특정 자식의 부모 관계 조회
|
|
*/
|
|
public static function getParents(string $childType, int $childId, ?string $parentType = null)
|
|
{
|
|
$query = self::where('child_type', $childType)
|
|
->where('child_id', $childId);
|
|
|
|
if ($parentType) {
|
|
$query->where('parent_type', $parentType);
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* 관계 생성 또는 업데이트
|
|
*/
|
|
public static function link(
|
|
int $tenantId,
|
|
string $parentType,
|
|
int $parentId,
|
|
string $childType,
|
|
int $childId,
|
|
int $orderNo = 0,
|
|
?array $metadata = null,
|
|
int $groupId = self::GROUP_ITEM_MASTER
|
|
): self {
|
|
return self::updateOrCreate(
|
|
[
|
|
'tenant_id' => $tenantId,
|
|
'group_id' => $groupId,
|
|
'parent_type' => $parentType,
|
|
'parent_id' => $parentId,
|
|
'child_type' => $childType,
|
|
'child_id' => $childId,
|
|
],
|
|
[
|
|
'order_no' => $orderNo,
|
|
'metadata' => $metadata,
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 관계 해제
|
|
*
|
|
* @throws \App\Exceptions\BusinessException 잠금된 연결인 경우
|
|
*/
|
|
public static function unlink(
|
|
int $tenantId,
|
|
string $parentType,
|
|
int $parentId,
|
|
string $childType,
|
|
int $childId,
|
|
int $groupId = self::GROUP_ITEM_MASTER
|
|
): bool {
|
|
$relationship = self::where([
|
|
'tenant_id' => $tenantId,
|
|
'group_id' => $groupId,
|
|
'parent_type' => $parentType,
|
|
'parent_id' => $parentId,
|
|
'child_type' => $childType,
|
|
'child_id' => $childId,
|
|
])->first();
|
|
|
|
if (! $relationship) {
|
|
return false;
|
|
}
|
|
|
|
// 잠금 체크
|
|
if ($relationship->is_locked) {
|
|
throw new \App\Exceptions\BusinessException(__('error.relationship_locked'));
|
|
}
|
|
|
|
return $relationship->delete();
|
|
}
|
|
|
|
/**
|
|
* 특정 부모의 모든 자식 관계 해제 (잠금되지 않은 것만)
|
|
*
|
|
* @throws \App\Exceptions\BusinessException 잠금된 연결이 있는 경우
|
|
*/
|
|
public static function unlinkAllChildren(
|
|
int $tenantId,
|
|
string $parentType,
|
|
int $parentId,
|
|
?string $childType = null,
|
|
int $groupId = self::GROUP_ITEM_MASTER
|
|
): int {
|
|
$query = self::where([
|
|
'tenant_id' => $tenantId,
|
|
'group_id' => $groupId,
|
|
'parent_type' => $parentType,
|
|
'parent_id' => $parentId,
|
|
]);
|
|
|
|
if ($childType) {
|
|
$query->where('child_type', $childType);
|
|
}
|
|
|
|
// 잠금된 연결이 있는지 확인
|
|
if ($query->clone()->where('is_locked', true)->exists()) {
|
|
throw new \App\Exceptions\BusinessException(__('error.has_locked_relationships'));
|
|
}
|
|
|
|
return $query->delete();
|
|
}
|
|
|
|
/**
|
|
* 특정 자식 엔티티가 잠금된 연결을 가지고 있는지 확인
|
|
*/
|
|
public static function hasLockedParentRelationship(string $childType, int $childId): bool
|
|
{
|
|
return self::where('child_type', $childType)
|
|
->where('child_id', $childId)
|
|
->where('is_locked', true)
|
|
->exists();
|
|
}
|
|
|
|
/**
|
|
* 특정 자식 엔티티의 잠금 상태 조회 (computed)
|
|
* 연결이 잠겨있으면 엔티티도 잠금 상태로 간주
|
|
*/
|
|
public static function getChildLockStatus(string $childType, int $childId): array
|
|
{
|
|
$lockedRelationships = self::where('child_type', $childType)
|
|
->where('child_id', $childId)
|
|
->where('is_locked', true)
|
|
->get(['id', 'parent_type', 'parent_id']);
|
|
|
|
return [
|
|
'is_locked' => $lockedRelationships->isNotEmpty(),
|
|
'locked_by_relationships' => $lockedRelationships->map(fn ($rel) => [
|
|
'relationship_id' => $rel->id,
|
|
'parent_type' => $rel->parent_type,
|
|
'parent_id' => $rel->parent_id,
|
|
])->toArray(),
|
|
];
|
|
}
|
|
}
|