feat: [archived-records] 아카이브 복원 기능 및 테넌트 필터링 구현
Phase 1 - 아카이브 복원 기능: - ArchiveService: 모델별 아카이브 로직 통합 (326줄) - RestoreService: 복원 로직 및 충돌 검사 (319줄) - ArchivedRecordController: restore, checkRestore 메서드 추가 - record_type enum→varchar 마이그레이션 - 복원 버튼 및 충돌 체크 UI (restore-check.blade.php) Phase 2 - 테넌트 필터링: - ArchivedRecord 모델: tenant_id fillable, tenant 관계 추가 - ArchiveService: tenant_id 저장 로직 (determineTenantId) - ArchivedRecordService: 테넌트별 필터링 쿼리 - 목록 UI: ID 컬럼, 대상 테넌트 컬럼 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
326
app/Services/ArchiveService.php
Normal file
326
app/Services/ArchiveService.php
Normal file
@@ -0,0 +1,326 @@
|
||||
<?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. 역할 아카이브
|
||||
$roles = $tenant->roles()->withTrashed()->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'),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user