Files
Gnuboard7/app/Http/Requests/Admin/UpdatePluginSettingsRequest.php
T
HeuJung cfc2caba1b feat(ckeditor5,board,ecommerce,page,core,admin_basic): 미참조 에디터 업로드 이미지 정리 신설 + 업로드 고아 동종 결함 7건 전수 해소
공개 (@Tuwasduliebst 제보) 대응.

- ckeditor5: 미참조 이미지 정리 엔진 신설 — 참조 판정(hash + 파일명 2토큰,
 filter_reference_sources 훅으로 확장 콘텐츠 등록) + 정리 커맨드(기본 꺼짐 옵트인,
 3중 게이트) + 업로드 관리 화면(권한 분리·참조 배지·개별/선택 삭제) + 기설치본
 설정 백필 upgrade step
- board: 임시 첨부 GC(상시) + 소프트삭제 첨부 영구 정리(옵트인) + 설정 UI,
 저장 payload 누락(F4)·빈 temp 디렉토리 잔존(F5) 수정
- ecommerce: 임시 상품 이미지 GC + 카테고리 삭제 시 이미지 파일 미삭제(S4) 수정
- page: 임시 첨부 GC
- 코어: 고아 첨부 정리 커맨드(옵트인, site_logo 보호) + 사이트 로고 교체 시
 구 로고 재편입 차단(S6) + DB 백업 미구현 501 정직화(B3) + 플러그인 관리자
 메뉴 설치·업데이트 자동 동기화(F1·F7)
- 동반: 플러그인 설정 검증 오류 필드명 라벨화(F3), Toggle 노드 type 정정
 수평 전개(marketing 1.0.2 bump·gdpr·ecommerce), ja 언어팩 4종, API 문서,
 hooks.md, 시나리오 매니페스트 8종, Playwright E2E 4종
2026-08-17 02:44:54 +09:00

202 lines
7.1 KiB
PHP

