'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(), ]; } }