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>
320 lines
11 KiB
PHP
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(),
|
|
];
|
|
}
|
|
}
|