getRetentionDays(); $partitions = DB::select(" SELECT PARTITION_NAME, PARTITION_DESCRIPTION, TABLE_ROWS, DATA_LENGTH, INDEX_LENGTH FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'trigger_audit_logs' AND PARTITION_NAME IS NOT NULL ORDER BY PARTITION_ORDINAL_POSITION ", [$dbName]); // 저장소 전체 크기 $storageInfo = DB::selectOne(" SELECT ROUND(SUM(DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) as size_mb FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'trigger_audit_logs' ", [$dbName]); $totalRows = 0; $cutoffTs = Carbon::now()->subDays($retentionDays)->timestamp; $nowTs = Carbon::now()->timestamp; $items = []; foreach ($partitions as $p) { $totalRows += $p->TABLE_ROWS; if ($p->PARTITION_DESCRIPTION === 'MAXVALUE') { $items[] = [ 'name' => $p->PARTITION_NAME, 'bound_raw' => 'MAXVALUE', 'bound_date' => null, 'rows' => $p->TABLE_ROWS, 'status' => 'future', 'can_drop' => false, ]; continue; } $boundTs = (int) $p->PARTITION_DESCRIPTION; $boundDate = Carbon::createFromTimestamp($boundTs)->format('Y-m-d'); // 상태 결정 if ($boundTs <= $cutoffTs) { $status = 'expired'; // 보관기간 초과 } elseif ($boundTs <= $nowTs) { $prevMonthTs = Carbon::now()->startOfMonth()->timestamp; $status = $boundTs <= $prevMonthTs ? 'archived' : 'current'; } else { $status = 'upcoming'; // 미래 파티션 } $items[] = [ 'name' => $p->PARTITION_NAME, 'bound_raw' => $boundTs, 'bound_date' => $boundDate, 'rows' => $p->TABLE_ROWS, 'status' => $status, 'can_drop' => $status === 'expired', ]; } return [ 'partitions' => $items, 'total_partitions' => count($items), 'total_rows' => $totalRows, 'storage_mb' => $storageInfo->size_mb ?? 0, 'retention_days' => $retentionDays, ]; } /** * 미래 파티션 추가 (REORGANIZE PARTITION p_future) */ public function addFuturePartitions(int $months): array { $dbName = config('database.connections.mysql.database'); // 현재 파티션 바운드 조회 $partitions = DB::select(" SELECT PARTITION_NAME, PARTITION_DESCRIPTION FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'trigger_audit_logs' AND PARTITION_NAME IS NOT NULL ORDER BY PARTITION_ORDINAL_POSITION ", [$dbName]); $existingBounds = []; foreach ($partitions as $p) { if ($p->PARTITION_DESCRIPTION !== 'MAXVALUE') { $existingBounds[] = (int) $p->PARTITION_DESCRIPTION; } } $added = 0; $now = Carbon::now(); for ($i = 0; $i <= $months; $i++) { $target = $now->copy()->addMonths($i)->startOfMonth()->addMonth(); $ts = $target->timestamp; $name = 'p'.$target->copy()->subMonth()->format('Ym'); if (in_array($ts, $existingBounds)) { continue; } DB::statement("ALTER TABLE trigger_audit_logs REORGANIZE PARTITION p_future INTO ( PARTITION {$name} VALUES LESS THAN ({$ts}), PARTITION p_future VALUES LESS THAN MAXVALUE )"); $added++; } if ($added === 0) { return ['success' => true, 'message' => '추가할 파티션이 없습니다 (이미 존재).']; } return ['success' => true, 'message' => "파티션 {$added}개 추가 완료."]; } /** * 파티션 삭제 (보관기간 초과 검증 포함) */ public function dropPartition(string $name): array { if ($name === 'p_future') { return ['success' => false, 'message' => 'p_future 파티션은 삭제할 수 없습니다.']; } $dbName = config('database.connections.mysql.database'); $retentionDays = $this->getRetentionDays(); // 파티션 존재 및 바운드 확인 $partition = DB::selectOne(" SELECT PARTITION_NAME, PARTITION_DESCRIPTION, TABLE_ROWS FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'trigger_audit_logs' AND PARTITION_NAME = ? ", [$dbName, $name]); if (! $partition) { return ['success' => false, 'message' => "파티션 '{$name}'을 찾을 수 없습니다."]; } if ($partition->PARTITION_DESCRIPTION === 'MAXVALUE') { return ['success' => false, 'message' => '이 파티션은 삭제할 수 없습니다.']; } // 보관기간 초과 여부 서버측 검증 $boundTs = (int) $partition->PARTITION_DESCRIPTION; $cutoffTs = Carbon::now()->subDays($retentionDays)->timestamp; if ($boundTs > $cutoffTs) { return ['success' => false, 'message' => '보관기간이 초과되지 않은 파티션은 삭제할 수 없습니다.']; } $rows = $partition->TABLE_ROWS; DB::statement("ALTER TABLE trigger_audit_logs DROP PARTITION {$name}"); return ['success' => true, 'message' => "파티션 '{$name}' 삭제 완료 ({$rows}행)."]; } /** * 보관기간 (일) */ public function getRetentionDays(): int { return (int) env('AUDIT_RETENTION_DAYS', 395); } }