응답 메타 배열 */ private function buildSettingsMeta(): array { return [ 'limits' => config('core.settings_limits', []), 'env_priority_enabled' => EnvPriority::enabled(), 'env_locked' => $this->settingsService->envLockedMeta(), ]; } /** * 모든 시스템 설정을 조회합니다. * * @return JsonResponse 시스템 설정 목록을 포함한 JSON 응답 */ public function index(): JsonResponse { try { $settings = $this->settingsService->getAllSettings(); $settings['available_drivers'] = $this->driverRegistryService->getAllAvailableDrivers(); $settings['_meta'] = $this->buildSettingsMeta(); return $this->success('settings.fetch_success', (new SettingsResource($settings))->toArray(request()) ); } catch (\Exception $e) { return $this->error('settings.fetch_failed', 500, $e->getMessage()); } } /** * 여러 시스템 설정을 일괄 저장합니다. * * 저장 성공 시 전체 settings를 응답에 포함하여 * 프론트엔드에서 새로고침 없이 전역 상태를 업데이트할 수 있도록 합니다. * * @param SaveSettingsRequest $request 설정 저장 요청 데이터 * @return JsonResponse 저장 결과 JSON 응답 */ public function store(SaveSettingsRequest $request): JsonResponse { try { $result = $this->settingsService->saveSettings($request->validated()); if ($result) { // 저장 후 전체 설정 반환 (관리자 UI 상태 업데이트용) $allSettings = $this->settingsService->getAllSettings(); $allSettings['available_drivers'] = $this->driverRegistryService->getAllAvailableDrivers(); $allSettings['_meta'] = $this->buildSettingsMeta(); return $this->success('settings.save_success', [ 'settings' => $allSettings, ]); } else { return $this->error('settings.save_failed'); } } catch (ValidationException $e) { return $this->error('settings.save_failed', 422, $e->errors()); } catch (\Exception $e) { return $this->error('settings.save_error', 500, $e->getMessage()); } } /** * 특정 키의 시스템 설정을 조회합니다. * * @param string $key 조회할 설정 키 * @return JsonResponse 설정 값을 포함한 JSON 응답 */ public function show(string $key): JsonResponse { try { $value = $this->settingsService->getSetting($key); return $this->success('settings.fetch_success', [ 'key' => $key, 'value' => $value, ]); } catch (\Exception $e) { return $this->error('settings.fetch_failed', 500, $e->getMessage()); } } /** * 특정 키의 시스템 설정을 업데이트합니다. * * @param string $key 업데이트할 설정 키 * @param UpdateSettingRequest $request 설정 업데이트 요청 데이터 * @return JsonResponse 업데이트 결과 JSON 응답 */ public function update(string $key, UpdateSettingRequest $request): JsonResponse { try { $result = $this->settingsService->setSetting($key, $request->validated()['value']); if ($result) { return $this->success('settings.update_success'); } else { return $this->error('settings.update_failed'); } } catch (ValidationException $e) { return $this->error('settings.update_failed', 422, $e->errors()); } catch (\Exception $e) { return $this->error('settings.update_error', 500, $e->getMessage()); } } /** * 시스템 환경 정보를 조회합니다. * * @return JsonResponse 시스템 정보를 포함한 JSON 응답 */ public function systemInfo(): JsonResponse { try { $systemInfo = $this->settingsService->getSystemInfo(); return $this->success('common.success', $systemInfo); } catch (\Throwable $e) { Log::error('시스템 정보 조회 실패', ['error' => $e->getMessage()]); return $this->error('common.error_occurred', 500, $e->getMessage()); } } /** * 신뢰 프록시(리버스 프록시) 설정 진단 결과를 조회합니다 (#124). * * 읽기 전용이다 — 값 편집 엔드포인트는 두지 않는다. 이 값은 "앱이 프록시 없이 도달 * 가능한가" 라는 배포 구조 지식이 있어야 정할 수 있고, 웹에서 편집 가능해지면 관리자 * 계정 탈취가 곧 X-Forwarded-For 위조 경로가 된다. 편집은 `.env` 전용이다. * * 판정 대상은 관리자 브라우저의 **실제 요청**이므로 현재 요청을 그대로 쓴다. * 입력을 받지 않는 읽기 전용 조회라 FormRequest 를 두지 않는다. * * @return JsonResponse 진단 결과 JSON 응답 */ public function trustedProxy(): JsonResponse { return $this->success('common.success', TrustedProxyDiagnostic::forRequest(request())); } /** * 시스템 캐시를 정리합니다. * * @return JsonResponse 캐시 정리 결과 JSON 응답 */ public function clearCache(): JsonResponse { try { $result = $this->settingsService->clearCache(); if ($result) { return $this->success('settings.cache_clear_success'); } else { return $this->error('settings.cache_clear_failed'); } } catch (\Exception $e) { return $this->error('settings.cache_clear_error', 500, $e->getMessage()); } } /** * 시스템을 최적화합니다 (캐시 생성). * * @return JsonResponse 최적화 결과 JSON 응답 */ public function optimizeSystem(): JsonResponse { try { $result = $this->settingsService->optimizeSystem(); if ($result) { return $this->success('settings.optimize_success'); } else { return $this->error('settings.optimize_failed'); } } catch (\Exception $e) { return $this->error('settings.optimize_error', 500, $e->getMessage()); } } /** * 데이터베이스 백업 — 아직 제공하지 않는 기능임을 알립니다. * * 코어에는 DB 덤프 수단이 없어 이 엔드포인트는 구현된 적이 없다. * 종전에는 존재하지 않는 `SettingsService::backupDatabase()` 를 호출했고, * PHP 가 던지는 `Error` 는 `catch (\Exception)` 에 걸리지 않아 그대로 500 이 되면서 * 내부 메서드 이름까지 응답에 실려 나갔다 (공개 #115 부록 B3). * * 기능 부재는 서버 고장이 아니므로 501(Not Implemented)로 답한다. * 설정 파일 백업은 `POST /api/admin/settings/backup` 이 담당한다. * * @return JsonResponse 미제공 안내 JSON 응답 (501) */ public function backupDatabase(): JsonResponse { return $this->error('settings.database_backup_unavailable', 501); } /** * 현재 앱 키를 조회합니다 (마스킹된 형태). * * @return JsonResponse 마스킹된 앱 키 JSON 응답 */ public function getAppKey(): JsonResponse { try { $maskedKey = $this->settingsService->maskAppKey(); return $this->success('common.success', [ 'app_key' => $maskedKey, ]); } catch (\Exception $e) { return $this->error('common.error_occurred', 500, $e->getMessage()); } } /** * 앱 키를 재생성합니다. * * @param RegenerateAppKeyRequest $request 앱 키 재생성 요청 데이터 * @return JsonResponse 재생성된 앱 키 JSON 응답 */ public function regenerateAppKey(RegenerateAppKeyRequest $request): JsonResponse { try { $result = $this->settingsService->regenerateAppKey($request->password); if (! $result['success']) { return $this->error($result['error'], 401); } return $this->success('settings.app_key_regenerated', [ 'app_key' => $result['app_key'], ]); } catch (\Exception $e) { return $this->error('settings.app_key_regenerate_failed', 500, $e->getMessage()); } } /** * 설정을 백업합니다. * * @return JsonResponse 백업 결과 JSON 응답 */ public function backup(): JsonResponse { try { $backupPath = $this->settingsService->backupSettings(); return $this->success('settings.backup_success', [ 'backup_path' => $backupPath, ]); } catch (\Exception $e) { return $this->error('settings.backup_failed', 500, $e->getMessage()); } } /** * 백업에서 설정을 복원합니다. * * @param RestoreSettingsRequest $request 복원 요청 데이터 (백업 경로) * @return JsonResponse 복원 결과 JSON 응답 */ public function restore(RestoreSettingsRequest $request): JsonResponse { try { $backupPath = $request->validated('backup_path'); $result = $this->settingsService->restoreSettings($backupPath); if ($result) { return $this->success('settings.restore_success'); } return $this->error('settings.restore_failed'); } catch (\Exception $e) { return $this->error('settings.restore_error', 500, $e->getMessage()); } } /** * 테스트 메일을 발송합니다. * * @param TestMailRequest $request 테스트 메일 발송 요청 데이터 * @return JsonResponse 발송 결과 JSON 응답 */ public function testMail(TestMailRequest $request): JsonResponse { try { $validated = $request->validated(); $toEmail = $validated['to_email']; $mailSettings = collect($validated)->except('to_email')->filter(fn ($v) => $v !== null)->toArray(); $result = $this->settingsService->sendTestMail($toEmail, $mailSettings); if ($result['success']) { return $this->success($result['message'], [ 'subject' => $result['subject'], 'body' => $result['body'], ]); } return $this->error($result['message'], 500, $result['error'] ?? null); } catch (\Exception $e) { return $this->error('settings.test_mail_error', 500, $e->getMessage()); } } /** * 드라이버 연결을 테스트합니다. * * S3, Redis, Memcached, Websocket 등 외부 서비스 드라이버의 * 연결 상태를 테스트합니다. * * @param TestDriverConnectionRequest $request 드라이버 테스트 요청 데이터 * @return JsonResponse 테스트 결과 JSON 응답 */ public function testDriverConnection(TestDriverConnectionRequest $request): JsonResponse { try { $settings = $request->validated(); $result = $this->driverConnectionTester->testAll($settings); if ($result['all_passed']) { return $this->success('settings.driver_test_success', $result); } // 일부 테스트 실패 시에도 결과 반환 (성공 상태지만 all_passed가 false) return $this->success('settings.driver_test_partial', $result); } catch (\Exception $e) { return $this->error('settings.driver_test_error', 500, $e->getMessage()); } } /** * 아웃바운드 프록시 연결을 테스트합니다. * * 저장하기 전에 프록시가 실제로 동작하는지, 그리고 그 프록시를 거쳐 나갔을 때 상대편에 * 어떤 IP 로 보이는지 확인합니다. 출발지 IP 는 운영자가 결제사·외부 서비스에 등록해야 * 하는 값이라 결과의 핵심입니다. * * 검사 대상은 저장된 설정이 아니라 이번 요청이 제출한 값입니다. * * @param TestOutboundProxyRequest $request 검증된 요청 * @return JsonResponse 검사 결과 */ public function testOutboundProxy(TestOutboundProxyRequest $request): JsonResponse { $validated = $request->validated(); $result = $this->outboundProxyTester->test( (string) $validated['outbound_proxy'], (array) ($validated['outbound_proxy_bypass'] ?? []) ); // 연결 실패는 요청 처리 실패가 아니라 진단 결과다 — 200 으로 결과를 돌려주고 // 성공 여부는 페이로드가 말한다 (드라이버 연결 테스트와 같은 규약). return $this->success($result['message_key'], $result); } }