인터뷰에서 방향만 정하고 남겨 뒀던 후속 5건을 구현하고, 커밋 전 종합 점검에서 발견한 결함 2건을 함께 고쳤다. 기능 도입: - 업로드 이미지 축소 — `upload.image_max_width` / `image_max_height` / `image_quality` 는 저장·검증만 되고 리사이즈 코드가 0건이었다. 비율 유지, 상한 이내면 재인코딩하지 않음(불필요한 화질 손실 방지), 한계 미설정이면 무동작(기존 사이트 이미지 보호). PNG·GIF·WebP 투명도 보존. 실패해도 업로드는 계속되고 원본이 저장된다. - 로그인 2단계 인증 — `security.two_factor_auth` 는 설정 항목만 있고 구현도 화면 컨트롤도 없었다. 코어 IDV 인프라를 재사용해 purpose `login` 을 추가하고, 비밀번호 확인 통과 후 challenge 만 발행한다(토큰 미발급 + logout). `POST /api/auth/login/two-factor` 가 코드를 확인해야 토큰이 나가며, 로그인과 같은 요청 제한을 적용한다. challenge 의 purpose 를 대조해 다른 흐름의 challenge 로 로그인하는 것을 막는다. 코드 발송이 실패하면 실패로 알린다 — 2단계를 건너뛰고 로그인시키면 통제가 조용히 열린다. 결함 수정: - 게시글 수정 시 사용자 경로가 `attachment_ids` 를 Service 로 넘기지 않아, 검증과 권한은 통과하고 200 을 받는데 첨부만 조용히 연결되지 않았다. 관리자 경로는 넘기고 있어 같은 요청이 화면에 따라 다르게 동작했다. - 설정 화면 라디오가 `pointer-events-none` + Label click 으로만 상태를 바꿔, 키보드 방향키(change) 조작 시 화면 표시와 저장될 값이 어긋났다. 동일 패턴 4개 레이아웃 12개 라디오에 change 액션을 부여하고 params 를 click 과 동일하게 고정한다. - 주문완료 화면이 결제수단 확인보다 영수증 조회를 앞세워, PG 를 거치지 않는 주문에서도 매번 404 요청이 나갔다. - 개별 게시판 폼의 숫자 항목에 경계값 바인딩이 없어 환경설정 화면과 안내가 달랐다. 커밋 전 점검에서 발견: - 2단계 인증이 반쪽으로 들어가 있었다. enum 에 case 만 추가하고 코어 purpose 레지스트리와 ko/en 라벨을 빠뜨려, 목적 목록 API 에서 통째로 빠지고 `hasPurpose` 가 false 였으며 라벨 대신 i18n 키 원문이 반환됐다. 관리자가 이 목적의 메시지 템플릿·정책을 만들 수 없는 상태였다. 세 곳을 enum 기준으로 전수 대조하는 회귀 테스트를 함께 둔다. - 업로드 이미지 축소가 적용되지 않은 8번째 서비스(레이아웃 편집기 첨부)가 있었다. 열거형 커버리지 테스트는 내 조사 결과를 green 으로 고정할 뿐 전수를 증명하지 못한다 — 같은 규약을 조건으로 표현한 audit 룰이 첫 실행에서 검출했다. 재발 방지: - audit 룰 `upload-service-image-resize` (신규 업로드 서비스의 축소 누락 검출) - 트러블슈팅 사례 20 (코어 설정이 일부 경로에만 적용되는 증상의 진단 절차) - `sirsoft-ckeditor5` 의 코어 최소 요구 버전을 7.0.6 으로 상향 (신설 클래스 의존) 공개 이슈: gnuboard/g7
680 lines
26 KiB
PHP
680 lines
26 KiB
PHP
<?php
|
|
|
|
namespace App\Providers;
|
|
|
|
use App\Repositories\JsonConfigRepository;
|
|
use App\Support\AllowedExtensions;
|
|
use Illuminate\Support\Facades\Config;
|
|
use Illuminate\Support\ServiceProvider;
|
|
|
|
/**
|
|
* 설정 서비스 프로바이더
|
|
*
|
|
* Laravel 부트스트랩 시 JSON 설정 파일을 로드하여
|
|
* config()에 주입합니다. DB 연결 전에 실행됩니다.
|
|
*/
|
|
class SettingsServiceProvider extends ServiceProvider
|
|
{
|
|
/**
|
|
* 코어 설정 카테고리 목록
|
|
*/
|
|
private const CORE_CATEGORIES = [
|
|
'mail',
|
|
'general',
|
|
'security',
|
|
'debug',
|
|
'drivers',
|
|
'cache',
|
|
'upload',
|
|
'core_update',
|
|
'geoip',
|
|
'seo',
|
|
'identity',
|
|
];
|
|
|
|
/**
|
|
* 서비스를 등록합니다.
|
|
*
|
|
* DB 연결 전에 실행되므로 JSON 파일에서 직접 읽습니다.
|
|
*/
|
|
public function register(): void
|
|
{
|
|
// JsonConfigRepository를 직접 인스턴스화 (DI 컨테이너 사용 불가)
|
|
$configRepository = new JsonConfigRepository;
|
|
|
|
// 각 설정 파일이 존재하는 경우에만 해당 설정을 오버라이드
|
|
$this->applyMailConfig($configRepository);
|
|
$this->applyAppConfig($configRepository);
|
|
$this->applyDebugConfig($configRepository);
|
|
$this->applyDriverConfig($configRepository);
|
|
$this->applyCacheConfig($configRepository);
|
|
$this->applyUploadConfig($configRepository);
|
|
$this->applyCoreUpdateConfig($configRepository);
|
|
$this->applyGeoIpConfig($configRepository);
|
|
$this->applyIdentityConfig($configRepository);
|
|
|
|
// 코어 설정을 g7_settings.core prefix로 저장
|
|
$this->loadCoreSettingsToConfig($configRepository);
|
|
}
|
|
|
|
/**
|
|
* 코어 설정을 g7_settings.core prefix로 Config에 저장합니다.
|
|
*
|
|
* 모든 코어 설정을 통합하여 g7_settings('core.category.key') 형태로
|
|
* 접근할 수 있도록 합니다.
|
|
*/
|
|
private function loadCoreSettingsToConfig(JsonConfigRepository $configRepository): void
|
|
{
|
|
$coreSettings = [];
|
|
|
|
foreach (self::CORE_CATEGORIES as $category) {
|
|
$settings = $configRepository->getCategory($category);
|
|
if (! empty($settings)) {
|
|
$coreSettings[$category] = $settings;
|
|
}
|
|
}
|
|
|
|
Config::set('g7_settings.core', $coreSettings);
|
|
}
|
|
|
|
/**
|
|
* 서비스를 부트스트랩합니다.
|
|
*/
|
|
public function boot(): void
|
|
{
|
|
// 부트스트랩 후 추가 설정 적용 가능
|
|
}
|
|
|
|
/**
|
|
* 메일 설정을 Laravel config에 적용합니다.
|
|
*/
|
|
private function applyMailConfig(JsonConfigRepository $configRepository): void
|
|
{
|
|
$mailSettings = $configRepository->getCategory('mail');
|
|
|
|
if (empty($mailSettings)) {
|
|
return;
|
|
}
|
|
|
|
// 메일러 설정
|
|
if (! empty($mailSettings['mailer'])) {
|
|
Config::set('mail.default', $mailSettings['mailer']);
|
|
}
|
|
|
|
// SMTP 설정
|
|
if (! empty($mailSettings['host'])) {
|
|
Config::set('mail.mailers.smtp.host', $mailSettings['host']);
|
|
}
|
|
|
|
if (! empty($mailSettings['port'])) {
|
|
Config::set('mail.mailers.smtp.port', (int) $mailSettings['port']);
|
|
}
|
|
|
|
if (! empty($mailSettings['username'])) {
|
|
Config::set('mail.mailers.smtp.username', $mailSettings['username']);
|
|
}
|
|
|
|
if (! empty($mailSettings['password'])) {
|
|
Config::set('mail.mailers.smtp.password', $mailSettings['password']);
|
|
}
|
|
|
|
if (isset($mailSettings['encryption'])) {
|
|
Config::set('mail.mailers.smtp.encryption', $mailSettings['encryption'] ?: null);
|
|
}
|
|
|
|
// 드라이버별 설정
|
|
$mailer = $mailSettings['mailer'] ?? '';
|
|
|
|
if ($mailer === 'mailgun') {
|
|
if (! empty($mailSettings['mailgun_domain'])) {
|
|
Config::set('services.mailgun.domain', $mailSettings['mailgun_domain']);
|
|
}
|
|
if (! empty($mailSettings['mailgun_secret'])) {
|
|
Config::set('services.mailgun.secret', $mailSettings['mailgun_secret']);
|
|
}
|
|
Config::set('services.mailgun.endpoint',
|
|
! empty($mailSettings['mailgun_endpoint']) ? $mailSettings['mailgun_endpoint'] : 'api.mailgun.net'
|
|
);
|
|
}
|
|
|
|
if ($mailer === 'ses') {
|
|
if (! empty($mailSettings['ses_key'])) {
|
|
Config::set('services.ses.key', $mailSettings['ses_key']);
|
|
}
|
|
if (! empty($mailSettings['ses_secret'])) {
|
|
Config::set('services.ses.secret', $mailSettings['ses_secret']);
|
|
}
|
|
Config::set('services.ses.region',
|
|
! empty($mailSettings['ses_region']) ? $mailSettings['ses_region'] : 'ap-northeast-2'
|
|
);
|
|
}
|
|
|
|
// 발신자 설정
|
|
if (! empty($mailSettings['from_address'])) {
|
|
Config::set('mail.from.address', $mailSettings['from_address']);
|
|
}
|
|
|
|
if (! empty($mailSettings['from_name'])) {
|
|
Config::set('mail.from.name', $mailSettings['from_name']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 앱 설정을 Laravel config에 적용합니다.
|
|
*/
|
|
private function applyAppConfig(JsonConfigRepository $configRepository): void
|
|
{
|
|
$generalSettings = $configRepository->getCategory('general');
|
|
|
|
if (empty($generalSettings)) {
|
|
return;
|
|
}
|
|
|
|
if (! empty($generalSettings['site_name'])) {
|
|
// site_name 이 다국어 JSON array 일 수 있으므로 현재/폴백 로케일 string 으로 정규화한다
|
|
// (공개#49). raw array 를 config('app.name') 에 넣으면 app.blade.php 의
|
|
// {{ config('app.name') }} (Blade e() → htmlspecialchars) 에서 TypeError 가 발생해
|
|
// SPA <title> 페이지가 깨진다. OG 경로(SeoMetaResolver)와 동일하게 안전화한다.
|
|
Config::set('app.name', $this->localizeSettingValue($generalSettings['site_name']));
|
|
}
|
|
|
|
if (! empty($generalSettings['site_url'])) {
|
|
Config::set('app.url', $generalSettings['site_url']);
|
|
}
|
|
|
|
if (! empty($generalSettings['timezone'])) {
|
|
// 환경설정의 timezone은 사용자 표시용 기본 타임존
|
|
// app.timezone(서버 저장 타임존)은 항상 UTC 유지
|
|
Config::set('app.default_user_timezone', $generalSettings['timezone']);
|
|
}
|
|
|
|
if (! empty($generalSettings['language'])) {
|
|
Config::set('app.locale', $generalSettings['language']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 설정값이 다국어 JSON array 면 현재/폴백 로케일 string 으로 정규화합니다 (공개#49).
|
|
*
|
|
* SeoMetaResolver/SeoRenderer 의 resolveLocalizedValue 와 동일 의미. 단, 이 Provider 는
|
|
* register() 단계(DI 컨테이너 사용 전)에서 호출되므로 SEO 트레이트에 의존하지 않고 인라인한다.
|
|
*
|
|
* @param mixed $value 설정값 (string / 다국어 array / scalar)
|
|
* @return string 현재/폴백 로케일 string
|
|
*/
|
|
private function localizeSettingValue(mixed $value): string
|
|
{
|
|
if (is_string($value)) {
|
|
return $value;
|
|
}
|
|
|
|
if (is_array($value)) {
|
|
$locale = config('app.locale', 'ko');
|
|
if (isset($value[$locale]) && is_string($value[$locale])) {
|
|
return $value[$locale];
|
|
}
|
|
$fallback = config('app.fallback_locale', 'en');
|
|
if (isset($value[$fallback]) && is_string($value[$fallback])) {
|
|
return $value[$fallback];
|
|
}
|
|
// 첫 string 값 폴백
|
|
foreach ($value as $candidate) {
|
|
if (is_string($candidate)) {
|
|
return $candidate;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
return (string) ($value ?? '');
|
|
}
|
|
|
|
/**
|
|
* 디버그 설정을 Laravel config에 적용합니다.
|
|
*
|
|
* testing 환경(PHPUnit, .env.testing)에서는 settings JSON 의 mode 값이 .env.testing 의
|
|
* APP_DEBUG 를 덮어쓰지 않는다. 운영 PC 의 settings(mode=false) 때문에 PHPUnit / E2E
|
|
* 인프라(PlaywrightIssueToken 등)가 차단되는 회귀를 방지한다. logging 레벨도 동일 사유로
|
|
* 스킵한다.
|
|
*
|
|
* production 환경에서 Playwright E2E 가 production DB 에 토큰을 발급해야 하는 경우
|
|
* (호스트 도메인이 .env.testing 의 testing DB 가 아니라 .env 의 production DB 를 사용하는 상황),
|
|
* G7_PLAYWRIGHT_BYPASS=1 환경변수가 부여된 호출은 settings JSON 덮어쓰기를 건너뛰어
|
|
* APP_DEBUG inline override 가 유지되도록 한다. CLI 한정 + 명시 옵트인이므로
|
|
* 운영 admin UI 토글 SSoT 정책은 보존된다.
|
|
*/
|
|
private function applyDebugConfig(JsonConfigRepository $configRepository): void
|
|
{
|
|
if (app()->environment('testing') || env('G7_PLAYWRIGHT_BYPASS') === '1') {
|
|
return;
|
|
}
|
|
|
|
$debugSettings = $configRepository->getCategory('debug');
|
|
|
|
if (empty($debugSettings)) {
|
|
return;
|
|
}
|
|
|
|
$isDebugMode = isset($debugSettings['mode']) && (bool) $debugSettings['mode'];
|
|
|
|
if (isset($debugSettings['mode'])) {
|
|
Config::set('app.debug', $isDebugMode);
|
|
}
|
|
|
|
// debug 모드가 true이면 log_level을 debug로 강제 설정
|
|
if ($isDebugMode) {
|
|
$logLevel = 'debug';
|
|
} elseif (! empty($debugSettings['log_level'])) {
|
|
$logLevel = $debugSettings['log_level'];
|
|
} else {
|
|
$logLevel = null;
|
|
}
|
|
|
|
if ($logLevel) {
|
|
Config::set('logging.level', $logLevel);
|
|
Config::set('logging.channels.single.level', $logLevel);
|
|
Config::set('logging.channels.daily.level', $logLevel);
|
|
}
|
|
|
|
// SQL 쿼리 로그 설정
|
|
if (isset($debugSettings['sql_query_log'])) {
|
|
Config::set('g7.sql_query_log', (bool) $debugSettings['sql_query_log']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 드라이버 설정을 Laravel config에 적용합니다.
|
|
*
|
|
* 캐시, 세션, 큐, 스토리지, Redis 드라이버 설정을 오버라이드합니다.
|
|
*
|
|
* **테스트 환경 격리**: `storage/app/settings/drivers.json`은 모든 환경이 공유하는
|
|
* 파일이므로, testing 환경에서 cache/session/queue 드라이버를 덮어쓰면 phpunit.xml이
|
|
* 지정한 테스트 전용 드라이버(array/array/sync)가 무시되고 dev의 Redis 등을 공유하게 됩니다.
|
|
* 그 결과 테스트가 dev 캐시를 오염시켜 알림 훅 등록 같은 부팅 데이터가 silent하게
|
|
* 망가지는 치명적 상태가 발생합니다. testing 환경에서는 드라이버 오버라이드를 건너뜁니다.
|
|
*/
|
|
private function applyDriverConfig(JsonConfigRepository $configRepository): void
|
|
{
|
|
$driverSettings = $configRepository->getCategory('drivers');
|
|
|
|
if (empty($driverSettings)) {
|
|
return;
|
|
}
|
|
|
|
// testing 환경에서는 drivers.json의 cache/session/queue 오버라이드 차단 — 테스트 격리 보호
|
|
// (storage/app/settings/drivers.json은 shared 파일이므로 dev의 Redis/DB 드라이버가
|
|
// testing으로 흘러들어가면 dev 캐시를 오염시킴)
|
|
$isTestingEnv = env('APP_ENV') === 'testing';
|
|
|
|
// 캐시 드라이버 설정
|
|
if (! $isTestingEnv && ! empty($driverSettings['cache_driver'])) {
|
|
Config::set('cache.default', $driverSettings['cache_driver']);
|
|
}
|
|
|
|
// 세션 드라이버 설정
|
|
if (! $isTestingEnv && ! empty($driverSettings['session_driver'])) {
|
|
Config::set('session.driver', $driverSettings['session_driver']);
|
|
}
|
|
|
|
// 세션 수명 설정
|
|
if (! $isTestingEnv && ! empty($driverSettings['session_lifetime'])) {
|
|
Config::set('session.lifetime', (int) $driverSettings['session_lifetime']);
|
|
}
|
|
|
|
// 큐 드라이버 설정
|
|
if (! $isTestingEnv && ! empty($driverSettings['queue_driver'])) {
|
|
Config::set('queue.default', $driverSettings['queue_driver']);
|
|
}
|
|
|
|
// Redis 설정 (캐시, 세션, 큐에서 공통 사용)
|
|
$this->applyRedisConfig($driverSettings);
|
|
|
|
// Memcached 설정
|
|
$this->applyMemcachedConfig($driverSettings);
|
|
|
|
// S3 스토리지 설정
|
|
$this->applyS3Config($driverSettings);
|
|
|
|
// 스토리지 드라이버 설정
|
|
if (! empty($driverSettings['storage_driver'])) {
|
|
Config::set('filesystems.default', $driverSettings['storage_driver']);
|
|
}
|
|
|
|
// 웹소켓 설정
|
|
$this->applyWebsocketConfig($driverSettings);
|
|
|
|
// 검색엔진 드라이버 설정
|
|
if (! empty($driverSettings['search_engine_driver'])) {
|
|
Config::set('scout.driver', $driverSettings['search_engine_driver']);
|
|
}
|
|
|
|
// 로그 설정
|
|
$this->applyLogConfig($driverSettings);
|
|
}
|
|
|
|
/**
|
|
* Redis 연결 설정을 적용합니다.
|
|
*/
|
|
private function applyRedisConfig(array $driverSettings): void
|
|
{
|
|
if (! empty($driverSettings['redis_host'])) {
|
|
Config::set('database.redis.default.host', $driverSettings['redis_host']);
|
|
Config::set('database.redis.cache.host', $driverSettings['redis_host']);
|
|
}
|
|
|
|
if (! empty($driverSettings['redis_port'])) {
|
|
Config::set('database.redis.default.port', (int) $driverSettings['redis_port']);
|
|
Config::set('database.redis.cache.port', (int) $driverSettings['redis_port']);
|
|
}
|
|
|
|
if (isset($driverSettings['redis_password']) && $driverSettings['redis_password'] !== '') {
|
|
Config::set('database.redis.default.password', $driverSettings['redis_password']);
|
|
Config::set('database.redis.cache.password', $driverSettings['redis_password']);
|
|
}
|
|
|
|
if (isset($driverSettings['redis_database'])) {
|
|
Config::set('database.redis.default.database', (int) $driverSettings['redis_database']);
|
|
Config::set('database.redis.cache.database', (int) $driverSettings['redis_database']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Memcached 연결 설정을 적용합니다.
|
|
*/
|
|
private function applyMemcachedConfig(array $driverSettings): void
|
|
{
|
|
if (! empty($driverSettings['memcached_host'])) {
|
|
Config::set('cache.stores.memcached.servers.0.host', $driverSettings['memcached_host']);
|
|
}
|
|
|
|
if (! empty($driverSettings['memcached_port'])) {
|
|
Config::set('cache.stores.memcached.servers.0.port', (int) $driverSettings['memcached_port']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* S3 스토리지 설정을 적용합니다.
|
|
*/
|
|
private function applyS3Config(array $driverSettings): void
|
|
{
|
|
if (! empty($driverSettings['s3_bucket'])) {
|
|
Config::set('filesystems.disks.s3.bucket', $driverSettings['s3_bucket']);
|
|
}
|
|
|
|
if (! empty($driverSettings['s3_region'])) {
|
|
Config::set('filesystems.disks.s3.region', $driverSettings['s3_region']);
|
|
}
|
|
|
|
if (! empty($driverSettings['s3_access_key'])) {
|
|
Config::set('filesystems.disks.s3.key', $driverSettings['s3_access_key']);
|
|
}
|
|
|
|
if (! empty($driverSettings['s3_secret_key'])) {
|
|
Config::set('filesystems.disks.s3.secret', $driverSettings['s3_secret_key']);
|
|
}
|
|
|
|
if (! empty($driverSettings['s3_url'])) {
|
|
Config::set('filesystems.disks.s3.url', $driverSettings['s3_url']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 로그 드라이버 설정을 적용합니다.
|
|
*/
|
|
private function applyLogConfig(array $driverSettings): void
|
|
{
|
|
// 로그 드라이버 설정 (single 또는 daily)
|
|
if (! empty($driverSettings['log_driver'])) {
|
|
Config::set('logging.channels.stack.channels', [$driverSettings['log_driver']]);
|
|
}
|
|
|
|
// 로그 레벨 설정
|
|
if (! empty($driverSettings['log_level'])) {
|
|
Config::set('logging.channels.single.level', $driverSettings['log_level']);
|
|
Config::set('logging.channels.daily.level', $driverSettings['log_level']);
|
|
}
|
|
|
|
// daily 드라이버의 로그 보관 일수 설정
|
|
if (! empty($driverSettings['log_days'])) {
|
|
Config::set('logging.channels.daily.days', (int) $driverSettings['log_days']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 웹소켓(Reverb) 설정을 적용합니다.
|
|
*
|
|
* 클라이언트(브라우저)와 서버(백엔드 broadcast HTTP API) endpoint를 분리합니다:
|
|
* - 클라이언트: 외부에서 WebSocket으로 접속할 host/port (예: g7.dev:443)
|
|
* → g7.websocket.client.* 에 저장 → admin.blade.php가 브라우저로 전달
|
|
* - 서버: 백엔드 Pusher SDK가 broadcast HTTP API를 호출할 내부 endpoint (예: 127.0.0.1:8080)
|
|
* → broadcasting.connections.reverb.options.* 에 저장 → Pusher SDK가 사용
|
|
* → reverb.apps.apps.0.options.* 에 저장 → Reverb 서버 자체 설정
|
|
*/
|
|
private function applyWebsocketConfig(array $driverSettings): void
|
|
{
|
|
if (empty($driverSettings['websocket_enabled'])) {
|
|
Config::set('broadcasting.default', 'null');
|
|
// 프론트(admin/app.blade.php)가 @if(broadcasting.connections.reverb.key)로 연결을
|
|
// 결정하므로, OFF 시 key 를 비워 .env REVERB_APP_KEY 가 살아 있어도 브라우저 WebSocket
|
|
// 연결을 차단한다 (전 계층 SSoT — 공개#50). 송신(broadcasting.default='null')만으로는
|
|
// 프론트 직접 연결을 못 막던 빈틈을 폐쇄한다.
|
|
Config::set('broadcasting.connections.reverb.key', '');
|
|
Config::set('g7.websocket.client.host', ''); // 잔재 endpoint 정리 (방어적)
|
|
|
|
return;
|
|
}
|
|
|
|
$appId = $driverSettings['websocket_app_id'] ?? '';
|
|
$appKey = $driverSettings['websocket_app_key'] ?? '';
|
|
$appSecret = $driverSettings['websocket_app_secret'] ?? '';
|
|
|
|
// 클라이언트 endpoint (외부 — 브라우저 접속용)
|
|
$clientHost = $driverSettings['websocket_host'] ?? '';
|
|
$clientPort = (int) ($driverSettings['websocket_port'] ?? 0);
|
|
$clientScheme = $driverSettings['websocket_scheme'] ?? '';
|
|
$verifySsl = isset($driverSettings['websocket_verify_ssl']) ? (bool) $driverSettings['websocket_verify_ssl'] : null;
|
|
|
|
// 서버 endpoint (내부 — 백엔드 broadcast HTTP API용)
|
|
// server 키가 비어있으면 클라이언트 값으로 fallback (단일 호스트 환경 호환)
|
|
$serverHost = $driverSettings['websocket_server_host'] ?? '';
|
|
$serverPort = (int) ($driverSettings['websocket_server_port'] ?? 0);
|
|
$serverScheme = $driverSettings['websocket_server_scheme'] ?? '';
|
|
if (empty($serverHost)) {
|
|
$serverHost = $clientHost;
|
|
}
|
|
if ($serverPort <= 0) {
|
|
$serverPort = $clientPort;
|
|
}
|
|
if (empty($serverScheme)) {
|
|
$serverScheme = $clientScheme;
|
|
}
|
|
$serverUseTLS = $serverScheme === 'https';
|
|
|
|
// 공통: 앱 자격증명
|
|
if (! empty($appKey)) {
|
|
Config::set('broadcasting.connections.reverb.key', $appKey);
|
|
Config::set('reverb.apps.apps.0.key', $appKey);
|
|
}
|
|
if (! empty($appSecret)) {
|
|
Config::set('broadcasting.connections.reverb.secret', $appSecret);
|
|
Config::set('reverb.apps.apps.0.secret', $appSecret);
|
|
}
|
|
if (! empty($appId)) {
|
|
Config::set('broadcasting.connections.reverb.app_id', $appId);
|
|
Config::set('reverb.apps.apps.0.app_id', $appId);
|
|
}
|
|
|
|
// 백엔드 broadcast HTTP API용 endpoint (Pusher SDK가 사용)
|
|
if (! empty($serverHost)) {
|
|
Config::set('broadcasting.connections.reverb.options.host', $serverHost);
|
|
Config::set('reverb.apps.apps.0.options.host', $serverHost);
|
|
}
|
|
if ($serverPort > 0) {
|
|
Config::set('broadcasting.connections.reverb.options.port', $serverPort);
|
|
Config::set('reverb.apps.apps.0.options.port', $serverPort);
|
|
}
|
|
if (! empty($serverScheme)) {
|
|
Config::set('broadcasting.connections.reverb.options.scheme', $serverScheme);
|
|
Config::set('broadcasting.connections.reverb.options.useTLS', $serverUseTLS);
|
|
Config::set('reverb.apps.apps.0.options.scheme', $serverScheme);
|
|
Config::set('reverb.apps.apps.0.options.useTLS', $serverUseTLS);
|
|
}
|
|
if ($verifySsl !== null) {
|
|
Config::set('broadcasting.connections.reverb.client_options.verify', $verifySsl);
|
|
}
|
|
|
|
// 클라이언트(브라우저) 접속용 endpoint — Blade에서 읽어 G7Core에 전달
|
|
if (! empty($clientHost)) {
|
|
Config::set('g7.websocket.client.host', $clientHost);
|
|
}
|
|
if ($clientPort > 0) {
|
|
Config::set('g7.websocket.client.port', $clientPort);
|
|
}
|
|
if (! empty($clientScheme)) {
|
|
Config::set('g7.websocket.client.scheme', $clientScheme);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 코어 업데이트 설정을 Laravel config에 적용합니다.
|
|
*
|
|
* core_update.json의 값으로 config('app.update.github_url')과
|
|
* config('app.update.github_token')을 오버라이드합니다.
|
|
*/
|
|
private function applyCoreUpdateConfig(JsonConfigRepository $configRepository): void
|
|
{
|
|
$settings = $configRepository->getCategory('core_update');
|
|
|
|
if (empty($settings)) {
|
|
return;
|
|
}
|
|
|
|
if (! empty($settings['github_url'])) {
|
|
Config::set('app.update.github_url', $settings['github_url']);
|
|
}
|
|
|
|
if (! empty($settings['github_token'])) {
|
|
Config::set('app.update.github_token', $settings['github_token']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GeoIP 설정을 Laravel config에 적용합니다.
|
|
*
|
|
* cache.json의 geoip_* 값으로 config('geoip.*')을 오버라이드합니다.
|
|
* 마스터 스위치(geoip.feature_enabled)가 곧 config('geoip.enabled')의
|
|
* 런타임 값이 됩니다. env('GEOIP_ENABLED')는 fallback 역할.
|
|
*
|
|
* @param JsonConfigRepository $configRepository 설정 저장소
|
|
*/
|
|
private function applyGeoIpConfig(JsonConfigRepository $configRepository): void
|
|
{
|
|
$settings = $configRepository->getCategory('geoip');
|
|
|
|
if (empty($settings)) {
|
|
return;
|
|
}
|
|
|
|
if (isset($settings['feature_enabled'])) {
|
|
Config::set('geoip.enabled', (bool) $settings['feature_enabled']);
|
|
}
|
|
|
|
if (! empty($settings['license_key'])) {
|
|
Config::set('geoip.license_key', $settings['license_key']);
|
|
}
|
|
|
|
if (isset($settings['auto_update_enabled'])) {
|
|
Config::set('geoip.auto_update_enabled', (bool) $settings['auto_update_enabled']);
|
|
}
|
|
|
|
if (! empty($settings['last_updated_at'])) {
|
|
Config::set('geoip.last_updated_at', $settings['last_updated_at']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 본인인증(IDV) 설정을 Laravel config 에 적용합니다.
|
|
*
|
|
* IdentityVerificationManager / MailIdentityProvider / InicisIdentityProvider 등이
|
|
* config('settings.identity.*') dot-path 로 직접 read 하므로 이 경로로 주입합니다.
|
|
*
|
|
* 영향 항목 (admin UI 환경설정 > 본인인증):
|
|
* - default_provider (기본 프로바이더)
|
|
* - purpose_providers.{purpose} (목적별 기본 프로바이더)
|
|
* - challenge_ttl_minutes (코드 유효시간)
|
|
* - max_attempts (최대 시도 횟수)
|
|
*
|
|
* @param JsonConfigRepository $configRepository 설정 저장소
|
|
*/
|
|
private function applyIdentityConfig(JsonConfigRepository $configRepository): void
|
|
{
|
|
$settings = $configRepository->getCategory('identity');
|
|
|
|
if (empty($settings)) {
|
|
return;
|
|
}
|
|
|
|
Config::set('settings.identity', $settings);
|
|
}
|
|
|
|
/**
|
|
* 캐시 설정을 Laravel config에 적용합니다.
|
|
*/
|
|
private function applyCacheConfig(JsonConfigRepository $configRepository): void
|
|
{
|
|
$cacheSettings = $configRepository->getCategory('cache');
|
|
|
|
if (empty($cacheSettings)) {
|
|
return;
|
|
}
|
|
|
|
if (! empty($cacheSettings['driver'])) {
|
|
Config::set('cache.default', $cacheSettings['driver']);
|
|
}
|
|
|
|
if (! empty($cacheSettings['prefix'])) {
|
|
Config::set('cache.prefix', $cacheSettings['prefix']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 업로드 설정을 Laravel config에 적용합니다.
|
|
*/
|
|
private function applyUploadConfig(JsonConfigRepository $configRepository): void
|
|
{
|
|
$uploadSettings = $configRepository->getCategory('upload');
|
|
|
|
if (empty($uploadSettings)) {
|
|
return;
|
|
}
|
|
|
|
if (! empty($uploadSettings['disk'])) {
|
|
Config::set('filesystems.default', $uploadSettings['disk']);
|
|
}
|
|
|
|
// 관리자 설정은 MB, config/attachment.* 는 KB — 변환은 이 지점 단 한 곳에서만 수행한다.
|
|
// (기존에는 존재하지 않는 키 `max_size` 를 읽어 설정이 어디에도 반영되지 않았다)
|
|
if (! empty($uploadSettings['max_file_size'])) {
|
|
Config::set('attachment.max_file_size', (int) $uploadSettings['max_file_size'] * 1024);
|
|
}
|
|
|
|
// 화면은 콤마 문자열을 보내고 API 직접 호출은 배열을 보낸다. 배열만 받아들이면
|
|
// 문자열로 저장된 설정이 조용히 버려져 관리자가 확장자를 바꿔도 반영되지 않는다.
|
|
if (isset($uploadSettings['allowed_extensions'])) {
|
|
Config::set(
|
|
'attachment.allowed_extensions',
|
|
AllowedExtensions::normalize($uploadSettings['allowed_extensions'])
|
|
);
|
|
}
|
|
|
|
// 이미지 축소 한계값·품질 — 픽셀/퍼센트라 단위 변환이 없다.
|
|
// 미설정(빈 값)은 "축소하지 않음" 이므로 그대로 넘겨 ImageResizer 가 판정하게 둔다.
|
|
foreach (['image_max_width', 'image_max_height', 'image_quality'] as $key) {
|
|
if (isset($uploadSettings[$key]) && $uploadSettings[$key] !== '') {
|
|
Config::set("attachment.{$key}", (int) $uploadSettings[$key]);
|
|
}
|
|
}
|
|
}
|
|
}
|