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>
151 lines
5.4 KiB
PHP
151 lines
5.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Archives\ArchivedRecord;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class ArchivedRecordService
|
|
{
|
|
/**
|
|
* 아카이브 레코드 목록 조회 (batch_id로 그룹핑, 페이지네이션)
|
|
* batch_id가 NULL인 기존 데이터는 각각 개별 batch로 취급
|
|
*
|
|
* NOTE: GROUP BY + paginate() 조합 시 total count가 잘못 계산되는 문제 해결
|
|
* 서브쿼리로 GROUP BY 결과를 감싼 후 외부에서 paginate() 실행
|
|
*/
|
|
public function getArchivedRecordsBatched(array $filters = [], int $perPage = 15): LengthAwarePaginator
|
|
{
|
|
// 1. 서브쿼리: batch_id별 그룹핑
|
|
$subQuery = ArchivedRecord::query()
|
|
->select([
|
|
DB::raw("COALESCE(batch_id, CONCAT('legacy_', id)) as batch_id"),
|
|
DB::raw("COALESCE(batch_description, CONCAT(record_type, ' 삭제 (ID: ', original_id, ')')) as batch_description"),
|
|
DB::raw('MIN(id) as id'),
|
|
DB::raw('MIN(tenant_id) as tenant_id'),
|
|
DB::raw('GROUP_CONCAT(DISTINCT record_type) as record_types'),
|
|
DB::raw('COUNT(*) as record_count'),
|
|
DB::raw('MIN(deleted_by) as deleted_by'),
|
|
DB::raw('MIN(deleted_at) as deleted_at'),
|
|
// 대상 정보 추출 (테넌트명, 사용자명 등)
|
|
DB::raw("MAX(JSON_UNQUOTE(JSON_EXTRACT(main_data, '$.company_name'))) as target_company_name"),
|
|
DB::raw("MAX(JSON_UNQUOTE(JSON_EXTRACT(main_data, '$.name'))) as target_name"),
|
|
DB::raw("MAX(JSON_UNQUOTE(JSON_EXTRACT(main_data, '$.email'))) as target_email"),
|
|
DB::raw("MAX(JSON_UNQUOTE(JSON_EXTRACT(main_data, '$.code'))) as target_code"),
|
|
]);
|
|
|
|
// 테넌트 필터링 (서브쿼리에서 적용)
|
|
if (! empty($filters['tenant_id'])) {
|
|
$subQuery->where('tenant_id', $filters['tenant_id']);
|
|
}
|
|
|
|
$subQuery->groupBy(
|
|
DB::raw("COALESCE(batch_id, CONCAT('legacy_', id))"),
|
|
DB::raw("COALESCE(batch_description, CONCAT(record_type, ' 삭제 (ID: ', original_id, ')'))")
|
|
);
|
|
|
|
// 2. 외부 쿼리: 서브쿼리를 감싸서 정확한 count 계산
|
|
$query = DB::table(DB::raw("({$subQuery->toSql()}) as grouped"))
|
|
->mergeBindings($subQuery->getQuery())
|
|
->select('*');
|
|
|
|
// 레코드 타입 필터
|
|
if (! empty($filters['record_type'])) {
|
|
$query->where('record_types', 'like', "%{$filters['record_type']}%");
|
|
}
|
|
|
|
// 삭제자 필터
|
|
if (! empty($filters['deleted_by'])) {
|
|
$query->where('deleted_by', $filters['deleted_by']);
|
|
}
|
|
|
|
// 검색
|
|
if (! empty($filters['search'])) {
|
|
$query->where('batch_description', 'like', "%{$filters['search']}%");
|
|
}
|
|
|
|
// 정렬 (기본: 삭제일시 내림차순)
|
|
$sortBy = $filters['sort_by'] ?? 'deleted_at';
|
|
$sortDirection = $filters['sort_direction'] ?? 'desc';
|
|
$query->orderBy($sortBy, $sortDirection);
|
|
|
|
// NOTE: DB::table() 사용 시 request의 page 파라미터를 자동으로 읽지 못함
|
|
// filters에서 page를 명시적으로 전달
|
|
$page = $filters['page'] ?? null;
|
|
|
|
return $query->paginate($perPage, ['*'], 'page', $page);
|
|
}
|
|
|
|
/**
|
|
* 특정 batch의 모든 레코드 조회
|
|
* legacy_ 접두사가 붙은 경우 단일 레코드 ID로 조회
|
|
*/
|
|
public function getRecordsByBatchId(string $batchId): Collection
|
|
{
|
|
// legacy_ 접두사가 붙은 경우 (기존 데이터)
|
|
if (str_starts_with($batchId, 'legacy_')) {
|
|
$id = (int) str_replace('legacy_', '', $batchId);
|
|
|
|
return ArchivedRecord::with(['deletedByUser', 'relations'])
|
|
->where('id', $id)
|
|
->get();
|
|
}
|
|
|
|
return ArchivedRecord::with(['deletedByUser', 'relations'])
|
|
->where('batch_id', $batchId)
|
|
->orderBy('id')
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* 특정 아카이브 레코드 조회
|
|
*/
|
|
public function getArchivedRecordById(int $id): ?ArchivedRecord
|
|
{
|
|
return ArchivedRecord::with(['deletedByUser', 'relations'])
|
|
->find($id);
|
|
}
|
|
|
|
/**
|
|
* batch_id로 첫 번째 레코드 조회 (상세 페이지 진입용)
|
|
*/
|
|
public function getFirstRecordByBatchId(string $batchId): ?ArchivedRecord
|
|
{
|
|
return ArchivedRecord::with(['deletedByUser', 'relations'])
|
|
->where('batch_id', $batchId)
|
|
->orderBy('id')
|
|
->first();
|
|
}
|
|
|
|
/**
|
|
* 삭제자 목록 조회 (필터용)
|
|
*/
|
|
public function getDeletedByUsers(): array
|
|
{
|
|
return ArchivedRecord::query()
|
|
->whereNotNull('deleted_by')
|
|
->with('deletedByUser:id,name')
|
|
->get()
|
|
->pluck('deletedByUser')
|
|
->filter()
|
|
->unique('id')
|
|
->values()
|
|
->toArray();
|
|
}
|
|
|
|
/**
|
|
* 아카이브 통계
|
|
*/
|
|
public function getStats(): array
|
|
{
|
|
return [
|
|
'total_batches' => ArchivedRecord::distinct('batch_id')->count('batch_id'),
|
|
'total_records' => ArchivedRecord::count(),
|
|
'tenants' => ArchivedRecord::tenant()->count(),
|
|
'users' => ArchivedRecord::user()->count(),
|
|
];
|
|
}
|
|
}
|