Files
sam-manage/app/Services/ArchiveService.php
kent 273ac6caf6 fix(mng): Role 모델 withTrashed() 호출 제거
- Role 모델에 SoftDeletes 트레이트가 없어 에러 발생
- 테넌트 아카이브 시 역할 조회 로직 수정

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 22:43:39 +09:00

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