{{ config('app.name', '그누보드7') }}
개발 환경 대시보드
개발 도구
.env, storage 파일 삭제하여 설치 상태 초기화
이 옵션을 활성화하면 시스템 알림을 받습니다
💡 라이트/다크 모드 모두 지원 | onChange 이벤트 객체 전달
💡 count > 0이면 삭제 차단 (onBeforeRemove)
💡 체크박스 다중 선택 | 새 항목 추가 불가
0, 'failed' => 0, 'names' => []]; if (!is_dir($dir)) { return $result; } $isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; $keep = array_merge(['.', '..', '.gitignore', '.gitkeep'], $keep); foreach (scandir($dir) as $item) { if (in_array($item, $keep, true)) { continue; } $path = $dir . DIRECTORY_SEPARATOR . $item; if (is_dir($path)) { $escaped = escapeshellarg($path); if ($isWindows) { exec("rmdir /s /q $escaped 2>nul"); } else { exec("rm -rf $escaped"); } if (is_dir($path)) { $result['failed']++; } else { $result['deleted']++; $result['names'][] = $item; } } elseif (@unlink($path)) { $result['deleted']++; $result['names'][] = $item; } else { $result['failed']++; } } return $result; } $modules = scanExtensions(base_path('modules')); $templates = scanExtensions(base_path('templates')); $plugins = scanExtensions(base_path('plugins')); // 언어팩은 활성 디렉토리(lang-packs/{identifier}) 와 번들(lang-packs/_bundled/{identifier}) // 양쪽 모두 식별자 기반이므로 동일 헬퍼 사용 가능 $languagePacks = scanExtensions(base_path('lang-packs')); // AJAX 요청 처리: GET 방식으로 처리 if (isset($_GET['ajax_action'])) { header('Content-Type: application/json'); $action = $_GET['ajax_action']; // Artisan 명령어 실행 if ($action === 'artisan') { $command = $_GET['command'] ?? ''; try { $parts = preg_split('/\s+/', trim($command)); $cmdName = array_shift($parts); $arguments = []; foreach ($parts as $part) { // 빈 문자열 스킵 (연속 공백 처리) if (empty($part)) { continue; } if (str_starts_with($part, '--')) { $optPart = substr($part, 2); if (str_contains($optPart, '=')) { [$optName, $optValue] = explode('=', $optPart, 2); $arguments['--' . $optName] = $optValue; } else { $arguments['--' . $optPart] = true; } } else { if (!isset($arguments['identifier'])) { $arguments['identifier'] = $part; } } } $exitCode = \Illuminate\Support\Facades\Artisan::call($cmdName, $arguments); $output = \Illuminate\Support\Facades\Artisan::output(); // 출력 메시지 정리 (전체 출력 유지) $cleanOutput = trim($output); // 출력이 비어있으면 기본 메시지 if (empty($cleanOutput)) { $cleanOutput = $exitCode === 0 ? '명령이 성공적으로 실행되었습니다.' : '명령 실행 중 오류가 발생했습니다.'; } // 종료 코드 표시 // // 비-0 종료가 곧 "명령 실패" 는 아니다. 점검 계열 커맨드(예: search:index)는 // 점검 자체는 정상 수행하고 "이상을 발견했다" 는 뜻으로 비-0 을 돌려준다. // 이것을 "실행 실패" 로 적으면 운영자가 도구가 고장난 것으로 읽고 // 정작 발견된 이상(색인 누락 등)을 놓친다. if ($exitCode === 0) { $cleanOutput = "✅ 완료\n\n" . $cleanOutput; } else { $cleanOutput = "⚠️ 종료 코드 {$exitCode} — 아래 출력을 확인하세요\n\n" . $cleanOutput; } echo json_encode([ 'success' => $exitCode === 0, 'output' => $cleanOutput, 'code' => $exitCode ]); } catch (\Exception $e) { echo json_encode([ 'success' => false, 'output' => '❌ ' . $e->getMessage(), 'code' => 1 ]); } exit; } // 템플릿 삭제 (Manager 직접 사용 - 확인 프롬프트 우회) if ($action === 'template_uninstall') { $identifier = $_GET['identifier'] ?? ''; try { $manager = app(\App\Extension\TemplateManager::class); $manager->loadTemplates(); $result = $manager->uninstallTemplate($identifier); echo json_encode([ 'success' => $result, 'output' => $result ? "✅ 템플릿 '{$identifier}' 삭제 완료" : "❌ 삭제 실패", 'code' => $result ? 0 : 1 ]); } catch (\Exception $e) { echo json_encode(['success' => false, 'output' => '❌ ' . $e->getMessage(), 'code' => 1]); } exit; } // 모듈 삭제 (Manager 직접 사용) if ($action === 'module_uninstall') { $identifier = $_GET['identifier'] ?? ''; try { $manager = app(\App\Extension\ModuleManager::class); $manager->loadModules(); $result = $manager->uninstallModule($identifier); echo json_encode([ 'success' => $result, 'output' => $result ? "✅ 모듈 '{$identifier}' 삭제 완료" : "❌ 삭제 실패", 'code' => $result ? 0 : 1 ]); } catch (\Exception $e) { echo json_encode(['success' => false, 'output' => '❌ ' . $e->getMessage(), 'code' => 1]); } exit; } // 플러그인 삭제 (Manager 직접 사용) if ($action === 'plugin_uninstall') { $identifier = $_GET['identifier'] ?? ''; try { $manager = app(\App\Extension\PluginManager::class); $manager->loadPlugins(); $result = $manager->uninstallPlugin($identifier); echo json_encode([ 'success' => $result, 'output' => $result ? "✅ 플러그인 '{$identifier}' 삭제 완료" : "❌ 삭제 실패", 'code' => $result ? 0 : 1 ]); } catch (\Exception $e) { echo json_encode(['success' => false, 'output' => '❌ ' . $e->getMessage(), 'code' => 1]); } exit; } // 템플릿 빌드 (npm run build) if ($action === 'template_build') { $identifier = $_GET['identifier'] ?? ''; try { $templatePath = base_path('templates/' . $identifier); if (!is_dir($templatePath)) { echo json_encode([ 'success' => false, 'output' => "❌ 템플릿 '{$identifier}' 디렉토리를 찾을 수 없습니다.", 'code' => 1 ]); exit; } // package.json 존재 확인 if (!file_exists($templatePath . '/package.json')) { echo json_encode([ 'success' => false, 'output' => "❌ package.json 파일이 없습니다.", 'code' => 1 ]); exit; } // Windows 환경에서 npm run build 실행 $command = "cd " . escapeshellarg($templatePath) . " && npm run build 2>&1"; if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { $command = "cd /d " . escapeshellarg($templatePath) . " && npm run build 2>&1"; } $output = []; $returnCode = 0; exec($command, $output, $returnCode); // ANSI 색상 코드 제거 및 로그 필터링 $filteredOutput = []; foreach ($output as $line) { // ANSI 색상 코드 제거 $cleanLine = preg_replace('/\x1b\[[0-9;]*m/', '', $line); // 필터링할 패턴 (상세 진행 로그 제외) $skipPatterns = [ '/^transforming\.\.\./', // transforming... '/^✓ \d+ modules transformed/', // ✓ 194 modules transformed '/^rendering chunks\.\.\./', // rendering chunks... '/^computing gzip size\.\.\./', // computing gzip size... '/^You are using Node\.js/', // Node.js 버전 경고 '/^vite v[\d\.]+ building/', // vite 버전 정보 ]; $shouldSkip = false; foreach ($skipPatterns as $pattern) { if (preg_match($pattern, $cleanLine)) { $shouldSkip = true; break; } } if (!$shouldSkip && trim($cleanLine) !== '') { $filteredOutput[] = $cleanLine; } } $outputText = implode("\n", $filteredOutput); // 성공/실패 여부 표시 if ($returnCode === 0) { $outputText = "✅ 빌드 완료\n\n" . $outputText; } else { $outputText = "❌ 빌드 실패\n\n" . $outputText; } echo json_encode([ 'success' => $returnCode === 0, 'output' => $outputText, 'code' => $returnCode ]); } catch (\Exception $e) { echo json_encode(['success' => false, 'output' => '❌ ' . $e->getMessage(), 'code' => 1]); } exit; } // 인스톨러 초기화 if ($action === 'reset') { $results = []; $deleteVendor = isset($_GET['delete_vendor']) && $_GET['delete_vendor'] === '1'; // DB 테이블 전체 삭제 (.env 삭제 전에 실행해야 DB 연결 유지) $dropTables = isset($_GET['drop_tables']) && $_GET['drop_tables'] === '1'; if ($dropTables) { try { $tables = \Illuminate\Support\Facades\DB::select('SHOW TABLES'); if (empty($tables)) { $results[] = ['name' => 'DB 테이블', 'status' => 'notfound']; } else { // SHOW TABLES 결과의 첫 번째 컬럼명을 동적으로 가져옴 $columnKey = array_key_first((array) $tables[0]); $tableNames = array_map(fn($t) => ((array) $t)[$columnKey], $tables); \Illuminate\Support\Facades\DB::statement('SET FOREIGN_KEY_CHECKS = 0'); $droppedCount = 0; foreach ($tableNames as $tableName) { \Illuminate\Support\Facades\DB::statement("DROP TABLE IF EXISTS `{$tableName}`"); $droppedCount++; } \Illuminate\Support\Facades\DB::statement('SET FOREIGN_KEY_CHECKS = 1'); $results[] = [ 'name' => "DB 테이블 ({$droppedCount}개 삭제)", 'status' => 'deleted' ]; } } catch (\Exception $e) { $results[] = ['name' => 'DB 테이블 (' . $e->getMessage() . ')', 'status' => 'failed']; } } // 삭제 대상 파일 목록 (표시명 => 경로) $filesToDelete = [ '.env' => base_path('.env'), 'storage/app/g7_installed' => storage_path('app/g7_installed'), 'storage/installer-state.json' => storage_path('installer-state.json') ]; foreach ($filesToDelete as $displayName => $file) { if (file_exists($file)) { if (@unlink($file)) { $results[] = ['name' => $displayName, 'status' => 'deleted']; } else { $results[] = ['name' => $displayName, 'status' => 'failed']; } } else { $results[] = ['name' => $displayName, 'status' => 'notfound']; } } if ($deleteVendor) { $vendorPath = base_path('vendor'); if (is_dir($vendorPath)) { // .gitkeep 보존: vendor 내부 항목만 개별 삭제 $vendorItems = array_diff(scandir($vendorPath), ['.', '..', '.gitkeep']); $deletedCount = 0; $failedCount = 0; foreach ($vendorItems as $item) { $itemPath = $vendorPath . DIRECTORY_SEPARATOR . $item; $escapedItem = escapeshellarg($itemPath); if (is_dir($itemPath)) { if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { exec("rmdir /s /q $escapedItem 2>nul", $output, $returnCode); } else { exec("rm -rf $escapedItem", $output, $returnCode); } is_dir($itemPath) ? $failedCount++ : $deletedCount++; } else { @unlink($itemPath) ? $deletedCount++ : $failedCount++; } } $results[] = [ 'name' => 'vendor/ (.gitkeep 보존)', 'status' => $failedCount === 0 ? 'deleted' : 'failed' ]; } else { $results[] = ['name' => 'vendor/', 'status' => 'notfound']; } } // 확장 설치 디렉토리 삭제 (_bundled, _pending 보존) $deleteExtensions = isset($_GET['delete_extensions']) && $_GET['delete_extensions'] === '1'; if ($deleteExtensions) { $extensionTypes = ['modules', 'plugins', 'templates']; $protectedDirs = ['_bundled', '_pending']; foreach ($extensionTypes as $type) { $basePath = base_path($type); if (!is_dir($basePath)) { $results[] = ['name' => "$type/", 'status' => 'notfound']; continue; } $dirs = array_filter(glob($basePath . '/*'), 'is_dir'); $deletedDirs = []; $failedDirs = []; foreach ($dirs as $dir) { $dirName = basename($dir); if (in_array($dirName, $protectedDirs)) { continue; // _bundled, _pending 보호 } $escapedDir = escapeshellarg($dir); if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { exec("rmdir /s /q $escapedDir 2>nul", $output, $returnCode); } else { exec("rm -rf $escapedDir", $output, $returnCode); } is_dir($dir) ? $failedDirs[] = $dirName : $deletedDirs[] = $dirName; } if (empty($deletedDirs) && empty($failedDirs)) { $results[] = ['name' => "$type/", 'status' => 'notfound']; } else { $status = empty($failedDirs) ? 'deleted' : 'failed'; $detail = implode(', ', $deletedDirs); $results[] = ['name' => "$type/ ($detail)", 'status' => $status]; } } } // 환경설정 파일 삭제 (코어 + 모듈 + 플러그인 + 템플릿) $deleteSettings = isset($_GET['delete_settings']) && $_GET['delete_settings'] === '1'; if ($deleteSettings) { // 코어: storage/app/settings/*.json (SettingsService 의 SSoT) $coreSettingsPath = storage_path('app/settings'); $cleared = devDashboardClearDirectory($coreSettingsPath); if ($cleared['deleted'] === 0 && $cleared['failed'] === 0) { $results[] = ['name' => '코어 환경설정 (storage/app/settings)', 'status' => 'notfound']; } else { $results[] = [ 'name' => "코어 환경설정 (storage/app/settings, {$cleared['deleted']}개)", 'status' => $cleared['failed'] === 0 ? 'deleted' : 'failed' ]; } // 모듈/플러그인: storage/app/{modules,plugins}/{identifier}/settings/ // (ModuleSettingsService / PluginSettingsService 가 StorageInterface 로 기록하는 경로) foreach (['modules' => '모듈', 'plugins' => '플러그인'] as $storageDir => $label) { $basePath = storage_path('app/' . $storageDir); $clearedIds = []; $failedIds = []; if (is_dir($basePath)) { foreach (array_filter(glob($basePath . '/*'), 'is_dir') as $extDir) { $settingsDir = $extDir . DIRECTORY_SEPARATOR . 'settings'; if (!is_dir($settingsDir)) { continue; } $cleared = devDashboardClearDirectory($settingsDir); if ($cleared['failed'] > 0) { $failedIds[] = basename($extDir); } elseif ($cleared['deleted'] > 0) { $clearedIds[] = basename($extDir); } } } if (empty($clearedIds) && empty($failedIds)) { $results[] = ['name' => "{$label} 환경설정 (storage/app/{$storageDir})", 'status' => 'notfound']; } else { $detail = implode(', ', array_merge($clearedIds, $failedIds)); $results[] = [ 'name' => "{$label} 환경설정 ({$detail})", 'status' => empty($failedIds) ? 'deleted' : 'failed' ]; } } // 템플릿은 파일 기반 환경설정 저장소를 갖지 않는다. // 활성 상태/레이아웃 편집분은 DB(templates, template_layouts), 나머지는 템플릿 패키지 파일이므로 // 각각 "DB 테이블 전체 삭제" 와 "확장 설치 디렉토리 삭제" 가 담당한다. // 삭제 대상이 0 건인 것과 대상 자체가 없는 것은 다르므로 그 사실을 그대로 표기한다. $results[] = [ 'name' => '템플릿 환경설정 (파일 저장소 없음 — DB/패키지 보관)', 'status' => 'notfound' ]; } // 첨부파일 스토리지 삭제 $deleteAttachments = isset($_GET['delete_attachments']) && $_GET['delete_attachments'] === '1'; if ($deleteAttachments) { // 코어 첨부파일 디스크(config/attachment.php) + 공개 업로드 디스크 $attachmentPaths = [ 'storage/app/attachments' => storage_path('app/attachments'), 'storage/app/public' => storage_path('app/public') ]; foreach ($attachmentPaths as $displayName => $path) { $cleared = devDashboardClearDirectory($path); if ($cleared['deleted'] === 0 && $cleared['failed'] === 0) { $results[] = ['name' => $displayName, 'status' => 'notfound']; } else { $results[] = [ 'name' => "{$displayName} ({$cleared['deleted']}개)", 'status' => $cleared['failed'] === 0 ? 'deleted' : 'failed' ]; } } // 모듈/플러그인 업로드 파일 (예: storage/app/modules/{id}/images/products) // settings/ 와 .preserve-ownership 마커는 보존 — 환경설정 삭제 옵션과 서로 간섭하지 않도록 분리한다. foreach (['modules' => '모듈', 'plugins' => '플러그인'] as $storageDir => $label) { $basePath = storage_path('app/' . $storageDir); $clearedIds = []; $failedIds = []; if (is_dir($basePath)) { foreach (array_filter(glob($basePath . '/*'), 'is_dir') as $extDir) { $cleared = devDashboardClearDirectory($extDir, ['settings', '.preserve-ownership']); if ($cleared['failed'] > 0) { $failedIds[] = basename($extDir); } elseif ($cleared['deleted'] > 0) { $clearedIds[] = basename($extDir); } } } if (empty($clearedIds) && empty($failedIds)) { $results[] = ['name' => "{$label} 업로드 파일 (storage/app/{$storageDir})", 'status' => 'notfound']; } else { $detail = implode(', ', array_merge($clearedIds, $failedIds)); $results[] = [ 'name' => "{$label} 업로드 파일 ({$detail})", 'status' => empty($failedIds) ? 'deleted' : 'failed' ]; } } } echo json_encode(['success' => true, 'results' => $results]); exit; } } ?>
개발 환경 대시보드
.env, storage 파일 삭제하여 설치 상태 초기화
이 옵션을 활성화하면 시스템 알림을 받습니다
💡 라이트/다크 모드 모두 지원 | onChange 이벤트 객체 전달
💡 count > 0이면 삭제 차단 (onBeforeRemove)
💡 체크박스 다중 선택 | 새 항목 추가 불가