- Role 모델에 SoftDeletes 트레이트가 없어 에러 발생 - 테넌트 아카이브 시 역할 조회 로직 수정 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
327 lines
11 KiB
PHP
327 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Archives\ArchivedRecord;
|
|
use App\Models\Archives\ArchivedRecordRelation;
|
|
use App\Models\Commons\Menu;
|
|
use App\Models\Department;
|
|
use App\Models\Role;
|
|
use App\Models\Tenants\Tenant;
|
|
use App\Models\User;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
|
|
class ArchiveService
|
|
{
|
|
/**
|
|
* 모델 클래스 → record_type 매핑
|
|
*/
|
|
private const RECORD_TYPE_MAP = [
|
|
Tenant::class => 'tenant',
|
|
User::class => 'user',
|
|
Department::class => 'department',
|
|
Menu::class => 'menu',
|
|
Role::class => 'role',
|
|
// 추후 확장
|
|
// Board::class => 'board',
|
|
// Project::class => 'project',
|
|
// Issue::class => 'issue',
|
|
// Task::class => 'task',
|
|
];
|
|
|
|
/**
|
|
* record_type → 모델 클래스 역매핑
|
|
*/
|
|
private const MODEL_CLASS_MAP = [
|
|
'tenant' => Tenant::class,
|
|
'user' => User::class,
|
|
'department' => Department::class,
|
|
'menu' => Menu::class,
|
|
'role' => Role::class,
|
|
];
|
|
|
|
/**
|
|
* 단일 모델 아카이브
|
|
*
|
|
* @param Model $model 아카이브할 모델
|
|
* @param array $relationNames 함께 아카이브할 관계명 (예: ['users', 'departments'])
|
|
* @param string|null $batchId 배치 ID (동일 배치로 묶을 경우)
|
|
* @param string|null $description 배치 설명
|
|
* @param int|null $tenantId 대상 테넌트 ID (명시적 지정)
|
|
*/
|
|
public function archiveModel(
|
|
Model $model,
|
|
array $relationNames = [],
|
|
?string $batchId = null,
|
|
?string $description = null,
|
|
?int $tenantId = null
|
|
): ArchivedRecord {
|
|
$batchId = $batchId ?? (string) Str::uuid();
|
|
$recordType = $this->getRecordType($model);
|
|
|
|
// 기본 설명 생성
|
|
if (! $description) {
|
|
$description = $this->generateDescription($model, $recordType);
|
|
}
|
|
|
|
// tenant_id 결정
|
|
$tenantId = $tenantId ?? $this->determineTenantId($model, $recordType);
|
|
|
|
return DB::transaction(function () use ($model, $relationNames, $batchId, $description, $recordType, $tenantId) {
|
|
// 1. 메인 레코드 아카이브
|
|
$archivedRecord = ArchivedRecord::create([
|
|
'batch_id' => $batchId,
|
|
'batch_description' => $description,
|
|
'record_type' => $recordType,
|
|
'tenant_id' => $tenantId,
|
|
'original_id' => $model->getKey(),
|
|
'main_data' => $model->toArray(),
|
|
'schema_version' => 'v1.0',
|
|
'deleted_by' => auth()->id(),
|
|
'deleted_at' => now(),
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
|
|
// 2. 관계 데이터 아카이브
|
|
foreach ($relationNames as $relationName) {
|
|
$this->archiveRelation($archivedRecord, $model, $relationName);
|
|
}
|
|
|
|
return $archivedRecord;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 배치 아카이브 (여러 모델을 하나의 배치로)
|
|
*
|
|
* @param Collection $models 아카이브할 모델 컬렉션
|
|
* @param string $description 배치 설명
|
|
* @param array $relationNames 각 모델에서 아카이브할 관계명
|
|
* @return string 배치 ID
|
|
*/
|
|
public function archiveBatch(
|
|
Collection $models,
|
|
string $description,
|
|
array $relationNames = []
|
|
): string {
|
|
$batchId = (string) Str::uuid();
|
|
|
|
DB::transaction(function () use ($models, $description, $relationNames, $batchId) {
|
|
foreach ($models as $model) {
|
|
$this->archiveModel($model, $relationNames, $batchId, $description);
|
|
}
|
|
});
|
|
|
|
return $batchId;
|
|
}
|
|
|
|
/**
|
|
* 테넌트와 관련 데이터 전체 아카이브 (테넌트 삭제 전용)
|
|
*/
|
|
public function archiveTenantWithRelations(Tenant $tenant): string
|
|
{
|
|
$batchId = (string) Str::uuid();
|
|
$description = "테넌트 삭제: {$tenant->company_name} (ID: {$tenant->id})";
|
|
|
|
DB::transaction(function () use ($tenant, $batchId, $description) {
|
|
// 1. 메인 테넌트 아카이브 (tenant_id = 자기 자신)
|
|
$archivedRecord = ArchivedRecord::create([
|
|
'batch_id' => $batchId,
|
|
'batch_description' => $description,
|
|
'record_type' => 'tenant',
|
|
'tenant_id' => $tenant->id,
|
|
'original_id' => $tenant->id,
|
|
'main_data' => $tenant->toArray(),
|
|
'schema_version' => 'v1.0',
|
|
'deleted_by' => auth()->id(),
|
|
'deleted_at' => now(),
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
|
|
// 2. 관련 사용자 아카이브 (user_tenants 피벗 포함)
|
|
$users = $tenant->users()->get();
|
|
if ($users->isNotEmpty()) {
|
|
ArchivedRecordRelation::create([
|
|
'archived_record_id' => $archivedRecord->id,
|
|
'table_name' => 'users',
|
|
'data' => $users->map(fn ($user) => [
|
|
'user' => $user->toArray(),
|
|
'pivot' => $user->pivot?->toArray(),
|
|
])->toArray(),
|
|
'record_count' => $users->count(),
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
}
|
|
|
|
// 3. 부서 아카이브
|
|
$departments = $tenant->departments()->withTrashed()->get();
|
|
if ($departments->isNotEmpty()) {
|
|
ArchivedRecordRelation::create([
|
|
'archived_record_id' => $archivedRecord->id,
|
|
'table_name' => 'departments',
|
|
'data' => $departments->toArray(),
|
|
'record_count' => $departments->count(),
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
}
|
|
|
|
// 4. 메뉴 아카이브
|
|
$menus = $tenant->menus()->withTrashed()->get();
|
|
if ($menus->isNotEmpty()) {
|
|
ArchivedRecordRelation::create([
|
|
'archived_record_id' => $archivedRecord->id,
|
|
'table_name' => 'menus',
|
|
'data' => $menus->toArray(),
|
|
'record_count' => $menus->count(),
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
}
|
|
|
|
// 5. 역할 아카이브 (Role 모델에는 SoftDeletes 없음)
|
|
$roles = $tenant->roles()->get();
|
|
if ($roles->isNotEmpty()) {
|
|
ArchivedRecordRelation::create([
|
|
'archived_record_id' => $archivedRecord->id,
|
|
'table_name' => 'roles',
|
|
'data' => $roles->toArray(),
|
|
'record_count' => $roles->count(),
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
}
|
|
});
|
|
|
|
return $batchId;
|
|
}
|
|
|
|
/**
|
|
* 사용자 아카이브 (사용자 삭제 전용)
|
|
*/
|
|
public function archiveUser(User $user): string
|
|
{
|
|
$batchId = (string) Str::uuid();
|
|
$description = "사용자 삭제: {$user->name} ({$user->email})";
|
|
|
|
// tenant_id = 현재 선택된 테넌트
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
DB::transaction(function () use ($user, $batchId, $description, $tenantId) {
|
|
// 1. 메인 사용자 아카이브
|
|
$archivedRecord = ArchivedRecord::create([
|
|
'batch_id' => $batchId,
|
|
'batch_description' => $description,
|
|
'record_type' => 'user',
|
|
'tenant_id' => $tenantId,
|
|
'original_id' => $user->id,
|
|
'main_data' => $user->toArray(),
|
|
'schema_version' => 'v1.0',
|
|
'deleted_by' => auth()->id(),
|
|
'deleted_at' => now(),
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
|
|
// 2. 테넌트 관계 아카이브
|
|
$tenants = $user->tenants()->get();
|
|
if ($tenants->isNotEmpty()) {
|
|
ArchivedRecordRelation::create([
|
|
'archived_record_id' => $archivedRecord->id,
|
|
'table_name' => 'user_tenants',
|
|
'data' => $tenants->map(fn ($tenant) => [
|
|
'tenant_id' => $tenant->id,
|
|
'tenant_name' => $tenant->company_name,
|
|
'pivot' => $tenant->pivot?->toArray(),
|
|
])->toArray(),
|
|
'record_count' => $tenants->count(),
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
}
|
|
});
|
|
|
|
return $batchId;
|
|
}
|
|
|
|
/**
|
|
* 모델에서 record_type 추출
|
|
*/
|
|
public function getRecordType(Model $model): string
|
|
{
|
|
$class = get_class($model);
|
|
|
|
return self::RECORD_TYPE_MAP[$class] ?? Str::snake(class_basename($class));
|
|
}
|
|
|
|
/**
|
|
* record_type에서 모델 클래스 추출
|
|
*/
|
|
public function getModelClass(string $recordType): ?string
|
|
{
|
|
return self::MODEL_CLASS_MAP[$recordType] ?? null;
|
|
}
|
|
|
|
/**
|
|
* 관계 데이터 아카이브
|
|
*/
|
|
private function archiveRelation(ArchivedRecord $archivedRecord, Model $model, string $relationName): void
|
|
{
|
|
if (! method_exists($model, $relationName)) {
|
|
return;
|
|
}
|
|
|
|
$relation = $model->{$relationName}();
|
|
|
|
// withTrashed가 있으면 soft deleted 포함
|
|
if (method_exists($relation, 'withTrashed')) {
|
|
$relatedData = $relation->withTrashed()->get();
|
|
} else {
|
|
$relatedData = $relation->get();
|
|
}
|
|
|
|
if ($relatedData->isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
ArchivedRecordRelation::create([
|
|
'archived_record_id' => $archivedRecord->id,
|
|
'table_name' => $relationName,
|
|
'data' => $relatedData->toArray(),
|
|
'record_count' => $relatedData->count(),
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 기본 설명 생성
|
|
*/
|
|
private function generateDescription(Model $model, string $recordType): string
|
|
{
|
|
$name = $model->name ?? $model->company_name ?? $model->title ?? "ID: {$model->getKey()}";
|
|
|
|
return match ($recordType) {
|
|
'tenant' => "테넌트 삭제: {$name}",
|
|
'user' => "사용자 삭제: {$name}",
|
|
'department' => "부서 삭제: {$name}",
|
|
'menu' => "메뉴 삭제: {$name}",
|
|
'role' => "역할 삭제: {$name}",
|
|
default => "{$recordType} 삭제: {$name}",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* tenant_id 결정 로직
|
|
* - tenant 삭제: 삭제되는 테넌트의 ID (자기 자신)
|
|
* - user 삭제: session('selected_tenant_id') (현재 선택된 테넌트)
|
|
* - department/menu/role 삭제: 해당 레코드의 tenant_id
|
|
*/
|
|
private function determineTenantId(Model $model, string $recordType): ?int
|
|
{
|
|
return match ($recordType) {
|
|
'tenant' => $model->getKey(),
|
|
'user' => session('selected_tenant_id'),
|
|
'department', 'menu', 'role' => $model->tenant_id ?? session('selected_tenant_id'),
|
|
default => session('selected_tenant_id'),
|
|
};
|
|
}
|
|
}
|