v7.0.0-beta.2 release

This commit is contained in:
HeuJung committed 2026-04-20 20:37:49 +09:00
1 parent 6595fd0eb5
commit fa8eba7a0d
1165 files changed
+107118 -762127

No files matched your search

@@ -195,6 +195,10 @@ class UpdateLayoutContentRequest extends FormRequest
'content.transition_overlay.spinner' => ['nullable', 'array'],
'content.transition_overlay.spinner.component' => ['nullable', 'string', 'max:100'],
'content.transition_overlay.spinner.text' => ['nullable', 'string', 'max:200'],
// wait_for: spinner 가 명시된 progressive/blocking 데이터소스 fetch 완료까지 표시되도록 가드
// background/websocket 데이터소스는 의도상 사용자 차단 불가 → withValidator 에서 cross-field 검증
'content.transition_overlay.wait_for' => ['nullable', 'array'],
'content.transition_overlay.wait_for.*' => ['string', 'max:100'],
];
// extends 레이아웃 또는 Base 레이아웃이 아닌 경우에만 endpoint, components 필수
@@ -225,6 +229,54 @@ class UpdateLayoutContentRequest extends FormRequest
return HookManager::applyFilters('core.layout.update_content_validation_rules', $rules, $this);
}
/**
* Cross-field 검증 — transition_overlay.wait_for 가 가리키는 데이터소스의 type/loading_strategy 검증
*
* wait_for 는 spinner 가 fetch 완료까지 표시되어야 할 데이터소스 ID 목록이지만,
* 의미상 사용자를 차단할 수 없는 background/websocket 데이터소스는 사전에 차단한다.
*/
public function withValidator(\Illuminate\Contracts\Validation\Validator $validator): void
{
$validator->after(function (\Illuminate\Contracts\Validation\Validator $v): void {
$waitFor = $this->input('content.transition_overlay.wait_for');
if (! is_array($waitFor) || empty($waitFor)) {
return;
}
$dataSources = $this->input('content.data_sources');
if (! is_array($dataSources)) {
return;
}
$byId = [];
foreach ($dataSources as $source) {
if (is_array($source) && isset($source['id'])) {
$byId[$source['id']] = $source;
}
}
foreach ($waitFor as $index => $id) {
if (! is_string($id) || ! isset($byId[$id])) {
continue; // 미존재 ID 는 엔진에서 자동 무시됨 (가드 무시)
}
$source = $byId[$id];
$type = $source['type'] ?? 'api';
$strategy = $source['loading_strategy'] ?? 'progressive';
if ($type === 'websocket') {
$v->errors()->add(
"content.transition_overlay.wait_for.$index",
__('validation.layout.transition_overlay.wait_for.websocket', ['id' => $id])
);
} elseif ($strategy === 'background') {
$v->errors()->add(
"content.transition_overlay.wait_for.$index",
__('validation.layout.transition_overlay.wait_for.background', ['id' => $id])
);
}
}
});
}
/**
* 검증 오류 메시지 커스터마이징
*
@@ -1,50 +0,0 @@
<?php
namespace App\Http\Requests\MailSendLog;
use App\Models\MailSendLog;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
/**
* 메일 발송 이력 일괄 삭제 요청을 검증합니다.
*/
class MailSendLogBulkDeleteRequest extends FormRequest
{
/**
* 요청 권한을 확인합니다.
*
* @return bool 항상 true (권한은 permission 미들웨어에서 처리)
*/
public function authorize(): bool
{
return true;
}
/**
* 유효성 검증 규칙을 반환합니다.
*
* @return array<string, mixed> 검증 규칙
*/
public function rules(): array
{
return [
'ids' => ['required', 'array', 'min:1'],
'ids.*' => ['required', 'integer', Rule::exists(MailSendLog::class, 'id')],
];
}
/**
* 사용자 정의 유효성 검증 메시지를 반환합니다.
*
* @return array<string, string> 메시지
*/
public function messages(): array
{
return [
'ids.required' => __('mail_send_log.validation.ids_required'),
'ids.min' => __('mail_send_log.validation.ids_min'),
'ids.*.exists' => __('mail_send_log.validation.id_not_found'),
];
}
}
@@ -1,31 +0,0 @@
<?php
namespace App\Http\Requests\MailSendLog;
use Illuminate\Foundation\Http\FormRequest;
/**
* 메일 발송 이력 삭제 요청을 검증합니다.
*/
class MailSendLogDeleteRequest extends FormRequest
{
/**
* 요청 권한을 확인합니다.
*
* @return bool 항상 true (권한은 permission 미들웨어에서 처리)
*/
public function authorize(): bool
{
return true;
}
/**
* 유효성 검증 규칙을 반환합니다.
*
* @return array<string, mixed> 검증 규칙
*/
public function rules(): array
{
return [];
}
}
@@ -1,67 +0,0 @@
<?php
namespace App\Http\Requests\MailSendLog;
use App\Enums\ExtensionOwnerType;
use App\Enums\MailSendStatus;
use App\Extension\HookManager;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
/**
* 메일 발송 이력 목록 조회 요청을 검증합니다.
*/
class MailSendLogIndexRequest extends FormRequest
{
/**
* 요청 권한을 확인합니다.
*
* @return bool 항상 true (권한은 permission 미들웨어에서 처리)
*/
public function authorize(): bool
{
return true;
}
/**
* 유효성 검증 규칙을 반환합니다.
*
* @return array<string, mixed> 검증 규칙
*/
public function rules(): array
{
$rules = [
'extension_type' => ['nullable', 'array'],
'extension_type.*' => ['string', Rule::in(ExtensionOwnerType::values())],
'extension_identifier' => ['nullable', 'array'],
'extension_identifier.*' => ['string', 'max:100'],
'template_type' => ['nullable', 'string', 'max:100'],
'status' => ['nullable', 'array'],
'status.*' => ['string', Rule::in(array_column(MailSendStatus::cases(), 'value'))],
'search' => ['nullable', 'string', 'max:255'],
'search_type' => ['nullable', 'string', Rule::in(['all', 'recipient_email', 'recipient_name', 'subject'])],
'date_from' => ['nullable', 'date'],
'date_to' => ['nullable', 'date', 'after_or_equal:date_from'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
'sort_by' => ['nullable', 'string', Rule::in(['sent_at', 'recipient_email', 'subject', 'extension_type', 'status'])],
'sort_order' => ['nullable', 'string', Rule::in(['asc', 'desc'])],
];
return HookManager::applyFilters('core.mail_send_log.index_validation_rules', $rules, $this);
}
/**
* 사용자 정의 유효성 검증 메시지를 반환합니다.
*
* @return array<string, string> 메시지
*/
public function messages(): array
{
return [
'status.in' => __('mail_send_log.validation.status_invalid'),
'date_to.after_or_equal' => __('mail_send_log.validation.date_range_invalid'),
'per_page.min' => __('mail_send_log.validation.per_page_min'),
'per_page.max' => __('mail_send_log.validation.per_page_max'),
];
}
}
@@ -1,54 +0,0 @@
<?php
namespace App\Http\Requests\MailTemplate;
use App\Extension\HookManager;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
/**
* 메일 템플릿 목록 조회 요청을 검증합니다.
*/
class MailTemplateIndexRequest extends FormRequest
{
/**
* 요청 권한을 확인합니다.
*
* @return bool 항상 true (권한은 permission 미들웨어에서 처리)
*/
public function authorize(): bool
{
return true;
}
/**
* 유효성 검증 규칙을 반환합니다.
*
* @return array<string, mixed> 검증 규칙
*/
public function rules(): array
{
$rules = [
'search' => ['nullable', 'string', 'max:255'],
'search_type' => ['nullable', 'string', Rule::in(['all', 'subject', 'body'])],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
'sort_by' => ['nullable', 'string', Rule::in(['id', 'type', 'is_active', 'created_at', 'updated_at'])],
'sort_order' => ['nullable', 'string', Rule::in(['asc', 'desc'])],
];
return HookManager::applyFilters('core.mail_template.index_validation_rules', $rules, $this);
}
/**
* 사용자 정의 유효성 검증 메시지를 반환합니다.
*
* @return array<string, string> 메시지
*/
public function messages(): array
{
return [
'per_page.min' => __('mail_template.validation.per_page_min'),
'per_page.max' => __('mail_template.validation.per_page_max'),
];
}
}
@@ -1,52 +0,0 @@
<?php
namespace App\Http\Requests\MailTemplate;
use App\Extension\HookManager;
use Illuminate\Foundation\Http\FormRequest;
/**
* 메일 템플릿 미리보기 요청을 검증합니다.
*/
class MailTemplatePreviewRequest extends FormRequest
{
/**
* 요청 권한을 확인합니다.
*
* @return bool 항상 true (권한은 permission 미들웨어에서 처리)
*/
public function authorize(): bool
{
return true;
}
/**
* 유효성 검증 규칙을 반환합니다.
*
* @return array<string, mixed> 검증 규칙
*/
public function rules(): array
{
$rules = [
'subject' => ['required', 'string', 'max:500'],
'body' => ['required', 'string', 'max:65535'],
'variables' => ['nullable', 'array'],
'variables.*.key' => ['required_with:variables', 'string', 'max:100'],
];
return HookManager::applyFilters('core.mail_template.preview_validation_rules', $rules, $this);
}
/**
* 사용자 정의 유효성 검증 메시지를 반환합니다.
*
* @return array<string, string> 메시지
*/
public function messages(): array
{
return [
'subject.required' => __('mail_template.validation.subject_required'),
'body.required' => __('mail_template.validation.body_required'),
];
}
}
@@ -1,52 +0,0 @@
<?php
namespace App\Http\Requests\MailTemplate;
use App\Extension\HookManager;
use App\Rules\LocaleRequiredTranslatable;
use Illuminate\Foundation\Http\FormRequest;
/**
* 메일 템플릿 수정 요청을 검증합니다.
*/
class UpdateMailTemplateRequest extends FormRequest
{
/**
* 요청 권한을 확인합니다.
*
* @return bool 항상 true (권한은 permission 미들웨어에서 처리)
*/
public function authorize(): bool
{
return true;
}
/**
* 유효성 검증 규칙을 반환합니다.
*
* @return array<string, mixed> 검증 규칙
*/
public function rules(): array
{
$rules = [
'subject' => ['required', 'array', new LocaleRequiredTranslatable(maxLength: 500)],
'body' => ['required', 'array', new LocaleRequiredTranslatable(maxLength: 65535)],
'is_active' => ['sometimes', 'boolean'],
];
return HookManager::applyFilters('core.mail_template.update_validation_rules', $rules, $this);
}
/**
* 사용자 정의 유효성 검증 메시지를 반환합니다.
*
* @return array<string, string> 메시지
*/
public function messages(): array
{
return [
'subject.required' => __('mail_template.validation.subject_required'),
'body.required' => __('mail_template.validation.body_required'),
];
}
}
@@ -25,6 +25,7 @@ class InstallModuleRequest extends FormRequest
{
$rules = [
'module_name' => ['required', 'string', 'max:255', new ValidExtensionIdentifier],
'vendor_mode' => ['nullable', 'string', 'in:auto,composer,bundled'],
];
// 모듈/플러그인이 validation rules를 동적으로 추가할 수 있도록 훅 제공
@@ -0,0 +1,53 @@
<?php
namespace App\Http\Requests\Module;
use App\Extension\HookManager;
use Illuminate\Foundation\Http\FormRequest;
/**
* 모듈 업데이트 실행 요청 검증
*
* layout_strategy: 레이아웃 전략 (overwrite 또는 keep)
* vendor_mode: Vendor 설치 모드 (auto, composer, bundled)
*/
class PerformModuleUpdateRequest extends FormRequest
{
/**
* 사용자가 이 요청을 수행할 권한이 있는지 확인
*
* 권한 체크는 라우트의 permission 미들웨어에서 수행됩니다.
*/
public function authorize(): bool
{
return true;
}
/**
* 요청에 적용할 검증 규칙
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$rules = [
'layout_strategy' => ['nullable', 'string', 'in:overwrite,keep'],
'vendor_mode' => ['nullable', 'string', 'in:auto,composer,bundled'],
];
return HookManager::applyFilters('core.module.perform_update_rules', $rules);
}
/**
* 검증 오류 메시지 커스터마이징
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'layout_strategy.in' => __('modules.errors.invalid_layout_strategy'),
'vendor_mode.in' => __('modules.errors.invalid_vendor_mode'),
];
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests\Notification;
use App\Extension\HookManager;
use Illuminate\Foundation\Http\FormRequest;
class NotificationBatchReadRequest extends FormRequest
{
/**
* 권한 확인 (미들웨어에서 처리).
*/
public function authorize(): bool
{
return true;
}
/**
* 검증 규칙을 반환합니다.
*/
public function rules(): array
{
$rules = [
'ids' => ['required', 'array', 'min:1', 'max:100'],
'ids.*' => ['required', 'string'],
];
return HookManager::applyFilters(
'core.notification.filter_batch_read_rules',
$rules
);
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests\Notification;
use App\Extension\HookManager;
use Illuminate\Foundation\Http\FormRequest;
class NotificationIndexRequest extends FormRequest
{
/**
* 권한 확인 (미들웨어에서 처리).
*/
public function authorize(): bool
{
return true;
}
/**
* 검증 규칙을 반환합니다.
*/
public function rules(): array
{
$rules = [
'read' => ['nullable', 'string', 'in:unread,read,all'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
'page' => ['nullable', 'integer', 'min:1'],
'sort_order' => ['nullable', 'string', 'in:asc,desc'],
];
return HookManager::applyFilters(
'core.notification.filter_index_rules',
$rules
);
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Http\Requests\NotificationDefinition;
use App\Extension\HookManager;
use Illuminate\Foundation\Http\FormRequest;
class NotificationDefinitionIndexRequest extends FormRequest
{
/**
* 권한 확인 (미들웨어에서 처리).
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* 검증 규칙을 반환합니다.
*
* @return array
*/
public function rules(): array
{
$rules = [
'search' => ['nullable', 'string', 'max:255'],
'extension_type' => ['nullable', 'string', 'in:core,module,plugin'],
'extension_identifier' => ['nullable', 'string', 'max:100'],
'channel' => ['nullable', 'string', 'max:50'],
'is_active' => ['nullable', 'boolean'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
'sort_by' => ['nullable', 'string', 'in:id,type,extension_type,is_active,created_at,updated_at'],
'sort_order' => ['nullable', 'string', 'in:asc,desc'],
];
return HookManager::applyFilters(
'core.notification_definition.filter_index_rules',
$rules
);
}
/**
* 검증 메시지를 반환합니다.
*
* @return array
*/
public function messages(): array
{
return [
'per_page.min' => __('validation.min.numeric', ['attribute' => 'per_page', 'min' => 1]),
'per_page.max' => __('validation.max.numeric', ['attribute' => 'per_page', 'max' => 100]),
];
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Http\Requests\NotificationDefinition;
use App\Extension\HookManager;
use Illuminate\Foundation\Http\FormRequest;
class UpdateNotificationDefinitionRequest extends FormRequest
{
/**
* 권한 확인 (미들웨어에서 처리).
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* 검증 규칙을 반환합니다.
*
* @return array
*/
public function rules(): array
{
$rules = [
'channels' => ['sometimes', 'array', 'min:1'],
'channels.*' => ['string', 'max:50'],
'hooks' => ['sometimes', 'array'],
'hooks.*' => ['string', 'max:255'],
'is_active' => ['sometimes', 'boolean'],
];
return HookManager::applyFilters(
'core.notification_definition.filter_update_rules',
$rules,
$this->route('definition')
);
}
}
@@ -0,0 +1,44 @@
<?php
namespace App\Http\Requests\NotificationLog;
use Illuminate\Foundation\Http\FormRequest;
class NotificationLogBulkDeleteRequest extends FormRequest
{
/**
* 권한 확인 (미들웨어에서 처리).
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* 검증 규칙을 반환합니다.
*
* @return array
*/
public function rules(): array
{
return [
'ids' => ['required', 'array', 'min:1'],
'ids.*' => ['integer'],
];
}
/**
* 검증 메시지를 반환합니다.
*
* @return array
*/
public function messages(): array
{
return [
'ids.required' => __('notification_log.no_items_selected'),
'ids.min' => __('notification_log.no_items_selected'),
];
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\NotificationLog;
use Illuminate\Foundation\Http\FormRequest;
class NotificationLogDeleteRequest extends FormRequest
{
/**
* 권한 확인 (미들웨어에서 처리).
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* 검증 규칙을 반환합니다.
*
* @return array
*/
public function rules(): array
{
return [];
}
}
@@ -0,0 +1,44 @@
<?php
namespace App\Http\Requests\NotificationLog;
use App\Enums\NotificationLogStatus;
use App\Extension\HookManager;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class NotificationLogIndexRequest extends FormRequest
{
/**
* 권한 확인 (미들웨어에서 처리).
*/
public function authorize(): bool
{
return true;
}
/**
* 검증 규칙을 반환합니다.
*/
public function rules(): array
{
$rules = [
'sender_user_id' => ['nullable', 'integer', Rule::exists(User::class, 'id')],
'recipient_user_id' => ['nullable', 'integer', Rule::exists(User::class, 'id')],
'search' => ['nullable', 'string', 'max:255'],
'channel' => ['nullable', 'string', 'max:50'],
'notification_type' => ['nullable', 'string', 'max:100'],
'extension_type' => ['nullable', 'string', 'in:core,module,plugin'],
'status' => ['nullable', 'string', Rule::enum(NotificationLogStatus::class)],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
'sort_by' => ['nullable', 'string', 'in:id,channel,notification_type,status,sent_at,created_at'],
'sort_order' => ['nullable', 'string', 'in:asc,desc'],
];
return HookManager::applyFilters(
'core.notification_log.filter_index_rules',
$rules
);
}
}
@@ -0,0 +1,50 @@
<?php
namespace App\Http\Requests\NotificationTemplate;
use App\Models\NotificationDefinition;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class PreviewNotificationTemplateRequest extends FormRequest
{
/**
* 권한 확인 (미들웨어에서 처리).
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* 검증 규칙을 반환합니다.
*
* @return array
*/
public function rules(): array
{
return [
'definition_id' => ['required', 'integer', Rule::exists(NotificationDefinition::class, 'id')],
'subject' => ['required', 'array'],
'body' => ['required', 'array'],
'locale' => ['sometimes', 'string', 'max:10'],
];
}
/**
* 검증 메시지를 반환합니다.
*
* @return array
*/
public function messages(): array
{
return [
'definition_id.required' => __('validation.required', ['attribute' => __('notification.definition')]),
'definition_id.exists' => __('validation.exists', ['attribute' => __('notification.definition')]),
'subject.required' => __('validation.required', ['attribute' => __('notification.subject')]),
'body.required' => __('validation.required', ['attribute' => __('notification.body')]),
];
}
}
@@ -0,0 +1,59 @@
<?php
namespace App\Http\Requests\NotificationTemplate;
use App\Extension\HookManager;
use App\Rules\LocaleRequiredTranslatable;
use Illuminate\Foundation\Http\FormRequest;
class UpdateNotificationTemplateRequest extends FormRequest
{
/**
* 권한 확인 (미들웨어에서 처리).
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* 검증 규칙을 반환합니다.
*
* @return array
*/
public function rules(): array
{
$rules = [
'subject' => ['required', 'array', new LocaleRequiredTranslatable(maxLength: 500)],
'body' => ['required', 'array', new LocaleRequiredTranslatable(maxLength: 65535)],
'click_url' => ['nullable', 'string', 'max:500'],
'recipients' => ['sometimes', 'nullable', 'array'],
'recipients.*.type' => ['required', 'string', 'in:trigger_user,related_user,role,specific_users'],
'recipients.*.value' => ['nullable'],
'recipients.*.relation' => ['nullable', 'string', 'max:100'],
'recipients.*.exclude_trigger_user' => ['nullable', 'boolean'],
'is_active' => ['sometimes', 'boolean'],
];
return HookManager::applyFilters(
'core.notification_template.filter_update_rules',
$rules,
$this->route('template')
);
}
/**
* 검증 메시지를 반환합니다.
*
* @return array
*/
public function messages(): array
{
return [
'subject.required' => __('validation.required', ['attribute' => __('notification.subject')]),
'body.required' => __('validation.required', ['attribute' => __('notification.body')]),
];
}
}
@@ -32,6 +32,7 @@ class InstallPluginRequest extends FormRequest
{
$rules = [
'plugin_name' => ['required', 'string', 'max:255', new ValidExtensionIdentifier],
'vendor_mode' => ['nullable', 'string', 'in:auto,composer,bundled'],
];
// 모듈/플러그인이 validation rules를 동적으로 추가할 수 있도록 훅 제공
@@ -0,0 +1,53 @@
<?php
namespace App\Http\Requests\Plugin;
use App\Extension\HookManager;
use Illuminate\Foundation\Http\FormRequest;
/**
* 플러그인 업데이트 실행 요청 검증
*
* layout_strategy: 레이아웃 전략 (overwrite 또는 keep)
* vendor_mode: Vendor 설치 모드 (auto, composer, bundled)
*/
class PerformPluginUpdateRequest extends FormRequest
{
/**
* 사용자가 이 요청을 수행할 권한이 있는지 확인
*
* 권한 체크는 라우트의 permission 미들웨어에서 수행됩니다.
*/
public function authorize(): bool
{
return true;
}
/**
* 요청에 적용할 검증 규칙
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$rules = [
'layout_strategy' => ['nullable', 'string', 'in:overwrite,keep'],
'vendor_mode' => ['nullable', 'string', 'in:auto,composer,bundled'],
];
return HookManager::applyFilters('core.plugin.perform_update_rules', $rules);
}
/**
* 검증 오류 메시지 커스터마이징
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'layout_strategy.in' => __('plugins.errors.invalid_layout_strategy'),
'vendor_mode.in' => __('plugins.errors.invalid_vendor_mode'),
];
}
}
+2 -1
View File
@@ -30,7 +30,7 @@ class SearchRequest extends FormRequest
{
$rules = [
// 기본 검색 파라미터
'q' => ['nullable', 'string', 'max:200'],
'q' => ['nullable', 'string', 'min:2', 'max:200'],
'type' => ['nullable', 'string'],
'sort' => ['nullable', 'string'],
'page' => ['nullable', 'integer', 'min:1'],
@@ -50,6 +50,7 @@ class SearchRequest extends FormRequest
public function messages(): array
{
return [
'q.min' => __('search.validation.q_min'),
'q.max' => __('search.validation.q_max'),
'page.integer' => __('search.validation.page_integer'),
'page.min' => __('search.validation.page_min'),
@@ -4,6 +4,7 @@ namespace App\Http\Requests\Settings;
use App\Extension\HookManager;
use App\Models\Attachment;
use App\Search\Engines\DatabaseFulltextEngine;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
@@ -98,6 +99,41 @@ class SaveSettingsRequest extends FormRequest
]),
]);
}
// GeoIP 토글 필드: 신규 필드라 초기값이 settings에 없을 수 있음.
// Toggle 컴포넌트가 undefined → true 전환 시 비표준 타입을 보낼 수 있으므로
// boolean으로 명시 캐스팅하여 Laravel boolean 검증 호환 보장.
$this->castAdvancedBooleanFields([
'advanced.geoip_enabled',
'advanced.geoip_auto_update_enabled',
]);
}
/**
* advanced 탭의 boolean 필드를 PHP boolean으로 캐스팅합니다.
*
* Toggle 컴포넌트가 초기값 미존재(undefined) 상태에서 전환 시
* 비표준 타입(문자열 "true", null 등)을 전송할 수 있으므로
* prepareForValidation 단계에서 명시 캐스팅합니다.
*
* @param array<int, string> $fields dot-notation 필드 경로 배열
*/
private function castAdvancedBooleanFields(array $fields): void
{
$advanced = $this->input('advanced', []);
$changed = false;
foreach ($fields as $dotPath) {
$key = str_replace('advanced.', '', $dotPath);
if (array_key_exists($key, $advanced)) {
$advanced[$key] = filter_var($advanced[$key], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
$changed = true;
}
}
if ($changed) {
$this->merge(['advanced' => $advanced]);
}
}
/**
@@ -114,7 +150,7 @@ class SaveSettingsRequest extends FormRequest
$rules = [
// 탭 식별자
'_tab' => ['nullable', 'string', Rule::in(['general', 'mail', 'upload', 'seo', 'security', 'drivers', 'advanced'])],
'_tab' => ['nullable', 'string', Rule::in(['general', 'mail', 'upload', 'seo', 'security', 'drivers', 'advanced', 'notifications'])],
// 각 탭의 컨테이너
'general' => ['sometimes', 'array'],
@@ -124,13 +160,18 @@ class SaveSettingsRequest extends FormRequest
'security' => ['sometimes', 'array'],
'drivers' => ['sometimes', 'array'],
'advanced' => ['sometimes', 'array'],
'notifications' => ['sometimes', 'array'],
'notifications.channels' => ['sometimes', 'array'],
'notifications.channels.*.id' => ['required_with:notifications.channels', 'string', 'max:50'],
'notifications.channels.*.is_active' => ['required_with:notifications.channels', 'boolean'],
'notifications.channels.*.sort_order' => ['nullable', 'integer', 'min:0'],
// 일반 설정
'general.site_name' => $this->getTabRules($tab, 'general', 'string|max:100'),
'general.site_url' => $this->getTabRules($tab, 'general', 'url|max:255'),
'general.site_description' => ['nullable', 'string', 'max:500'],
'general.admin_email' => $this->getTabRules($tab, 'general', 'email|max:255'),
'general.timezone' => $this->getTabRules($tab, 'general', [Rule::in(config('app.supported_timezones', ['Asia/Seoul', 'UTC']))]),
'general.timezone' => $this->getTabRules($tab, 'general', ['timezone']),
'general.language' => $this->getTabRules($tab, 'general', [Rule::in(self::SUPPORTED_LANGUAGES)]),
'general.currency' => ['nullable', 'string', 'max:10'],
'general.maintenance_mode' => ['nullable', 'boolean'],
@@ -201,6 +242,13 @@ class SaveSettingsRequest extends FormRequest
'advanced.core_update_github_url' => ['nullable', 'url', 'max:500'],
'advanced.core_update_github_token' => ['nullable', 'string', 'max:500'],
// GeoIP 설정 (advanced 탭)
// 마스터 토글 OFF 시 하위 필드(license_key, auto_update)는 collapse 되어 미전송됨
// → nullable 필수 (기존 cache/debug 필드와 달리 조건부 렌더링 내부에 있음)
'advanced.geoip_enabled' => ['nullable', 'boolean'],
'advanced.geoip_license_key' => ['nullable', 'string', 'max:200', 'regex:/^[A-Za-z0-9_]+$/'],
'advanced.geoip_auto_update_enabled' => ['nullable', 'boolean'],
// 드라이버 설정 (drivers 탭)
'drivers.storage_driver' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_STORAGE_DRIVERS)]),
'drivers.s3_bucket' => ['nullable', 'string', 'max:255'],
@@ -219,10 +267,16 @@ class SaveSettingsRequest extends FormRequest
'drivers.session_lifetime' => ['nullable', 'integer', 'min:1', 'max:43200'], // 1분 ~ 30일
'drivers.queue_driver' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_QUEUE_DRIVERS)]),
'drivers.websocket_enabled' => ['nullable', 'boolean'],
'drivers.websocket_app_key' => ['nullable', 'string', 'max:255'],
'drivers.websocket_app_id' => [Rule::requiredIf(fn () => $this->boolean('drivers.websocket_enabled')), 'nullable', 'string', 'max:255'],
'drivers.websocket_app_key' => [Rule::requiredIf(fn () => $this->boolean('drivers.websocket_enabled')), 'nullable', 'string', 'max:255'],
'drivers.websocket_app_secret' => [Rule::requiredIf(fn () => $this->boolean('drivers.websocket_enabled')), 'nullable', 'string', 'max:255'],
'drivers.websocket_host' => ['nullable', 'string', 'max:255'],
'drivers.websocket_port' => ['nullable', 'integer', 'min:1', 'max:65535'],
'drivers.websocket_scheme' => ['nullable', Rule::in(self::SUPPORTED_WEBSOCKET_SCHEMES)],
'drivers.websocket_verify_ssl' => ['nullable', 'boolean'],
'drivers.websocket_server_host' => ['nullable', 'string', 'max:255'],
'drivers.websocket_server_port' => ['nullable', 'integer', 'min:1', 'max:65535'],
'drivers.websocket_server_scheme' => ['nullable', Rule::in(self::SUPPORTED_WEBSOCKET_SCHEMES)],
'drivers.search_engine_driver' => ['nullable', Rule::in($this->getSupportedSearchEngineDrivers())],
'drivers.log_driver' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_LOG_DRIVERS)]),
'drivers.log_level' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_LOG_LEVELS)]),
@@ -366,7 +420,7 @@ class SaveSettingsRequest extends FormRequest
{
$drivers = HookManager::applyFilters(
'core.search.engine_drivers',
['mysql-fulltext' => \App\Search\Engines\DatabaseFulltextEngine::class]
['mysql-fulltext' => DatabaseFulltextEngine::class]
);
return array_keys($drivers);
@@ -535,12 +589,23 @@ class SaveSettingsRequest extends FormRequest
'drivers.queue_driver.required' => __('validation.settings.queue_driver_required'),
'drivers.queue_driver.in' => __('validation.settings.queue_driver_invalid'),
'drivers.websocket_enabled.boolean' => __('validation.settings.websocket_enabled_boolean'),
'drivers.websocket_app_id.required' => __('validation.settings.websocket_app_id_required'),
'drivers.websocket_app_id.max' => __('validation.settings.websocket_app_id_max'),
'drivers.websocket_app_key.required' => __('validation.settings.websocket_app_key_required'),
'drivers.websocket_app_key.max' => __('validation.settings.websocket_app_key_max'),
'drivers.websocket_app_secret.required' => __('validation.settings.websocket_app_secret_required'),
'drivers.websocket_app_secret.max' => __('validation.settings.websocket_app_secret_max'),
'drivers.websocket_host.max' => __('validation.settings.websocket_host_max'),
'drivers.websocket_port.integer' => __('validation.settings.websocket_port_integer'),
'drivers.websocket_port.min' => __('validation.settings.websocket_port_min'),
'drivers.websocket_port.max' => __('validation.settings.websocket_port_max'),
'drivers.websocket_scheme.in' => __('validation.settings.websocket_scheme_invalid'),
'drivers.websocket_verify_ssl.boolean' => __('validation.settings.websocket_verify_ssl_boolean'),
'drivers.websocket_server_host.max' => __('validation.settings.websocket_server_host_max'),
'drivers.websocket_server_port.integer' => __('validation.settings.websocket_server_port_integer'),
'drivers.websocket_server_port.min' => __('validation.settings.websocket_server_port_min'),
'drivers.websocket_server_port.max' => __('validation.settings.websocket_server_port_max'),
'drivers.websocket_server_scheme.in' => __('validation.settings.websocket_server_scheme_invalid'),
'drivers.search_engine_driver.in' => __('validation.settings.search_engine_driver_invalid'),
'drivers.log_driver.required' => __('validation.settings.log_driver_required'),
'drivers.log_driver.in' => __('validation.settings.log_driver_invalid'),
+1 -1
View File
@@ -36,7 +36,7 @@ class CreateUserRequest extends FormRequest
'password' => ['required', 'confirmed', Password::min(8)],
'language' => 'nullable|string|in:'.$supportedLocales,
'country' => 'nullable|string|size:2|alpha',
'timezone' => ['nullable', 'string', Rule::in(config('app.supported_timezones', ['Asia/Seoul']))],
'timezone' => ['nullable', 'string', 'timezone'],
'status' => 'nullable|string|in:active,inactive,blocked,withdrawn',
// 연락처 정보
@@ -44,7 +44,7 @@ class UpdateProfileRequest extends FormRequest
'current_password' => ['required_with:password', 'current_password'],
'language' => ['nullable', 'string', Rule::in($supportedLocales)],
'country' => ['nullable', 'string', 'size:2', 'alpha'],
'timezone' => ['nullable', 'string', Rule::in(config('app.supported_timezones', ['Asia/Seoul']))],
'timezone' => ['nullable', 'string', 'timezone'],
// 연락처 정보
'homepage' => ['nullable', 'url', 'max:255'],
+1 -1
View File
@@ -57,7 +57,7 @@ class UpdateUserRequest extends FormRequest
'password' => ['nullable', 'confirmed', Password::min(8)],
'language' => 'nullable|string|in:'.$supportedLocales,
'country' => 'nullable|string|size:2|alpha',
'timezone' => ['nullable', 'string', Rule::in(config('app.supported_timezones', ['Asia/Seoul']))],
'timezone' => ['nullable', 'string', 'timezone'],
'status' => 'nullable|string|in:active,inactive,blocked,withdrawn',
// 연락처 정보