name('boost.browser-logs'); /* |-------------------------------------------------------------------------- | DevTools Routes |-------------------------------------------------------------------------- | | G7 DevTools 시스템을 위한 라우트입니다. | 상태 덤프, 로그 전송 등 디버깅 관련 엔드포인트를 제공합니다. | | 이 그룹은 자체 미들웨어를 선언하지 않는다 — `bootstrap/app.php` 가 이 파일 전체를 | `['api', 'debug.gate']` 로 감싸므로, 디버그 모드 게이트를 포함한 미들웨어는 그 단일 지점이 | 부여한다. 여기서 `->middleware('api')` 를 덧붙이면 같은 그룹이 두 번 지정되고 | (`Router::uniqueMiddleware()` 가 중복을 제거해 동작은 같다), `->middleware('debug.gate')` 를 | 라우트마다 붙이면 그룹 부착의 의미가 사라져 빠뜨린 라우트가 다시 생긴다. | 위 `_boost/browser-logs` 라우트도 같은 래퍼 안에 있어 동일한 게이트를 공유한다. | | 등록 계약(모든 `_boost` 라우트의 `gatherMiddleware()` 에 `debug.gate` 포함)은 | `tests/Feature/DevTools/DevtoolsRouteGateContractTest.php` 가 강제한다. | */ Route::prefix('_boost/g7-debug')->group(function () { /** * 상태 덤프 엔드포인트 * * 브라우저에서 전송한 디버깅 데이터를 파일로 저장합니다. * 섹션별 분할 전송을 지원합니다. */ Route::post('dump-state', function (Request $request): JsonResponse { // 테스트 요청 처리 if ($request->boolean('test')) { return response()->json([ 'status' => 'success', 'message' => __('devtools.connection_ok'), ]); } $debugDir = storage_path('debug-dump'); $sessionsDir = $debugDir.'/sessions'; if (! File::isDirectory($debugDir)) { File::makeDirectory($debugDir, 0755, true); } // 일괄 전송 처리 (소용량 데이터) if ($request->boolean('bulk') && $request->has('sections')) { return DebugDumpWriter::handleBulk($request, $debugDir); } // 섹션별 분할 전송 처리 (대용량 데이터) if ($request->has('sessionId') && $request->has('sectionName')) { return DebugDumpWriter::handleSectional($request, $debugDir, $sessionsDir); } // 레거시 전체 전송 처리 (하위 호환성) return DebugDumpWriter::handleLegacy($request, $debugDir); })->name('devtools.dump-state'); /** * 로그 전송 엔드포인트 * * 디버그 로그, 에러, 프로파일 데이터를 저장합니다. */ Route::post('log', function (Request $request): JsonResponse { $debugDir = storage_path('debug-dump'); if (! File::isDirectory($debugDir)) { File::makeDirectory($debugDir, 0755, true); } $type = $request->input('type', 'debug'); $data = $request->input('data'); $timestamp = $request->input('timestamp', now()->getTimestampMs()); $logEntry = [ 'type' => $type, 'data' => $data, 'timestamp' => $timestamp, 'created_at' => now()->toIso8601String(), ]; // 로그 타입별 파일 저장 $filename = match ($type) { 'error' => 'errors-latest.json', 'profile' => 'profile-latest.json', default => 'debug-latest.json', }; // 기존 로그 읽기 $logFile = $debugDir.'/'.$filename; $logs = []; if (File::exists($logFile)) { $existing = json_decode(File::get($logFile), true); if (is_array($existing)) { $logs = $existing; } } // 새 로그 추가 (최대 100개 유지) $logs[] = $logEntry; if (count($logs) > 100) { $logs = array_slice($logs, -100); } File::put($logFile, json_encode($logs, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); return response()->json([ 'status' => 'success', 'logged' => true, ]); })->name('devtools.log'); /** * 상태 조회 엔드포인트 (MCP 도구용) */ Route::get('state', function (): JsonResponse { $debugDir = storage_path('debug-dump'); $stateFile = $debugDir.'/state-latest.json'; if (! File::exists($stateFile)) { return response()->json([ 'status' => 'no_data', 'message' => __('devtools.no_state'), ]); } $state = json_decode(File::get($stateFile), true); return response()->json([ 'status' => 'success', 'state' => $state, 'timestamp' => File::lastModified($stateFile), ]); })->name('devtools.state'); /** * 액션 이력 조회 엔드포인트 (MCP 도구용) */ Route::get('actions', function (Request $request): JsonResponse { $debugDir = storage_path('debug-dump'); $actionsFile = $debugDir.'/actions-latest.json'; if (! File::exists($actionsFile)) { return response()->json([ 'status' => 'no_data', 'message' => __('devtools.no_actions'), ]); } $actions = json_decode(File::get($actionsFile), true); $limit = (int) $request->input('limit', 50); $filter = $request->input('filter'); // 필터 적용 if ($filter && is_array($actions)) { $actions = array_filter($actions, function ($action) use ($filter) { return ($action['type'] ?? '') === $filter || ($action['status'] ?? '') === $filter; }); } // 제한 적용 if (is_array($actions)) { $actions = array_slice($actions, -$limit); } return response()->json([ 'status' => 'success', 'actions' => $actions, 'total' => count($actions ?? []), ]); })->name('devtools.actions'); /** * 캐시 통계 조회 엔드포인트 (MCP 도구용) */ Route::get('cache', function (): JsonResponse { $debugDir = storage_path('debug-dump'); $cacheFile = $debugDir.'/cache-latest.json'; if (! File::exists($cacheFile)) { return response()->json([ 'status' => 'no_data', 'message' => __('devtools.no_cache'), ]); } $cache = json_decode(File::get($cacheFile), true); return response()->json([ 'status' => 'success', 'cache' => $cache, ]); })->name('devtools.cache'); /** * 변경 감지 정보 조회 엔드포인트 (MCP 도구용) */ Route::get('change-detection', function (Request $request): JsonResponse { $debugDir = storage_path('debug-dump'); $changeDetectionFile = $debugDir.'/change-detection-latest.json'; if (! File::exists($changeDetectionFile)) { return response()->json([ 'status' => 'no_data', 'message' => __('devtools.no_change_detection'), ]); } $changeDetection = json_decode(File::get($changeDetectionFile), true); $limit = (int) $request->input('limit', 20); $warningsOnly = $request->boolean('warningsOnly'); $handlerName = $request->input('handlerName'); // 경고만 필터 if ($warningsOnly && isset($changeDetection['alerts'])) { $changeDetection['alerts'] = array_filter($changeDetection['alerts'], function ($alert) { return in_array($alert['severity'] ?? '', ['warning', 'error']); }); } // 핸들러 이름 필터 if ($handlerName && isset($changeDetection['executionDetails'])) { $changeDetection['executionDetails'] = array_filter($changeDetection['executionDetails'], function ($detail) use ($handlerName) { return stripos($detail['handlerName'] ?? '', $handlerName) !== false; }); } // 제한 적용 if (isset($changeDetection['executionDetails']) && is_array($changeDetection['executionDetails'])) { $changeDetection['executionDetails'] = array_slice($changeDetection['executionDetails'], -$limit); } if (isset($changeDetection['alerts']) && is_array($changeDetection['alerts'])) { $changeDetection['alerts'] = array_slice($changeDetection['alerts'], -$limit); } return response()->json([ 'status' => 'success', 'changeDetection' => $changeDetection, 'timestamp' => File::lastModified($changeDetectionFile), ]); })->name('devtools.change-detection'); /** * 전체 디버그 데이터 삭제 */ Route::delete('clear', function (): JsonResponse { $debugDir = storage_path('debug-dump'); if (File::isDirectory($debugDir)) { File::cleanDirectory($debugDir); } return response()->json([ 'status' => 'success', 'message' => __('devtools.cleared'), ]); })->name('devtools.clear'); });