접속 IP 를 제한하는 결제사 API 를 로컬·스테이징에서 연동하려면 서버가 내보내는 요청의 출발지 IP 를 바꿔야 한다. 브라우저 프록시로는 바뀌지 않는 축이라 코어 환경설정으로 도입한다. 게이트는 디버그 모드이며 판정은 OutboundProxy 한 곳이 소유한다. 화면의 조건부 렌더링은 편의일 뿐이라 저장 API 직접 호출을 막지 못하므로, 실질 게이트를 판정 지점에 둔다. 주입·적용 지점은 결과만 소비한다. 적용은 Http::globalOptions 전역 옵션이라 확장의 Http:: 호출까지 함께 경유한다. 외부 연동 규약상 curl 핸들을 직접 다뤄야 하는 확장은 OutboundProxy::curlOptions 로 같은 프록시를 탄다 — KG이니시스 본인인증 승인 요청과 CBT 연결 점검을 이 통로로 편입했다. CBT 의 TCP 443 확인은 원시 소켓으로는 프록시를 태울 수 없어 curl CONNECT_ONLY 로 교체했다. 저장 전 연결 테스트는 저장값이 아니라 제출값을 검사하고, 그 프록시를 거쳤을 때 외부에 보이는 IP 를 함께 보고한다. 운영자가 결제사에 등록할 값이라 저장하고 나서 되짚지 않도록 했다. 적용과 같은 조립을 거치므로 확인한 구성과 저장 후 적용되는 구성이 어긋나지 않는다.
373 lines
13 KiB
PHP
373 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\Admin;
|
|
|
|
use App\Http\Controllers\Api\Base\AdminBaseController;
|
|
use App\Http\Requests\Settings\RegenerateAppKeyRequest;
|
|
use App\Http\Requests\Settings\RestoreSettingsRequest;
|
|
use App\Http\Requests\Settings\SaveSettingsRequest;
|
|
use App\Http\Requests\Settings\TestDriverConnectionRequest;
|
|
use App\Http\Requests\Settings\TestMailRequest;
|
|
use App\Http\Requests\Settings\TestOutboundProxyRequest;
|
|
use App\Http\Requests\Settings\UpdateSettingRequest;
|
|
use App\Http\Resources\SettingsResource;
|
|
use App\Services\DriverConnectionTester;
|
|
use App\Services\DriverRegistryService;
|
|
use App\Services\OutboundProxyTester;
|
|
use App\Services\SettingsService;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
/**
|
|
* 관리자용 시스템 설정 컨트롤러
|
|
*
|
|
* 관리자가 시스템 설정을 관리할 수 있는 기능을 제공합니다.
|
|
*/
|
|
class SettingsController extends AdminBaseController
|
|
{
|
|
public function __construct(
|
|
private SettingsService $settingsService,
|
|
private DriverConnectionTester $driverConnectionTester,
|
|
private DriverRegistryService $driverRegistryService,
|
|
private OutboundProxyTester $outboundProxyTester
|
|
) {
|
|
parent::__construct();
|
|
}
|
|
|
|
/**
|
|
* 모든 시스템 설정을 조회합니다.
|
|
*
|
|
* @return JsonResponse 시스템 설정 목록을 포함한 JSON 응답
|
|
*/
|
|
public function index(): JsonResponse
|
|
{
|
|
try {
|
|
$settings = $this->settingsService->getAllSettings();
|
|
$settings['available_drivers'] = $this->driverRegistryService->getAllAvailableDrivers();
|
|
$settings['_meta'] = ['limits' => config('core.settings_limits', [])];
|
|
|
|
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'] = ['limits' => config('core.settings_limits', [])];
|
|
|
|
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());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 시스템 캐시를 정리합니다.
|
|
*
|
|
* @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);
|
|
}
|
|
}
|