settingsService->getAllSettings(); $settings = $this->appendCarriersToSettings($settings); $settings = $this->appendShippingTypesToSettings($settings); $settings = $this->appendShippingApiRequestFieldsToSettings($settings); $settings = $this->appendClaimReasonsToSettings($settings); $settings = $this->appendMileageNotificationChannelsToSettings($settings); $settings['available_pg_providers'] = $this->settingsService->getRegisteredPgProviders(); // 현금영수증 발급 프로바이더 후보 — 결제 플러그인이 훅으로 자신을 등록한다. // 발급 PG 와 결제 PG 는 독립 선택이므로 목록도 별도로 내린다 (KG 결제 + 토스 발급 등). $settings['available_cash_receipt_providers'] = $this->settingsService->getRegisteredCashReceiptProviders(); // 공개 자산 디스크 선택지 — 카탈로그 SSoT 는 코어 DriverRegistryService 게터 한 곳 $settings['available_public_asset_disks'] = app(DriverRegistryService::class) ->getAvailableDrivers('public_asset'); $settings['abilities'] = [ 'can_update' => PermissionHelper::check('sirsoft-ecommerce.settings.update', request()->user()), ]; // 화면의 숫자 입력 경계값 — 저장 규칙과 같은 출처를 내려준다. // 화면이 리터럴을 들면 규칙이 바뀔 때 따라오지 못해 "화면은 받는데 저장은 422" 가 된다. $settings['_meta'] = ['limits' => config('sirsoft-ecommerce.limits', [])]; return ResponseHelper::moduleSuccess( 'sirsoft-ecommerce', 'messages.settings.fetch_success', $settings ); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.settings.fetch_failed', 500 ); } } /** * 카테고리별 설정을 조회합니다. * * @param string $category 카테고리명 * @return JsonResponse 카테고리 설정을 포함한 JSON 응답 */ public function show(string $category): JsonResponse { try { $settings = $this->settingsService->getSettings($category); return ResponseHelper::moduleSuccess( 'sirsoft-ecommerce', 'messages.settings.fetch_success', [ 'category' => $category, 'settings' => $settings, 'abilities' => [ 'can_update' => PermissionHelper::check('sirsoft-ecommerce.settings.update', request()->user()), ], ] ); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.settings.fetch_failed', 500 ); } } /** * 이커머스 설정을 저장합니다. * * @param StoreEcommerceSettingsRequest $request 저장 요청 데이터 * @return JsonResponse 저장 결과 JSON 응답 */ public function store(StoreEcommerceSettingsRequest $request): JsonResponse { try { $settings = $request->validatedSettings(); // shipping.carriers는 DB 관리 대상 — JSON 저장에서 제외 $carriers = null; if (isset($settings['shipping']['carriers'])) { $carriers = $settings['shipping']['carriers']; unset($settings['shipping']['carriers']); } // shipping.types는 DB 관리 대상 — JSON 저장에서 제외 $shippingTypes = null; if (isset($settings['shipping']['types'])) { $shippingTypes = $settings['shipping']['types']; unset($settings['shipping']['types']); } // claim.refund_reasons는 DB 관리 대상 — JSON 저장에서 제외 $refundReasons = null; if (isset($settings['claim']['refund_reasons'])) { $refundReasons = $settings['claim']['refund_reasons']; unset($settings['claim']['refund_reasons']); } $result = $this->settingsService->saveSettings($settings); // carriers DB 동기화 if ($result && $carriers !== null) { $this->carrierService->syncCarriers($carriers); } // shipping types DB 동기화 if ($result && $shippingTypes !== null) { $this->shippingTypeService->syncShippingTypes($shippingTypes); } // claim reasons DB 동기화 if ($result && $refundReasons !== null) { $this->claimReasonService->syncReasons('refund', $refundReasons); } if ($result) { // 설정 저장 활동로그 (저장된 카테고리 목록 전달) HookManager::doAction('sirsoft-ecommerce.settings.after_save', array_keys($settings)); // 코어 모듈 설정 저장 훅 — SEO 캐시 무효화 등 코어/타 확장 리스너가 구독한다. // 발화 지점을 서비스가 아니라 관리자 컨트롤러에 두는 이유: 훅 의미가 // "관리자가 설정을 저장했다" 이고, 서비스에 두면 내부 저장 호출 전부가 // 활동로그·캐시 무효화를 유발한다. HookManager::doAction('core.module_settings.after_save', 'sirsoft-ecommerce', $settings, $result); // 저장 후 전체 설정 반환 (관리자 UI 상태 업데이트용) $updatedSettings = $this->settingsService->getAllSettings(); $updatedSettings = $this->appendCarriersToSettings($updatedSettings); $updatedSettings = $this->appendShippingTypesToSettings($updatedSettings); $updatedSettings = $this->appendShippingApiRequestFieldsToSettings($updatedSettings); $updatedSettings = $this->appendClaimReasonsToSettings($updatedSettings); $updatedSettings = $this->appendMileageNotificationChannelsToSettings($updatedSettings); $updatedSettings['available_pg_providers'] = $this->settingsService->getRegisteredPgProviders(); $updatedSettings['available_cash_receipt_providers'] = $this->settingsService->getRegisteredCashReceiptProviders(); // 공개 자산 디스크 선택지 — 저장 응답에도 재부착 (관리자 UI 상태 갱신용) $updatedSettings['available_public_asset_disks'] = app(DriverRegistryService::class) ->getAvailableDrivers('public_asset'); return ResponseHelper::moduleSuccess( 'sirsoft-ecommerce', 'messages.settings.save_success', $updatedSettings ); } else { return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.settings.save_failed', 400 ); } } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.settings.save_error', 500 ); } } /** * 은행 목록만 저장합니다. * * @param StoreBanksRequest $request 은행 목록 저장 요청 데이터 * @return JsonResponse 저장 결과 JSON 응답 */ public function storeBanks(StoreBanksRequest $request): JsonResponse { try { $banks = $request->validated('banks') ?? []; $result = $this->settingsService->saveBanks($banks); if ($result) { HookManager::doAction( 'core.module_settings.after_save', 'sirsoft-ecommerce', ['order_settings' => ['banks' => $banks]], $result ); $updatedSettings = $this->settingsService->getAllSettings(); return ResponseHelper::moduleSuccess( 'sirsoft-ecommerce', 'messages.settings.save_success', $updatedSettings ); } else { return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.settings.save_failed', 400 ); } } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.settings.save_error', 500 ); } } /** * 특정 설정값을 조회합니다. * * @param GetSettingRequest $request 요청 데이터 * @return JsonResponse 설정값을 포함한 JSON 응답 */ public function getSetting(GetSettingRequest $request): JsonResponse { try { $key = $request->validated('key'); $value = $this->settingsService->getSetting($key); return ResponseHelper::moduleSuccess( 'sirsoft-ecommerce', 'messages.settings.fetch_success', [ 'key' => $key, 'value' => $value, ] ); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.settings.fetch_failed', 500 ); } } /** * 특정 설정값을 업데이트합니다. * * @param UpdateSettingRequest $request 요청 데이터 * @return JsonResponse 업데이트 결과 JSON 응답 */ public function updateSetting(UpdateSettingRequest $request): JsonResponse { try { $key = $request->validated('key'); $value = $request->validated('value'); $result = $this->settingsService->setSetting($key, $value); if ($result) { // dot-key 를 카테고리 하위 구조로 되돌려 벌크 저장과 같은 payload 형태로 만든다 $payload = []; Arr::set($payload, $key, $value); HookManager::doAction('core.module_settings.after_save', 'sirsoft-ecommerce', $payload, $result); return ResponseHelper::moduleSuccess( 'sirsoft-ecommerce', 'messages.settings.update_success', ['updated' => true] ); } else { return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.settings.update_failed', 400 ); } } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.settings.update_error', 500 ); } } /** * 설정 캐시를 초기화합니다. * * @return JsonResponse 초기화 결과 JSON 응답 */ public function clearCache(): JsonResponse { try { $this->settingsService->clearCache(); app(SeoCacheManagerInterface::class)->clearAll(); return ResponseHelper::moduleSuccess( 'sirsoft-ecommerce', 'messages.settings.cache_clear_success', ['cleared' => true] ); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.settings.cache_clear_error', 500 ); } } /** * SEO 캐시 정보를 조회합니다. * * @return JsonResponse 캐시 페이지 수 및 용량을 포함한 JSON 응답 */ public function seoCacheInfo(): JsonResponse { try { $cacheManager = app(SeoCacheManagerInterface::class); $urls = $cacheManager->getCachedUrls(); $count = count($urls); // 각 캐시 엔트리의 HTML 크기를 합산하여 총 용량 계산 $totalBytes = 0; foreach ($urls as $url) { foreach (config('app.supported_locales', ['ko']) as $locale) { $html = $cacheManager->get($url, $locale); if ($html !== null) { $totalBytes += strlen($html); } } } return ResponseHelper::moduleSuccess( 'sirsoft-ecommerce', 'messages.settings.fetch_success', [ 'count' => $count, 'size_bytes' => $totalBytes, 'size_formatted' => $this->formatBytes($totalBytes), ] ); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.settings.fetch_failed', 500 ); } } /** * 바이트를 사람이 읽기 쉬운 형태로 변환합니다. * * @param int $bytes 바이트 수 * @return string 포맷된 문자열 (예: "1.5 MB") */ private function formatBytes(int $bytes): string { if ($bytes === 0) { return '0 B'; } $units = ['B', 'KB', 'MB', 'GB']; $i = (int) floor(log($bytes, 1024)); $i = min($i, count($units) - 1); return round($bytes / (1024 ** $i), 1).' '.$units[$i]; } /** * 설정 응답에 배송사 목록을 추가합니다. * * DB 관리 대상인 carriers를 shipping 섹션에 포함시킵니다. * * @param array $settings 설정 배열 * @return array carriers가 추가된 설정 배열 */ private function appendCarriersToSettings(array $settings): array { $carriers = $this->carrierService->getAllCarriers(); $settings['shipping']['carriers'] = $carriers->map(fn ($c) => [ 'id' => $c->id, 'code' => $c->code, 'name' => $c->name, 'type' => $c->type, 'tracking_url' => $c->tracking_url, 'is_active' => $c->is_active, 'sort_order' => $c->sort_order, ])->values()->toArray(); return $settings; } /** * 설정 응답에 배송유형 목록을 추가합니다. * * DB 관리 대상인 shipping types를 shipping 섹션에 포함시킵니다. * * @param array $settings 설정 배열 * @return array shipping types가 추가된 설정 배열 */ private function appendShippingTypesToSettings(array $settings): array { $settings['shipping']['types'] = $this->shippingTypeService->getTypesForSettings(); return $settings; } /** * 설정 응답에 배송 계산 API 요청 참고 필드 후보 목록을 추가합니다. * * 배송정책 폼의 "API 전송 필드" 후보(체크박스)는 프론트 하드코딩이 아니라 * 백엔드 SSoT enum(ShippingApiRequestField)에서 번역된 {value, label} 로 내려갑니다. * * @param array $settings 설정 배열 * @return array 후보 목록이 추가된 설정 배열 */ private function appendShippingApiRequestFieldsToSettings(array $settings): array { $settings['shipping']['api_request_fields'] = ShippingApiRequestField::options(); // 계산 API 고급 설정 옵션 — 프론트 하드코딩이 아닌 백엔드 enum SSoT 에서 번역되어 내려감 $settings['shipping']['api_http_methods'] = ShippingApiHttpMethod::options(); $settings['shipping']['api_auth_types'] = ShippingApiAuthType::options(); $settings['shipping']['api_response_types'] = ShippingApiResponseType::options(); return $settings; } /** * 설정 응답에 클레임 사유 목록을 추가합니다. * * DB 관리 대상인 claim reasons를 claim 섹션에 포함시킵니다. * * @param array $settings 설정 배열 * @return array claim reasons가 추가된 설정 배열 */ private function appendClaimReasonsToSettings(array $settings): array { $settings['claim']['refund_reasons'] = $this->claimReasonService->getReasonsForSettings('refund'); return $settings; } /** * 마일리지 소멸 예정 알림의 실제 활성 채널을 설정 응답에 병합합니다. * * 설정 카드의 "활성 채널" 칩은 선언 고정값이 아니라 관리자가 알림 설정에서 켜고 끈 * 실제 활성 채널을 보여줍니다. GenericNotification::via() 와 동일하게 도출합니다: * definition resolve → notification.channels 필터 → 확장 단위 채널 활성 여부. * * @param array $settings 설정 배열 * @return array 활성 채널이 병합된 설정 배열 */ private function appendMileageNotificationChannelsToSettings(array $settings): array { $type = 'mileage_expiring_soon'; try { $definition = app(NotificationDefinitionService::class)->resolve($type); $channels = $definition?->channels ?? ['mail', 'database']; $channels = HookManager::applyFilters( 'sirsoft-ecommerce.notification.channels', $channels, $type ); $channelService = app(NotificationChannelService::class); $active = array_values(array_filter( $channels, fn (string $channel) => $channelService->isChannelEnabledForExtension('module', 'sirsoft-ecommerce', $channel) )); } catch (Exception) { $active = []; } $settings['mileage']['notification_channels'] = $active; return $settings; } }