Files
sam-manage/app/Services/RestoreService.php
kent 6b40362392 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>
2025-12-01 00:43:58 +09:00

320 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;
class RestoreService
{
/**
* record_type → 모델 클래스 매핑
*/
private const MODEL_CLASS_MAP = [
'tenant' => Tenant::class,
'user' => User::class,
'department' => Department::class,
'menu' => Menu::class,
'role' => Role::class,
];
/**
* record_type → 테이블명 매핑
*/
private const TABLE_NAME_MAP = [
'tenant' => 'tenants',
'user' => 'users',
'department' => 'departments',
'menu' => 'menus',
'role' => 'roles',
];
/**
* 단일 아카이브 레코드 복원
*
* @return Model 복원된 모델
*
* @throws \Exception 복원 실패 시
*/
public function restoreRecord(ArchivedRecord $record): Model
{
$modelClass = $this->getModelClass($record->record_type);
if (! $modelClass) {
throw new \Exception("지원하지 않는 레코드 타입: {$record->record_type}");
}
return DB::transaction(function () use ($record, $modelClass) {
// 1. 메인 데이터 복원
$mainData = $record->main_data;
// ID, timestamps 제거 (새로 생성되도록)
unset($mainData['id'], $mainData['created_at'], $mainData['updated_at'], $mainData['deleted_at']);
// soft delete 관련 필드 초기화
$mainData['deleted_by'] = null;
// 복원자 정보 추가
$mainData['created_by'] = auth()->id();
$mainData['updated_by'] = auth()->id();
// 모델 생성
$model = $modelClass::create($mainData);
// 2. 아카이브 레코드 삭제
$record->relations()->delete();
$record->delete();
return $model;
});
}
/**
* 배치 전체 복원 (테넌트 + 관련 데이터)
*
* @return Collection 복원된 모델들
*/
public function restoreBatch(string $batchId): Collection
{
$records = ArchivedRecord::where('batch_id', $batchId)
->with('relations')
->get();
if ($records->isEmpty()) {
throw new \Exception("배치를 찾을 수 없습니다: {$batchId}");
}
$restoredModels = collect();
DB::transaction(function () use ($records, &$restoredModels) {
foreach ($records as $record) {
// 테넌트 복원의 경우 특별 처리
if ($record->record_type === 'tenant') {
$restoredModels->push($this->restoreTenantWithRelations($record));
} elseif ($record->record_type === 'user') {
$restoredModels->push($this->restoreUserWithRelations($record));
} else {
$restoredModels->push($this->restoreRecord($record));
}
}
});
return $restoredModels;
}
/**
* 테넌트와 관련 데이터 복원
*/
public function restoreTenantWithRelations(ArchivedRecord $record): Tenant
{
if ($record->record_type !== 'tenant') {
throw new \Exception('테넌트 타입의 레코드가 아닙니다.');
}
return DB::transaction(function () use ($record) {
// 1. 테넌트 복원
$tenantData = $record->main_data;
unset($tenantData['id'], $tenantData['created_at'], $tenantData['updated_at'], $tenantData['deleted_at']);
$tenantData['deleted_by'] = null;
$tenantData['created_by'] = auth()->id();
$tenant = Tenant::create($tenantData);
// 2. 관련 데이터 복원
foreach ($record->relations as $relation) {
$this->restoreRelationData($tenant, $relation);
}
// 3. 아카이브 삭제
$record->relations()->delete();
$record->delete();
return $tenant;
});
}
/**
* 사용자와 관련 데이터 복원
*/
public function restoreUserWithRelations(ArchivedRecord $record): User
{
if ($record->record_type !== 'user') {
throw new \Exception('사용자 타입의 레코드가 아닙니다.');
}
return DB::transaction(function () use ($record) {
// 1. 사용자 복원
$userData = $record->main_data;
unset($userData['id'], $userData['created_at'], $userData['updated_at'], $userData['deleted_at']);
$userData['deleted_by'] = null;
$userData['created_by'] = auth()->id();
$user = User::create($userData);
// 2. 테넌트 관계 복원
foreach ($record->relations as $relation) {
if ($relation->table_name === 'user_tenants') {
foreach ($relation->data as $tenantData) {
// 테넌트가 존재하는지 확인
$tenantId = $tenantData['tenant_id'] ?? null;
if ($tenantId && Tenant::find($tenantId)) {
$pivotData = $tenantData['pivot'] ?? [];
unset($pivotData['user_id'], $pivotData['tenant_id']);
$user->tenants()->attach($tenantId, $pivotData);
}
}
}
}
// 3. 아카이브 삭제
$record->relations()->delete();
$record->delete();
return $user;
});
}
/**
* 관계 데이터 복원
*/
private function restoreRelationData(Tenant $tenant, ArchivedRecordRelation $relation): void
{
$tableName = $relation->table_name;
$data = $relation->data;
switch ($tableName) {
case 'users':
// 사용자 복원은 별도 처리 (user_tenants 피벗 포함)
foreach ($data as $item) {
$userData = $item['user'] ?? $item;
$pivotData = $item['pivot'] ?? [];
// 이미 존재하는 사용자인지 확인
$existingUser = User::where('email', $userData['email'])->first();
if ($existingUser) {
// 기존 사용자에 테넌트 관계만 추가
if (! $existingUser->tenants()->where('tenants.id', $tenant->id)->exists()) {
unset($pivotData['user_id'], $pivotData['tenant_id']);
$existingUser->tenants()->attach($tenant->id, $pivotData);
}
}
// 새 사용자 생성은 보안상 하지 않음 (비밀번호 문제)
}
break;
case 'departments':
foreach ($data as $deptData) {
unset($deptData['id'], $deptData['created_at'], $deptData['updated_at'], $deptData['deleted_at']);
$deptData['tenant_id'] = $tenant->id;
$deptData['created_by'] = auth()->id();
Department::create($deptData);
}
break;
case 'menus':
// 메뉴는 parent_id 순서 고려 필요 (부모 먼저)
$sortedMenus = collect($data)->sortBy('parent_id')->values();
$menuIdMap = []; // 원본 ID → 새 ID 매핑
foreach ($sortedMenus as $menuData) {
$originalId = $menuData['id'];
unset($menuData['id'], $menuData['created_at'], $menuData['updated_at'], $menuData['deleted_at']);
$menuData['tenant_id'] = $tenant->id;
$menuData['created_by'] = auth()->id();
// parent_id 매핑
if (! empty($menuData['parent_id']) && isset($menuIdMap[$menuData['parent_id']])) {
$menuData['parent_id'] = $menuIdMap[$menuData['parent_id']];
} else {
$menuData['parent_id'] = null;
}
$newMenu = Menu::create($menuData);
$menuIdMap[$originalId] = $newMenu->id;
}
break;
case 'roles':
foreach ($data as $roleData) {
unset($roleData['id'], $roleData['created_at'], $roleData['updated_at'], $roleData['deleted_at']);
$roleData['tenant_id'] = $tenant->id;
$roleData['created_by'] = auth()->id();
Role::create($roleData);
}
break;
}
}
/**
* record_type에서 모델 클래스 가져오기
*/
private function getModelClass(string $recordType): ?string
{
return self::MODEL_CLASS_MAP[$recordType] ?? null;
}
/**
* 복원 가능 여부 확인
*/
public function canRestore(ArchivedRecord $record): array
{
$issues = [];
// 1. 모델 클래스 지원 여부
if (! $this->getModelClass($record->record_type)) {
$issues[] = "지원하지 않는 레코드 타입입니다: {$record->record_type}";
}
// 2. 테넌트 복원 시, 동일 코드 존재 여부 확인
if ($record->record_type === 'tenant') {
$code = $record->main_data['code'] ?? null;
if ($code && Tenant::where('code', $code)->exists()) {
$issues[] = "동일한 테넌트 코드가 이미 존재합니다: {$code}";
}
}
// 3. 사용자 복원 시, 동일 이메일 존재 여부 확인
if ($record->record_type === 'user') {
$email = $record->main_data['email'] ?? null;
if ($email && User::where('email', $email)->exists()) {
$issues[] = "동일한 이메일의 사용자가 이미 존재합니다: {$email}";
}
}
return [
'can_restore' => empty($issues),
'issues' => $issues,
];
}
/**
* 배치 내 모든 레코드의 복원 가능 여부 확인
*/
public function canRestoreBatch(string $batchId): array
{
$records = ArchivedRecord::where('batch_id', $batchId)->get();
$allIssues = [];
foreach ($records as $record) {
$check = $this->canRestore($record);
if (! $check['can_restore']) {
$allIssues = array_merge($allIssues, $check['issues']);
}
}
return [
'can_restore' => empty($allIssues),
'issues' => array_unique($allIssues),
'record_count' => $records->count(),
];
}
}