<?php
namespace App\Http\Requests\Admin;
use App\Extension\HookManager;
use App\Extension\PluginManager;
use App\Services\DriverRegistryService;
use Illuminate\Foundation\Http\FormRequest;
/**
* 플러그인 설정 업데이트 요청 검증
*
* 플러그인의 설정 스키마를 기반으로 동적으로 유효성 검사 규칙을 생성합니다.
*/
class UpdatePluginSettingsRequest extends FormRequest
{
/**
* 사용자가 이 요청을 수행할 권한이 있는지 확인합니다.
*
* 권한 체크는 라우트의 permission 미들웨어에서 수행됩니다.
*
* @return bool 항상 true
*/
public function authorize(): bool
{
return true;
}
/**
* 요청에 적용할 검증 규칙을 반환합니다.
*
* 플러그인의 설정 스키마를 기반으로 동적으로 규칙을 생성합니다.
*
* @return array 검증 규칙 배열
*/
public function rules(): array
{
$identifier = $this->route('identifier');
$pluginManager = app(PluginManager::class);
$plugin = $pluginManager->getPlugin($identifier);
if (! $plugin) {
return [];
}
$schema = $plugin->getSettingsSchema();
$rules = [];
foreach ($schema as $field => $config) {
$fieldRules = [];
// 필수 여부
if ($config['required'] ?? false) {
$fieldRules[] = 'required';
} else {
$fieldRules[] = 'nullable';
}
// 타입별 규칙 추가
$type = $config['type'] ?? 'string';
switch ($type) {
case 'string':
$fieldRules[] = 'string';
if (isset($config['max'])) {
$fieldRules[] = 'max:'.$config['max'];
}
if (isset($config['min'])) {
$fieldRules[] = 'min:'.$config['min'];
}
break;
case 'integer':
$fieldRules[] = 'integer';
if (isset($config['min'])) {
$fieldRules[] = 'min:'.$config['min'];
}
if (isset($config['max'])) {
$fieldRules[] = 'max:'.$config['max'];
}
break;
case 'boolean':
$fieldRules[] = 'boolean';
break;
case 'enum':
$options = $config['options'] ?? [];
if (! empty($options)) {
$fieldRules[] = 'in:'.implode(',', $options);
}
break;
case 'url':
$fieldRules[] = 'url';
break;
case 'email':
$fieldRules[] = 'email';
break;
case 'array':
$fieldRules[] = 'array';
break;
}
$rules[$field] = $fieldRules;
}
// 공개 자산 디스크는 스키마 타입만으로 검증할 수 없다 — 선택지가 코어 3종 +
// 플러그인이 훅으로 등록한 디스크라 런타임에만 확정되기 때문이다.
// PluginSettingsController 가 카탈로그를 부착하는 게이트(스키마에 이 키를 선언한
// 플러그인)와 동일한 조건에서 검증도 걸어, "선택지를 내려준 표면" 과 "값을 받는
// 표면" 의 강도를 맞춘다. 이렇게 두면 이 키를 선언하는 플러그인은 각자 훅을
// 구독하지 않아도 코어 환경설정과 같은 422 를 얻는다.
if (array_key_exists('public_asset_disk', $schema)) {
$rules['public_asset_disk'][] = function (string $attribute, mixed $value, \Closure $fail): void {
if ($value === null || $value === '') {
return;
}
if (! app(DriverRegistryService::class)->isDriverAvailable('public_asset', $value)) {
$fail(__('validation.settings.public_asset_disk_invalid'));
}
};
}
// 표준 이름(`core.{대상}.{동작}_validation_rules`)으로 발행하되, 이미 공개돼 구독 중일 수 있는
// 구 이름(`core.plugin_settings.update_rules`)도 함께 발행한다 — 구 이름을 구독하는 제3자
// 확장이 조용히 멈추지 않도록 한다. 구 이름은 구독자가 있을 때만 1회 경고가 남는다.
//
// 3번째 인자로 이번 요청의 입력값을 함께 넘긴다. 확장이 "현재 입력한 모드에 따라
// 다른 필드를 필수로 만드는" 조건부 규칙을 만들려면 입력값이 필요한데, 그것이 없어
// 확장이 request() 를 직접 들여다보던 것을 없애기 위함이다.
// 기존 2인자 구독자는 그대로 동작한다(초과 인자는 무시된다).
return HookManager::applyFiltersWithLegacyName(
'core.plugin_settings.update_validation_rules',
'core.plugin_settings.update_rules',
$rules,
$identifier,
$this->all(),
);
}
/**
* 검증 에러 메시지를 반환합니다.
*
* @return array 에러 메시지 배열
*/
public function messages(): array
{
return [
'required' => __('validation.required'),
'string' => __('validation.string'),
'integer' => __('validation.integer'),
'boolean' => __('validation.boolean'),
'in' => __('validation.in'),
'url' => __('validation.url'),
'email' => __('validation.email'),
'array' => __('validation.array'),
'max' => __('validation.max'),
'min' => __('validation.min'),
];
}
/**
* 검증 오류에 쓸 필드 표시명을 반환합니다.
*
* 설정 스키마의 `label` 을 그대로 씁니다. 이 값이 없으면 Laravel 이 원시 키를 풀어쓴
* 이름("unused image retention days")을 그대로 노출하는데, 같은 화면의 입력 라벨은
* 한국어인 상태라 운영자에게는 어느 항목을 고치라는 말인지 닿지 않습니다.
*
* @return array<string, string> 필드 => 표시명
*/
public function attributes(): array
{
$plugin = app(PluginManager::class)->getPlugin($this->route('identifier'));
if (! $plugin) {
return [];
}
$locale = app()->getLocale();
$fallback = config('app.fallback_locale', 'en');
$attributes = [];
foreach ($plugin->getSettingsSchema() as $field => $config) {
$label = $config['label'] ?? null;
if (is_array($label)) {
$label = $label[$locale] ?? $label[$fallback] ?? reset($label);
}
if (is_string($label) && $label !== '') {
$attributes[$field] = $label;
}
}
return $attributes;
}
}