v7.0.0-beta.2 release
This commit is contained in:
1 parent
6595fd0eb5
commit
fa8eba7a0d
1165 files changed
+107118
-762127
No files matched your search
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Helpers\PermissionHelper;
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Http\Requests\ActivityLog\ActivityLogBulkDeleteRequest;
|
||||
use App\Http\Requests\ActivityLog\ActivityLogDeleteRequest;
|
||||
@@ -42,14 +41,7 @@ class ActivityLogController extends AdminBaseController
|
||||
|
||||
$collection = new ActivityLogCollection($logs);
|
||||
|
||||
$responseData = $collection->toArray($request);
|
||||
|
||||
// 컬렉션 레벨 abilities (페이지 레벨 버튼 제어용)
|
||||
$responseData['abilities'] = [
|
||||
'can_delete' => PermissionHelper::check('core.activities.delete', $request->user()),
|
||||
];
|
||||
|
||||
return $this->success('activity_log.fetch_success', $responseData);
|
||||
return $this->success('activity_log.fetch_success', $collection->toArray($request));
|
||||
} catch (Exception $e) {
|
||||
return $this->error('activity_log.fetch_failed', 500, $e->getMessage());
|
||||
}
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Helpers\ResponseHelper;
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Http\Requests\Auth\LoginRequest;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Services\AuthService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AuthController extends AdminBaseController
|
||||
@@ -38,7 +36,7 @@ class AuthController extends AdminBaseController
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (!$user->isAdmin()) {
|
||||
return ResponseHelper::forbidden('auth.admin_required');
|
||||
return $this->forbidden('auth.admin_required');
|
||||
}
|
||||
|
||||
// 사용자 정보는 Resource로, 토큰은 그대로
|
||||
@@ -46,7 +44,7 @@ class AuthController extends AdminBaseController
|
||||
|
||||
return $this->success('auth.admin_login_success', $data);
|
||||
} catch (ValidationException $e) {
|
||||
return ResponseHelper::unauthorized('auth.login_failed', [], 'core');
|
||||
return $this->unauthorized('auth.login_failed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +97,7 @@ class AuthController extends AdminBaseController
|
||||
}
|
||||
return $this->success('common.success', $data);
|
||||
} catch (ValidationException $e) {
|
||||
return ResponseHelper::unauthorized('auth.unauthenticated', [], 'core');
|
||||
return $this->unauthorized('auth.unauthenticated');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Services\GeoIpDatabaseService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* GeoIP DB 관리 컨트롤러.
|
||||
*
|
||||
* 수동 DB 업데이트 트리거를 제공합니다. 정기 갱신은
|
||||
* routes/console.php의 스케줄(geoip:update)이 담당합니다.
|
||||
* 비즈니스 로직은 모두 GeoIpDatabaseService에 위임합니다.
|
||||
*/
|
||||
class GeoIpController extends AdminBaseController
|
||||
{
|
||||
public function __construct(
|
||||
private GeoIpDatabaseService $geoIpDatabaseService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* MaxMind GeoLite2-City DB를 즉시 재다운로드합니다.
|
||||
*
|
||||
* 내부적으로 GeoIpDatabaseService::updateDatabase(true)를 호출합니다.
|
||||
* 다운로드는 동기 실행이므로 PHP-FPM/웹서버 타임아웃(90초 이상) 필요.
|
||||
*
|
||||
* @return JsonResponse 업데이트 결과 JSON 응답
|
||||
*/
|
||||
public function update(): JsonResponse
|
||||
{
|
||||
$result = $this->geoIpDatabaseService->updateDatabase(true);
|
||||
|
||||
if ($result['success']) {
|
||||
return $this->success(
|
||||
'settings.geoip.update_success',
|
||||
$result['data'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
return $this->error(
|
||||
$this->mapStatusToMessageKey((string) $result['status']),
|
||||
$this->mapStatusToHttpCode((string) $result['status']),
|
||||
$result['message'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Service 상태 코드를 i18n 메시지 키로 매핑합니다.
|
||||
*
|
||||
* @param string $status Service 상태 코드
|
||||
* @return string i18n 키
|
||||
*/
|
||||
private function mapStatusToMessageKey(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
'missing_license_key' => 'settings.geoip.license_key_missing',
|
||||
'unauthorized' => 'settings.geoip.license_key_invalid',
|
||||
'connection_failed' => 'settings.geoip.connection_failed',
|
||||
default => 'settings.geoip.update_failed',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Service 상태 코드를 HTTP 상태 코드로 매핑합니다.
|
||||
*
|
||||
* @param string $status Service 상태 코드
|
||||
* @return int HTTP 상태 코드
|
||||
*/
|
||||
private function mapStatusToHttpCode(string $status): int
|
||||
{
|
||||
return match ($status) {
|
||||
'missing_license_key' => 400,
|
||||
'unauthorized' => 401,
|
||||
default => 500,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Helpers\PermissionHelper;
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Http\Requests\MailSendLog\MailSendLogBulkDeleteRequest;
|
||||
use App\Http\Requests\MailSendLog\MailSendLogDeleteRequest;
|
||||
use App\Http\Requests\MailSendLog\MailSendLogIndexRequest;
|
||||
use App\Http\Resources\MailSendLogCollection;
|
||||
use App\Models\MailSendLog;
|
||||
use App\Services\MailSendLogService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 메일 발송 이력 관리 컨트롤러
|
||||
*/
|
||||
class MailSendLogController extends AdminBaseController
|
||||
{
|
||||
/**
|
||||
* MailSendLogController 생성자.
|
||||
*
|
||||
* @param MailSendLogService $mailSendLogService 메일 발송 이력 서비스
|
||||
*/
|
||||
public function __construct(
|
||||
private MailSendLogService $mailSendLogService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 발송 이력 목록을 조회합니다.
|
||||
*
|
||||
* @param MailSendLogIndexRequest $request 검증된 조회 요청
|
||||
* @return JsonResponse 발송 이력 목록
|
||||
*/
|
||||
public function index(MailSendLogIndexRequest $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$filters = array_filter([
|
||||
'extension_type' => $request->validated('extension_type'),
|
||||
'extension_identifier' => $request->validated('extension_identifier'),
|
||||
'template_type' => $request->validated('template_type'),
|
||||
'status' => $request->validated('status'),
|
||||
'search' => $request->validated('search'),
|
||||
'search_type' => $request->validated('search_type'),
|
||||
'date_from' => $request->validated('date_from'),
|
||||
'date_to' => $request->validated('date_to'),
|
||||
'sort_by' => $request->validated('sort_by'),
|
||||
'sort_order' => $request->validated('sort_order'),
|
||||
], fn ($value) => $value !== null);
|
||||
|
||||
$perPage = (int) ($request->validated('per_page') ?? 20);
|
||||
$logs = $this->mailSendLogService->getLogs($filters, $perPage);
|
||||
|
||||
$collection = new MailSendLogCollection($logs);
|
||||
|
||||
$responseData = $collection->toArray($request);
|
||||
|
||||
// 컬렉션 레벨 abilities (페이지 레벨 버튼 제어용)
|
||||
$responseData['abilities'] = [
|
||||
'can_delete' => PermissionHelper::check('core.mail-send-logs.delete', $request->user()),
|
||||
];
|
||||
|
||||
return $this->success('mail_send_log.fetch_success', $responseData);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('메일 발송 이력 조회 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error('mail_send_log.fetch_failed', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 발송 이력을 삭제합니다.
|
||||
*
|
||||
* @param MailSendLogDeleteRequest $request 검증된 삭제 요청
|
||||
* @param MailSendLog $mailSendLog 삭제할 발송 이력 모델
|
||||
* @return JsonResponse 삭제 결과
|
||||
*/
|
||||
public function destroy(MailSendLogDeleteRequest $request, MailSendLog $mailSendLog): JsonResponse
|
||||
{
|
||||
try {
|
||||
$this->mailSendLogService->delete($mailSendLog->id);
|
||||
|
||||
return $this->success('mail_send_log.delete_success');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('메일 발송 이력 삭제 실패', ['id' => $mailSendLog->id, 'error' => $e->getMessage()]);
|
||||
|
||||
return $this->error('mail_send_log.delete_failed', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 발송 이력을 일괄 삭제합니다.
|
||||
*
|
||||
* @param MailSendLogBulkDeleteRequest $request 검증된 일괄 삭제 요청
|
||||
* @return JsonResponse 삭제 결과
|
||||
*/
|
||||
public function bulkDestroy(MailSendLogBulkDeleteRequest $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$count = $this->mailSendLogService->deleteMany($request->validated('ids'));
|
||||
|
||||
return $this->success('mail_send_log.bulk_delete_success', ['deleted_count' => $count]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('메일 발송 이력 일괄 삭제 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error('mail_send_log.delete_failed', 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Http\Requests\MailTemplate\MailTemplateIndexRequest;
|
||||
use App\Http\Requests\MailTemplate\MailTemplatePreviewRequest;
|
||||
use App\Http\Requests\MailTemplate\UpdateMailTemplateRequest;
|
||||
use App\Http\Resources\MailTemplateCollection;
|
||||
use App\Http\Resources\MailTemplateResource;
|
||||
use App\Models\MailTemplate;
|
||||
use App\Services\MailTemplateService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 메일 템플릿 관리 컨트롤러
|
||||
*
|
||||
* 메일 템플릿의 조회, 수정, 미리보기, 초기화 기능을 제공합니다.
|
||||
*/
|
||||
class MailTemplateController extends AdminBaseController
|
||||
{
|
||||
/**
|
||||
* MailTemplateController 생성자.
|
||||
*
|
||||
* @param MailTemplateService $mailTemplateService 메일 템플릿 서비스
|
||||
*/
|
||||
public function __construct(
|
||||
private MailTemplateService $mailTemplateService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 템플릿 목록을 페이지네이션하여 조회합니다.
|
||||
*
|
||||
* @param MailTemplateIndexRequest $request 검증된 목록 조회 요청
|
||||
* @return JsonResponse 페이지네이션된 템플릿 목록
|
||||
*/
|
||||
public function index(MailTemplateIndexRequest $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$filters = array_filter($request->validated(), fn ($value) => $value !== null);
|
||||
$perPage = (int) ($request->validated('per_page') ?? 20);
|
||||
$templates = $this->mailTemplateService->getTemplates($filters, $perPage);
|
||||
|
||||
$collection = new MailTemplateCollection($templates);
|
||||
|
||||
return $this->success('mail_template.fetch_success', $collection->toArray($request));
|
||||
} catch (\Exception $e) {
|
||||
Log::error('메일 템플릿 목록 조회 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error('mail_template.fetch_failed', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 템플릿을 수정합니다.
|
||||
*
|
||||
* @param UpdateMailTemplateRequest $request 수정 요청
|
||||
* @param MailTemplate $mailTemplate 수정 대상
|
||||
* @return JsonResponse 수정 결과
|
||||
*/
|
||||
public function update(UpdateMailTemplateRequest $request, MailTemplate $mailTemplate): JsonResponse
|
||||
{
|
||||
try {
|
||||
$userId = $request->user()?->id;
|
||||
$template = $this->mailTemplateService->updateTemplate(
|
||||
$mailTemplate,
|
||||
$request->validated(),
|
||||
$userId
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
'mail_template.save_success',
|
||||
new MailTemplateResource($template)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('메일 템플릿 수정 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error('mail_template.save_error', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 템플릿의 활성 상태를 토글합니다.
|
||||
*
|
||||
* @param MailTemplate $mailTemplate 토글 대상
|
||||
* @return JsonResponse 토글 결과
|
||||
*/
|
||||
public function toggleActive(MailTemplate $mailTemplate): JsonResponse
|
||||
{
|
||||
try {
|
||||
$template = $this->mailTemplateService->toggleActive($mailTemplate);
|
||||
|
||||
return $this->success(
|
||||
'mail_template.toggle_success',
|
||||
new MailTemplateResource($template)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('메일 템플릿 활성 상태 토글 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error('mail_template.toggle_failed', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 템플릿 미리보기를 반환합니다.
|
||||
*
|
||||
* @param MailTemplatePreviewRequest $request 검증된 미리보기 요청
|
||||
* @return JsonResponse 미리보기 결과
|
||||
*/
|
||||
public function preview(MailTemplatePreviewRequest $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$result = $this->mailTemplateService->getPreview($request->validated());
|
||||
|
||||
return $this->success('mail_template.preview_success', $result);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('메일 템플릿 미리보기 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error('mail_template.preview_failed', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 템플릿을 시더 기본값으로 복원합니다.
|
||||
*
|
||||
* @param MailTemplate $mailTemplate 복원 대상
|
||||
* @return JsonResponse 복원 결과
|
||||
*/
|
||||
public function reset(MailTemplate $mailTemplate): JsonResponse
|
||||
{
|
||||
try {
|
||||
$defaultData = $this->mailTemplateService->getDefaultTemplateData($mailTemplate->type);
|
||||
|
||||
if (! $defaultData) {
|
||||
return $this->error('mail_template.reset_no_default', 404);
|
||||
}
|
||||
|
||||
$template = $this->mailTemplateService->resetToDefault($mailTemplate, $defaultData);
|
||||
|
||||
return $this->success(
|
||||
'mail_template.reset_success',
|
||||
new MailTemplateResource($template)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('메일 템플릿 기본값 복원 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error('mail_template.reset_failed', 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Helpers\PermissionHelper;
|
||||
use App\Helpers\ResponseHelper;
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Http\Requests\Module\ActivateModuleRequest;
|
||||
use App\Http\Requests\Module\DeactivateModuleRequest;
|
||||
@@ -11,6 +9,7 @@ use App\Http\Requests\Module\IndexModuleRequest;
|
||||
use App\Http\Requests\Module\InstallModuleFromFileRequest;
|
||||
use App\Http\Requests\Module\InstallModuleFromGithubRequest;
|
||||
use App\Http\Requests\Module\InstallModuleRequest;
|
||||
use App\Http\Requests\Module\PerformModuleUpdateRequest;
|
||||
use App\Http\Requests\Module\RefreshModuleLayoutsRequest;
|
||||
use App\Http\Requests\Module\UninstallModuleRequest;
|
||||
use App\Http\Requests\Extension\ChangelogRequest;
|
||||
@@ -62,15 +61,9 @@ class ModuleController extends AdminBaseController
|
||||
$collection = new ModuleCollection(collect($responseData['data']));
|
||||
$responseData['data'] = $collection->toArray($request)['data'];
|
||||
$responseData['meta'] = $collection->with($request)['meta'];
|
||||
$responseData['abilities'] = $collection->resolveCollectionAbilities($request);
|
||||
}
|
||||
|
||||
// 컬렉션 레벨 abilities (페이지 레벨 버튼 제어용)
|
||||
$responseData['abilities'] = [
|
||||
'can_install' => PermissionHelper::check('core.modules.install', $request->user()),
|
||||
'can_activate' => PermissionHelper::check('core.modules.activate', $request->user()),
|
||||
'can_uninstall' => PermissionHelper::check('core.modules.uninstall', $request->user()),
|
||||
];
|
||||
|
||||
return $this->success('module.fetch_success', $responseData);
|
||||
} catch (\Exception $e) {
|
||||
return $this->error('module.fetch_failed', 500, $e->getMessage());
|
||||
@@ -154,8 +147,12 @@ class ModuleController extends AdminBaseController
|
||||
public function install(InstallModuleRequest $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$moduleName = $request->validated()['module_name'];
|
||||
$module = $this->moduleService->installModule($moduleName);
|
||||
$validated = $request->validated();
|
||||
$moduleName = $validated['module_name'];
|
||||
$vendorMode = \App\Extension\Vendor\VendorMode::fromStringOrAuto(
|
||||
$validated['vendor_mode'] ?? null
|
||||
);
|
||||
$module = $this->moduleService->installModule($moduleName, $vendorMode);
|
||||
|
||||
if ($module) {
|
||||
return $this->successWithResource(
|
||||
@@ -172,7 +169,7 @@ class ModuleController extends AdminBaseController
|
||||
$firstError = collect($e->errors())->flatten()->first()
|
||||
?? __('module.install_failed');
|
||||
|
||||
return ResponseHelper::error($firstError, 422, $e->errors());
|
||||
return $this->validationError($e->errors(), $firstError);
|
||||
} catch (\Exception $e) {
|
||||
return $this->error('modules.installation_failed', 500, $e->getMessage(), [
|
||||
'error' => $e->getMessage(),
|
||||
@@ -411,16 +408,48 @@ class ModuleController extends AdminBaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 모듈의 수정된 레이아웃을 확인합니다.
|
||||
*
|
||||
* 업데이트 전 사용자가 수정한 레이아웃이 있는지 확인하여
|
||||
* 레이아웃 전략(overwrite/keep) 선택에 참고할 수 있도록 합니다.
|
||||
*
|
||||
* @param string $moduleName 모듈 식별자
|
||||
* @return JsonResponse 수정된 레이아웃 정보를 포함한 JSON 응답
|
||||
*/
|
||||
public function checkModifiedLayouts(string $moduleName): JsonResponse
|
||||
{
|
||||
try {
|
||||
$result = $this->moduleService->checkModifiedLayouts($moduleName);
|
||||
|
||||
return $this->success('modules.check_modified_layouts_success', $result);
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error('modules.check_modified_layouts_failed', 422, $e->errors());
|
||||
} catch (\Exception $e) {
|
||||
return $this->error('modules.check_modified_layouts_failed', 500, $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 모듈을 업데이트합니다.
|
||||
*
|
||||
* layout_strategy 파라미터로 레이아웃 처리 방식을 결정합니다:
|
||||
* - overwrite: 모든 레이아웃을 새 버전으로 교체
|
||||
* - keep: 사용자가 수정한 레이아웃을 유지
|
||||
*
|
||||
* @param PerformModuleUpdateRequest $request 업데이트 요청 데이터
|
||||
* @param string $moduleName 업데이트할 모듈 identifier
|
||||
* @return JsonResponse 업데이트 결과 JSON 응답
|
||||
*/
|
||||
public function performUpdate(string $moduleName): JsonResponse
|
||||
public function performUpdate(PerformModuleUpdateRequest $request, string $moduleName): JsonResponse
|
||||
{
|
||||
try {
|
||||
$result = $this->moduleService->updateModule($moduleName);
|
||||
$validated = $request->validated();
|
||||
$vendorMode = \App\Extension\Vendor\VendorMode::fromStringOrAuto(
|
||||
$validated['vendor_mode'] ?? null
|
||||
);
|
||||
$layoutStrategy = $validated['layout_strategy'] ?? 'overwrite';
|
||||
$result = $this->moduleService->updateModule($moduleName, $vendorMode, $layoutStrategy);
|
||||
|
||||
$moduleInfo = $result['module_info'] ?? null;
|
||||
|
||||
@@ -438,7 +467,7 @@ class ModuleController extends AdminBaseController
|
||||
$firstError = collect($e->errors())->flatten()->first()
|
||||
?? __('modules.errors.update_failed', ['module' => $moduleName, 'error' => '']);
|
||||
|
||||
return ResponseHelper::error($firstError, 422, $e->errors());
|
||||
return $this->validationError($e->errors(), $firstError);
|
||||
} catch (\Exception $e) {
|
||||
return $this->error('modules.errors.update_failed', 500, $e->getMessage(), [
|
||||
'module' => $moduleName,
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Contracts\Notifications\ChannelReadinessCheckerInterface;
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Services\NotificationChannelService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 알림 채널 관리 컨트롤러
|
||||
*
|
||||
* 시스템에서 사용 가능한 알림 채널 목록을 제공합니다.
|
||||
* 플러그인이 Filter 훅으로 채널을 확장할 수 있습니다.
|
||||
*/
|
||||
class NotificationChannelController extends AdminBaseController
|
||||
{
|
||||
/**
|
||||
* @param NotificationChannelService $channelService
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly NotificationChannelService $channelService,
|
||||
private readonly ChannelReadinessCheckerInterface $readinessChecker,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용 가능한 알림 채널 목록을 반환합니다.
|
||||
*
|
||||
* 각 채널에 readiness (설정 완료 여부) 정보를 포함합니다.
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
$channels = $this->channelService->getAvailableChannels();
|
||||
|
||||
// 각 채널에 readiness 정보 추가
|
||||
$channels = array_map(function (array $channel) {
|
||||
$channelId = $channel['id'] ?? '';
|
||||
$channel['readiness'] = $this->readinessChecker->check($channelId);
|
||||
|
||||
return $channel;
|
||||
}, $channels);
|
||||
|
||||
return $this->success(
|
||||
__('notification.channels_success'),
|
||||
['channels' => $channels]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('알림 채널 목록 조회 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification.channels_failed'), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,29 +3,170 @@
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Http\Requests\Notification\NotificationBatchReadRequest;
|
||||
use App\Http\Requests\Notification\NotificationIndexRequest;
|
||||
use App\Http\Resources\UserNotificationCollection;
|
||||
use App\Http\Resources\UserNotificationResource;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 관리자 알림 컨트롤러 (더미)
|
||||
* 관리자 알림 컨트롤러
|
||||
*
|
||||
* 추후 알림 기능 구현 예정. 현재는 빈 목록을 반환합니다.
|
||||
* 관리자의 사이트내 알림을 관리합니다.
|
||||
*/
|
||||
class NotificationController extends AdminBaseController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly NotificationService $notificationService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 목록을 조회합니다.
|
||||
*
|
||||
* @return JsonResponse 알림 목록 (현재 빈 배열)
|
||||
*/
|
||||
public function index(): JsonResponse
|
||||
public function index(NotificationIndexRequest $request): JsonResponse
|
||||
{
|
||||
// TODO: 실제 알림 기능 구현 시 교체 예정
|
||||
return $this->success('messages.success', [
|
||||
'data' => [],
|
||||
'meta' => [
|
||||
'total' => 0,
|
||||
'unread_count' => 0,
|
||||
],
|
||||
]);
|
||||
try {
|
||||
$validated = $request->validated();
|
||||
$user = $request->user();
|
||||
$perPage = (int) ($validated['per_page'] ?? 15);
|
||||
$filters = collect($validated)->only(['read'])->filter(fn ($v) => $v !== null)->toArray();
|
||||
|
||||
$notifications = $this->notificationService->getNotifications($user, $filters, $perPage);
|
||||
|
||||
return $this->success(
|
||||
__('notification.user.list_success'),
|
||||
new UserNotificationCollection($notifications)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('관리자 알림 목록 조회 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification.user.list_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 미읽음 알림 수를 반환합니다.
|
||||
*/
|
||||
public function unreadCount(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$count = $this->notificationService->getUnreadCount($request->user());
|
||||
|
||||
return $this->success(
|
||||
__('notification.user.unread_count_success'),
|
||||
['unread_count' => $count]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('관리자 미읽음 알림 수 조회 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification.user.unread_count_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림을 읽음 처리합니다.
|
||||
*/
|
||||
public function markAsRead(Request $request, string $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$notification = $this->notificationService->markAsRead($request->user(), $id);
|
||||
|
||||
if (! $notification) {
|
||||
return $this->error(__('notification.user.not_found'), 404);
|
||||
}
|
||||
|
||||
return $this->success(
|
||||
__('notification.user.read_success'),
|
||||
new UserNotificationResource($notification)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('관리자 알림 읽음 처리 실패', ['id' => $id, 'error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification.user.read_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 지정된 알림들을 일괄 읽음 처리합니다.
|
||||
*/
|
||||
public function markBatchAsRead(NotificationBatchReadRequest $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$ids = $request->validated()['ids'];
|
||||
|
||||
$count = $this->notificationService->markBatchAsRead($request->user(), $ids);
|
||||
|
||||
return $this->success(
|
||||
__('notification.user.read_success'),
|
||||
['marked_count' => $count]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('관리자 알림 일괄 읽음 처리 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification.user.read_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 알림을 읽음 처리합니다.
|
||||
*/
|
||||
public function markAllAsRead(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$count = $this->notificationService->markAllAsRead($request->user());
|
||||
|
||||
return $this->success(
|
||||
__('notification.user.read_all_success'),
|
||||
['marked_count' => $count]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('관리자 전체 읽음 처리 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification.user.read_all_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 모든 알림을 삭제합니다.
|
||||
*/
|
||||
public function destroyAll(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$count = $this->notificationService->deleteAllNotifications($request->user());
|
||||
|
||||
return $this->success(
|
||||
__('notification.user.delete_all_success'),
|
||||
['deleted_count' => $count]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('관리자 전체 알림 삭제 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification.user.delete_all_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림을 삭제합니다.
|
||||
*/
|
||||
public function destroy(Request $request, string $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$deleted = $this->notificationService->deleteNotification($request->user(), $id);
|
||||
|
||||
if (! $deleted) {
|
||||
return $this->error(__('notification.user.not_found'), 404);
|
||||
}
|
||||
|
||||
return $this->success(__('notification.user.delete_success'));
|
||||
} catch (\Exception $e) {
|
||||
Log::error('관리자 알림 삭제 실패', ['id' => $id, 'error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification.user.delete_failed'), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Http\Requests\NotificationDefinition\NotificationDefinitionIndexRequest;
|
||||
use App\Http\Requests\NotificationDefinition\UpdateNotificationDefinitionRequest;
|
||||
use App\Http\Resources\NotificationDefinitionCollection;
|
||||
use App\Http\Resources\NotificationDefinitionResource;
|
||||
use App\Models\NotificationDefinition;
|
||||
use App\Services\NotificationDefinitionService;
|
||||
use App\Services\NotificationTemplateService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 알림 정의 관리 컨트롤러
|
||||
*/
|
||||
class NotificationDefinitionController extends AdminBaseController
|
||||
{
|
||||
/**
|
||||
* @param NotificationDefinitionService $definitionService
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly NotificationDefinitionService $definitionService,
|
||||
private readonly NotificationTemplateService $templateService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 정의 목록을 조회합니다.
|
||||
*
|
||||
* @param NotificationDefinitionIndexRequest $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function index(NotificationDefinitionIndexRequest $request)
|
||||
{
|
||||
try {
|
||||
$filters = $request->validated();
|
||||
$perPage = $filters['per_page'] ?? 20;
|
||||
|
||||
$definitions = $this->definitionService->getDefinitions($filters, $perPage);
|
||||
$collection = new NotificationDefinitionCollection($definitions);
|
||||
|
||||
return $this->success(
|
||||
__('notification.definition_list_success'),
|
||||
$collection->toArray($request)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('알림 정의 목록 조회 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification.definition_list_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 정의 상세를 조회합니다.
|
||||
*
|
||||
* @param NotificationDefinition $definition
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function show(NotificationDefinition $definition)
|
||||
{
|
||||
try {
|
||||
$definition->load('templates');
|
||||
|
||||
return $this->success(
|
||||
__('notification.definition_show_success'),
|
||||
new NotificationDefinitionResource($definition)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('알림 정의 상세 조회 실패', [
|
||||
'definition_id' => $definition->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error(__('notification.definition_show_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 정의를 수정합니다.
|
||||
*
|
||||
* @param UpdateNotificationDefinitionRequest $request
|
||||
* @param NotificationDefinition $definition
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function update(UpdateNotificationDefinitionRequest $request, NotificationDefinition $definition)
|
||||
{
|
||||
try {
|
||||
$updated = $this->definitionService->updateDefinition(
|
||||
$definition,
|
||||
$request->validated(),
|
||||
$this->getCurrentAdmin()?->id
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
__('notification.definition_updated'),
|
||||
new NotificationDefinitionResource($updated->load('templates'))
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('알림 정의 수정 실패', [
|
||||
'definition_id' => $definition->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error(__('notification.definition_update_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 정의 활성/비활성을 토글합니다.
|
||||
*
|
||||
* @param NotificationDefinition $definition
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function toggleActive(NotificationDefinition $definition)
|
||||
{
|
||||
try {
|
||||
$updated = $this->definitionService->toggleActive($definition);
|
||||
|
||||
return $this->success(
|
||||
__('notification.definition_toggled'),
|
||||
new NotificationDefinitionResource($updated)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('알림 정의 활성 토글 실패', [
|
||||
'definition_id' => $definition->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error(__('notification.definition_toggle_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 정의의 모든 템플릿을 기본값으로 일괄 복원합니다.
|
||||
*
|
||||
* @param NotificationDefinition $definition
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function reset(NotificationDefinition $definition)
|
||||
{
|
||||
try {
|
||||
$definition->load('templates');
|
||||
$resetCount = 0;
|
||||
|
||||
foreach ($definition->templates as $template) {
|
||||
$defaultData = $this->templateService->getDefaultTemplateData(
|
||||
$definition->type,
|
||||
$template->channel
|
||||
);
|
||||
|
||||
if (! empty($defaultData)) {
|
||||
$this->templateService->resetToDefault($template, $defaultData);
|
||||
$resetCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->definitionService->markAsDefault($definition);
|
||||
$definition->load('templates');
|
||||
|
||||
return $this->success(
|
||||
__('notification.definition_reset'),
|
||||
new NotificationDefinitionResource($definition)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('알림 정의 초기화 실패', [
|
||||
'definition_id' => $definition->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error(__('notification.definition_reset_failed'), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Http\Requests\NotificationLog\NotificationLogBulkDeleteRequest;
|
||||
use App\Http\Requests\NotificationLog\NotificationLogDeleteRequest;
|
||||
use App\Http\Requests\NotificationLog\NotificationLogIndexRequest;
|
||||
use App\Http\Resources\NotificationLogCollection;
|
||||
use App\Models\NotificationLog;
|
||||
use App\Services\NotificationLogService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 알림 발송 이력 관리 컨트롤러
|
||||
*/
|
||||
class NotificationLogController extends AdminBaseController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly NotificationLogService $logService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 발송 이력 목록을 조회합니다.
|
||||
*/
|
||||
public function index(NotificationLogIndexRequest $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$filters = $request->validated();
|
||||
$perPage = $filters['per_page'] ?? 20;
|
||||
|
||||
$logs = $this->logService->getLogs($filters, $perPage, $request->user());
|
||||
$collection = new NotificationLogCollection($logs);
|
||||
|
||||
return $this->success(
|
||||
__('notification_log.list_success'),
|
||||
$collection->toArray($request)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('알림 발송 이력 목록 조회 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification_log.list_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 발송 이력을 삭제합니다.
|
||||
*/
|
||||
public function destroy(NotificationLogDeleteRequest $request, NotificationLog $notificationLog): JsonResponse
|
||||
{
|
||||
try {
|
||||
$this->logService->deleteLog($notificationLog);
|
||||
|
||||
return $this->success(__('notification_log.delete_success'));
|
||||
} catch (\Exception $e) {
|
||||
Log::error('알림 발송 이력 삭제 실패', [
|
||||
'id' => $notificationLog->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error(__('notification_log.delete_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 발송 이력을 다건 삭제합니다.
|
||||
*/
|
||||
public function bulkDestroy(NotificationLogBulkDeleteRequest $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$ids = $request->validated()['ids'];
|
||||
$count = $this->logService->bulkDelete($ids);
|
||||
|
||||
return $this->success(
|
||||
__('notification_log.bulk_delete_success'),
|
||||
['deleted_count' => $count]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('알림 발송 이력 다건 삭제 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification_log.bulk_delete_failed'), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Http\Requests\NotificationTemplate\PreviewNotificationTemplateRequest;
|
||||
use App\Http\Requests\NotificationTemplate\UpdateNotificationTemplateRequest;
|
||||
use App\Http\Resources\NotificationTemplateResource;
|
||||
use App\Models\NotificationTemplate;
|
||||
use App\Services\NotificationTemplateService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 알림 템플릿 관리 컨트롤러
|
||||
*/
|
||||
class NotificationTemplateController extends AdminBaseController
|
||||
{
|
||||
/**
|
||||
* @param NotificationTemplateService $templateService
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly NotificationTemplateService $templateService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 템플릿을 수정합니다.
|
||||
*
|
||||
* @param UpdateNotificationTemplateRequest $request
|
||||
* @param NotificationTemplate $template
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function update(UpdateNotificationTemplateRequest $request, NotificationTemplate $template)
|
||||
{
|
||||
try {
|
||||
$updated = $this->templateService->updateTemplate(
|
||||
$template,
|
||||
$request->validated(),
|
||||
$this->getCurrentAdmin()?->id
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
__('notification.template_updated'),
|
||||
new NotificationTemplateResource($updated)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('알림 템플릿 수정 실패', [
|
||||
'template_id' => $template->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error(__('notification.template_update_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 템플릿 활성/비활성을 토글합니다.
|
||||
*
|
||||
* @param NotificationTemplate $template
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function toggleActive(NotificationTemplate $template)
|
||||
{
|
||||
try {
|
||||
$updated = $this->templateService->toggleActive($template);
|
||||
|
||||
return $this->success(
|
||||
__('notification.template_toggled'),
|
||||
new NotificationTemplateResource($updated)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('알림 템플릿 활성 토글 실패', [
|
||||
'template_id' => $template->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error(__('notification.template_toggle_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 템플릿 미리보기를 반환합니다.
|
||||
*
|
||||
* @param PreviewNotificationTemplateRequest $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function preview(PreviewNotificationTemplateRequest $request)
|
||||
{
|
||||
try {
|
||||
$result = $this->templateService->getPreview($request->validated());
|
||||
|
||||
return $this->success(__('notification.preview_success'), $result);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('알림 템플릿 미리보기 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification.preview_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 템플릿을 기본값으로 복원합니다.
|
||||
*
|
||||
* @param NotificationTemplate $template
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function reset(NotificationTemplate $template)
|
||||
{
|
||||
try {
|
||||
$definition = $template->definition;
|
||||
if (! $definition) {
|
||||
return $this->error(__('notification.definition_not_found'), 404);
|
||||
}
|
||||
|
||||
$defaultData = $this->templateService->getDefaultTemplateData($definition->type, $template->channel);
|
||||
if (empty($defaultData)) {
|
||||
return $this->error(__('notification.default_data_not_found'), 404);
|
||||
}
|
||||
|
||||
$updated = $this->templateService->resetToDefault($template, $defaultData);
|
||||
|
||||
return $this->success(
|
||||
__('notification.template_reset'),
|
||||
new NotificationTemplateResource($updated)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('알림 템플릿 초기화 실패', [
|
||||
'template_id' => $template->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error(__('notification.template_reset_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Helpers\PermissionHelper;
|
||||
use App\Helpers\ResponseHelper;
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Http\Requests\Plugin\ActivatePluginRequest;
|
||||
use App\Http\Requests\Plugin\DeactivatePluginRequest;
|
||||
@@ -11,6 +10,7 @@ use App\Http\Requests\Plugin\IndexPluginRequest;
|
||||
use App\Http\Requests\Plugin\InstallPluginFromFileRequest;
|
||||
use App\Http\Requests\Plugin\InstallPluginFromGithubRequest;
|
||||
use App\Http\Requests\Plugin\InstallPluginRequest;
|
||||
use App\Http\Requests\Plugin\PerformPluginUpdateRequest;
|
||||
use App\Http\Requests\Plugin\RefreshPluginLayoutsRequest;
|
||||
use App\Http\Requests\Plugin\UninstallPluginRequest;
|
||||
use App\Http\Requests\Extension\ChangelogRequest;
|
||||
@@ -139,8 +139,12 @@ class PluginController extends AdminBaseController
|
||||
public function install(InstallPluginRequest $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$pluginName = $request->validated()['plugin_name'];
|
||||
$pluginInfo = $this->pluginService->installPlugin($pluginName);
|
||||
$validated = $request->validated();
|
||||
$pluginName = $validated['plugin_name'];
|
||||
$vendorMode = \App\Extension\Vendor\VendorMode::fromStringOrAuto(
|
||||
$validated['vendor_mode'] ?? null
|
||||
);
|
||||
$pluginInfo = $this->pluginService->installPlugin($pluginName, $vendorMode);
|
||||
|
||||
if ($pluginInfo) {
|
||||
return $this->successWithResource(
|
||||
@@ -156,7 +160,7 @@ class PluginController extends AdminBaseController
|
||||
$firstError = collect($e->errors())->flatten()->first()
|
||||
?? __('plugins.install_failed');
|
||||
|
||||
return ResponseHelper::error($firstError, 422, $e->errors());
|
||||
return $this->validationError($e->errors(), $firstError);
|
||||
} catch (\Exception $e) {
|
||||
return $this->error('plugins.installation_failed', 500, $e->getMessage(), [
|
||||
'error' => $e->getMessage(),
|
||||
@@ -419,16 +423,45 @@ class PluginController extends AdminBaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 플러그인의 수정된 레이아웃을 확인합니다.
|
||||
*
|
||||
* @param string $pluginName 플러그인 식별자
|
||||
* @return JsonResponse 수정된 레이아웃 정보를 포함한 JSON 응답
|
||||
*/
|
||||
public function checkModifiedLayouts(string $pluginName): JsonResponse
|
||||
{
|
||||
try {
|
||||
$result = $this->pluginService->checkModifiedLayouts($pluginName);
|
||||
|
||||
return $this->success('plugins.check_modified_layouts_success', $result);
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error('plugins.check_modified_layouts_failed', 422, $e->errors());
|
||||
} catch (\Exception $e) {
|
||||
return $this->error('plugins.check_modified_layouts_failed', 500, $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 플러그인을 업데이트합니다.
|
||||
*
|
||||
* layout_strategy 파라미터로 레이아웃 처리 방식을 결정합니다:
|
||||
* - overwrite: 모든 레이아웃을 새 버전으로 교체
|
||||
* - keep: 사용자가 수정한 레이아웃을 유지
|
||||
*
|
||||
* @param PerformPluginUpdateRequest $request 업데이트 요청 데이터
|
||||
* @param string $pluginName 업데이트할 플러그인 identifier
|
||||
* @return JsonResponse 업데이트 결과 JSON 응답
|
||||
*/
|
||||
public function performUpdate(string $pluginName): JsonResponse
|
||||
public function performUpdate(PerformPluginUpdateRequest $request, string $pluginName): JsonResponse
|
||||
{
|
||||
try {
|
||||
$result = $this->pluginService->updatePlugin($pluginName);
|
||||
$validated = $request->validated();
|
||||
$vendorMode = \App\Extension\Vendor\VendorMode::fromStringOrAuto(
|
||||
$validated['vendor_mode'] ?? null
|
||||
);
|
||||
$layoutStrategy = $validated['layout_strategy'] ?? 'overwrite';
|
||||
$result = $this->pluginService->updatePlugin($pluginName, $vendorMode, $layoutStrategy);
|
||||
|
||||
$pluginInfo = $result['plugin_info'] ?? null;
|
||||
|
||||
@@ -446,7 +479,7 @@ class PluginController extends AdminBaseController
|
||||
$firstError = collect($e->errors())->flatten()->first()
|
||||
?? __('plugins.errors.update_failed', ['plugin' => $pluginName, 'error' => '']);
|
||||
|
||||
return ResponseHelper::error($firstError, 422, $e->errors());
|
||||
return $this->validationError($e->errors(), $firstError);
|
||||
} catch (\Exception $e) {
|
||||
return $this->error('plugins.errors.update_failed', 500, $e->getMessage(), [
|
||||
'plugin' => $pluginName,
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Http\Requests\Settings\TestMailRequest;
|
||||
use App\Http\Requests\Settings\UpdateSettingRequest;
|
||||
use App\Http\Resources\SettingsResource;
|
||||
use App\Services\DriverConnectionTester;
|
||||
use App\Services\DriverRegistryService;
|
||||
use App\Services\SettingsService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -24,7 +25,8 @@ class SettingsController extends AdminBaseController
|
||||
{
|
||||
public function __construct(
|
||||
private SettingsService $settingsService,
|
||||
private DriverConnectionTester $driverConnectionTester
|
||||
private DriverConnectionTester $driverConnectionTester,
|
||||
private DriverRegistryService $driverRegistryService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -38,6 +40,7 @@ class SettingsController extends AdminBaseController
|
||||
{
|
||||
try {
|
||||
$settings = $this->settingsService->getAllSettings();
|
||||
$settings['available_drivers'] = $this->driverRegistryService->getAllAvailableDrivers();
|
||||
|
||||
return $this->success('settings.fetch_success',
|
||||
(new SettingsResource($settings))->toArray(request())
|
||||
@@ -64,6 +67,7 @@ class SettingsController extends AdminBaseController
|
||||
if ($result) {
|
||||
// 저장 후 전체 설정 반환 (관리자 UI 상태 업데이트용)
|
||||
$allSettings = $this->settingsService->getAllSettings();
|
||||
$allSettings['available_drivers'] = $this->driverRegistryService->getAllAvailableDrivers();
|
||||
|
||||
return $this->success('settings.save_success', [
|
||||
'settings' => $allSettings,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Helpers\PermissionHelper;
|
||||
use App\Helpers\ResponseHelper;
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Http\Requests\Template\ActivateTemplateRequest;
|
||||
use App\Http\Requests\Template\DeactivateTemplateRequest;
|
||||
@@ -135,7 +134,7 @@ class TemplateController extends AdminBaseController
|
||||
$firstError = collect($e->errors())->flatten()->first()
|
||||
?? __('templates.install_failed');
|
||||
|
||||
return ResponseHelper::error($firstError, 422, $e->errors());
|
||||
return $this->validationError($e->errors(), $firstError);
|
||||
} catch (\Exception $e) {
|
||||
return $this->error('templates.errors.installation_failed', 500, $e->getMessage(), [
|
||||
'error' => $e->getMessage(),
|
||||
@@ -414,7 +413,7 @@ class TemplateController extends AdminBaseController
|
||||
$firstError = collect($e->errors())->flatten()->first()
|
||||
?? __('templates.errors.update_failed', ['template' => $templateName, 'error' => '']);
|
||||
|
||||
return ResponseHelper::error($firstError, 422, $e->errors());
|
||||
return $this->validationError($e->errors(), $firstError);
|
||||
} catch (\Exception $e) {
|
||||
return $this->error('templates.errors.update_failed', 500, $e->getMessage(), [
|
||||
'template' => $templateName,
|
||||
|
||||
@@ -23,7 +23,7 @@ use Illuminate\Validation\ValidationException;
|
||||
|
||||
/**
|
||||
* 관리자용 사용자 관리 컨트롤러
|
||||
*
|
||||
*
|
||||
* 관리자가 시스템 사용자들을 관리할 수 있는 기능을 제공합니다.
|
||||
*/
|
||||
class UserController extends AdminBaseController
|
||||
@@ -78,7 +78,7 @@ class UserController extends AdminBaseController
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error('user.create_failed', 422, $e->errors());
|
||||
} catch (Exception $e) {
|
||||
return $this->error('user.create_failed', 500, $e->getMessage());
|
||||
return $this->error('user.create_failed', 500, $e, ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ class UserController extends AdminBaseController
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error('user.update_failed', 422, $e->errors());
|
||||
} catch (Exception $e) {
|
||||
return $this->error('user.update_failed', 500, $e->getMessage());
|
||||
return $this->error('user.update_failed', 500, $e, ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ class UserController extends AdminBaseController
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error('user.delete_failed', 422, $e->errors());
|
||||
} catch (Exception $e) {
|
||||
return $this->error('user.delete_failed', 500);
|
||||
return $this->error('user.delete_failed', 500, $e, ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Auth;
|
||||
|
||||
use App\Http\Controllers\Api\Base\AuthBaseController;
|
||||
use App\Http\Requests\Notification\NotificationBatchReadRequest;
|
||||
use App\Http\Requests\Notification\NotificationIndexRequest;
|
||||
use App\Http\Resources\UserNotificationCollection;
|
||||
use App\Http\Resources\UserNotificationResource;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 사용자 알림 컨트롤러
|
||||
*
|
||||
* 인증된 사용자의 사이트내 알림을 관리합니다.
|
||||
*/
|
||||
class NotificationController extends AuthBaseController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly NotificationService $notificationService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 목록을 조회합니다.
|
||||
*/
|
||||
public function index(NotificationIndexRequest $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$validated = $request->validated();
|
||||
$user = $request->user();
|
||||
$perPage = (int) ($validated['per_page'] ?? 20);
|
||||
$filters = collect($validated)->only(['read'])->filter(fn ($v) => $v !== null)->toArray();
|
||||
|
||||
$notifications = $this->notificationService->getNotifications($user, $filters, $perPage);
|
||||
|
||||
return $this->success(
|
||||
__('notification.user.list_success'),
|
||||
new UserNotificationCollection($notifications)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('알림 목록 조회 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification.user.list_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 미읽음 알림 수를 반환합니다.
|
||||
*/
|
||||
public function unreadCount(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$count = $this->notificationService->getUnreadCount($request->user());
|
||||
|
||||
return $this->success(
|
||||
__('notification.user.unread_count_success'),
|
||||
['unread_count' => $count]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('미읽음 알림 수 조회 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification.user.unread_count_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림을 읽음 처리합니다.
|
||||
*/
|
||||
public function markAsRead(Request $request, string $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$notification = $this->notificationService->markAsRead($request->user(), $id);
|
||||
|
||||
if (! $notification) {
|
||||
return $this->error(__('notification.user.not_found'), 404);
|
||||
}
|
||||
|
||||
return $this->success(
|
||||
__('notification.user.read_success'),
|
||||
new UserNotificationResource($notification)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('알림 읽음 처리 실패', ['id' => $id, 'error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification.user.read_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 지정된 알림들을 일괄 읽음 처리합니다.
|
||||
*/
|
||||
public function markBatchAsRead(NotificationBatchReadRequest $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$ids = $request->validated()['ids'];
|
||||
|
||||
$count = $this->notificationService->markBatchAsRead($request->user(), $ids);
|
||||
|
||||
return $this->success(
|
||||
__('notification.user.read_success'),
|
||||
['marked_count' => $count]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('알림 일괄 읽음 처리 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification.user.read_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 알림을 읽음 처리합니다.
|
||||
*/
|
||||
public function markAllAsRead(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$count = $this->notificationService->markAllAsRead($request->user());
|
||||
|
||||
return $this->success(
|
||||
__('notification.user.read_all_success'),
|
||||
['marked_count' => $count]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('전체 읽음 처리 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification.user.read_all_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 모든 알림을 삭제합니다.
|
||||
*/
|
||||
public function destroyAll(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$count = $this->notificationService->deleteAllNotifications($request->user());
|
||||
|
||||
return $this->success(
|
||||
__('notification.user.delete_all_success'),
|
||||
['deleted_count' => $count]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('전체 알림 삭제 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification.user.delete_all_failed'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림을 삭제합니다.
|
||||
*/
|
||||
public function destroy(Request $request, string $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$deleted = $this->notificationService->deleteNotification($request->user(), $id);
|
||||
|
||||
if (! $deleted) {
|
||||
return $this->error(__('notification.user.not_found'), 404);
|
||||
}
|
||||
|
||||
return $this->success(__('notification.user.delete_success'));
|
||||
} catch (\Exception $e) {
|
||||
Log::error('알림 삭제 실패', ['id' => $id, 'error' => $e->getMessage()]);
|
||||
|
||||
return $this->error(__('notification.user.delete_failed'), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,8 +130,7 @@ class ProfileController extends AuthBaseController
|
||||
return $this->unauthorized('auth.unauthenticated');
|
||||
}
|
||||
|
||||
// TODO: 사용자 활동 로그 서비스 구현
|
||||
$activities = []; // $this->userService->getUserActivities($user->id);
|
||||
$activities = $this->userService->getUserActivityLogs($user->id);
|
||||
|
||||
$this->logUserActivity('profile.activity_log');
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\Base;
|
||||
|
||||
use App\Contracts\Extension\CacheInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
@@ -22,6 +23,10 @@ abstract class PublicBaseController extends BaseApiController
|
||||
/**
|
||||
* 캐시된 응답을 반환하거나 새로 생성합니다.
|
||||
*
|
||||
* 주의: 콜백 반환값이 배열이고 `error` 키가 있으면 캐시하지 않는다.
|
||||
* 이는 확장 설치 직후·활성화 전 등의 일시적 상태에서 얻은 "not found" 같은
|
||||
* 에러 응답이 영구 캐시되어 복구 후에도 잘못된 응답을 반환하는 문제를 방지한다.
|
||||
*
|
||||
* @param string $key 캐시 키
|
||||
* @param callable $callback 데이터 생성 콜백
|
||||
* @param int $ttl 캐시 유지 시간 (초)
|
||||
@@ -29,7 +34,21 @@ abstract class PublicBaseController extends BaseApiController
|
||||
*/
|
||||
protected function cached(string $key, callable $callback, int $ttl = 3600)
|
||||
{
|
||||
return cache()->remember($key, $ttl, $callback);
|
||||
$cache = app(CacheInterface::class);
|
||||
|
||||
$cached = $cache->get($key);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$result = $callback();
|
||||
|
||||
// 에러 응답은 캐시하지 않음 (일시적 상태 회복 가능성 보장)
|
||||
if (! (is_array($result) && isset($result['error']))) {
|
||||
$cache->put($key, $result, $ttl);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -57,7 +57,7 @@ class PublicSearchController extends PublicBaseController
|
||||
$response = $this->buildResponse($q, $results, $context);
|
||||
|
||||
return $this->success(
|
||||
__('search.results_found', ['count' => $response['total']]),
|
||||
__('search.results_found', ['count' => number_format($response['total'])]),
|
||||
$response
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\Public;
|
||||
|
||||
use App\Contracts\Extension\CacheInterface;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Seo\SitemapGenerator;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* Sitemap XML 컨트롤러
|
||||
@@ -21,20 +21,21 @@ class SitemapController extends Controller
|
||||
* 캐시가 있으면 캐시된 XML, 없으면 실시간 생성 후 캐시 저장.
|
||||
*
|
||||
* @param SitemapGenerator $generator Sitemap 생성기
|
||||
* @param CacheInterface $cache 코어 캐시 드라이버
|
||||
*/
|
||||
public function index(SitemapGenerator $generator): Response
|
||||
public function index(SitemapGenerator $generator, CacheInterface $cache): Response
|
||||
{
|
||||
$enabled = (bool) g7_core_settings('seo.sitemap_enabled', true);
|
||||
if (! $enabled) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$xml = Cache::get('seo:sitemap');
|
||||
$xml = $cache->get('seo.sitemap');
|
||||
|
||||
if (! $xml) {
|
||||
$xml = $generator->generate();
|
||||
$ttl = (int) g7_core_settings('seo.sitemap_cache_ttl', 86400);
|
||||
Cache::put('seo:sitemap', $xml, $ttl);
|
||||
$ttl = (int) g7_core_settings('cache.seo_sitemap_ttl', g7_core_settings('seo.sitemap_cache_ttl', 86400));
|
||||
$cache->put('seo.sitemap', $xml, $ttl);
|
||||
}
|
||||
|
||||
return response($xml, 200, [
|
||||
|
||||
@@ -52,35 +52,60 @@ class SetTimezone
|
||||
*/
|
||||
private function determineTimezone(Request $request): string
|
||||
{
|
||||
$supportedTimezones = config('app.supported_timezones', ['Asia/Seoul', 'UTC']);
|
||||
|
||||
// 1. 인증된 사용자의 타임존 설정 확인 (최우선)
|
||||
if (Auth::check() && Auth::user()->timezone) {
|
||||
$userTimezone = Auth::user()->timezone;
|
||||
if (in_array($userTimezone, $supportedTimezones)) {
|
||||
if ($this->isValidTimezone($userTimezone)) {
|
||||
return $userTimezone;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. X-Timezone 헤더 확인 (브라우저 타임존)
|
||||
$browserTimezone = $request->header('X-Timezone');
|
||||
if ($browserTimezone && in_array($browserTimezone, $supportedTimezones)) {
|
||||
if ($browserTimezone && $this->isValidTimezone($browserTimezone)) {
|
||||
return $browserTimezone;
|
||||
}
|
||||
|
||||
// 3. IP 기반 GeoIP 타임존 감지
|
||||
$geoTimezone = $this->geoIpService->getTimezoneByIp(
|
||||
$request->ip(),
|
||||
$supportedTimezones
|
||||
);
|
||||
if ($geoTimezone) {
|
||||
return $geoTimezone;
|
||||
// 마스터 스위치(관리자 > 환경설정 > 고급)가 OFF면 이 단계 전체 스킵.
|
||||
// g7_core_settings()는 코어 설정의 정식 읽기 경로로, config('geoip.enabled')
|
||||
// 파이프라인(SettingsServiceProvider::applyGeoIpConfig)이 실패하더라도
|
||||
// 직접 소스 오브 트루스를 참조하여 안전하게 동작한다.
|
||||
// 전 지역 지원 정책(GeoIpService 두 번째 인자)에 따라 IANA 전체 목록을 전달한다.
|
||||
if ((bool) g7_core_settings('geoip.feature_enabled', false)) {
|
||||
$geoTimezone = $this->geoIpService->getTimezoneByIp(
|
||||
$request->ip(),
|
||||
\DateTimeZone::listIdentifiers()
|
||||
);
|
||||
if ($geoTimezone) {
|
||||
return $geoTimezone;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 기본값 반환
|
||||
return config('app.default_user_timezone', 'Asia/Seoul');
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 문자열이 유효한 IANA 타임존 식별자인지 확인합니다.
|
||||
*
|
||||
* PHP 내장 DateTimeZone 생성자를 사용하여 검증합니다.
|
||||
* 전 지역 IANA 타임존을 허용하므로 별도 화이트리스트를 사용하지 않습니다.
|
||||
*
|
||||
* @param string $timezone 검증할 타임존 문자열
|
||||
* @return bool 유효 여부
|
||||
*/
|
||||
private function isValidTimezone(string $timezone): bool
|
||||
{
|
||||
try {
|
||||
new \DateTimeZone($timezone);
|
||||
|
||||
return true;
|
||||
} catch (\Exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 요청에 설정된 타임존을 가져옵니다.
|
||||
*
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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',
|
||||
|
||||
// 연락처 정보
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Http\Resources\Traits\HasRowNumber;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
/**
|
||||
@@ -12,9 +10,17 @@ use Illuminate\Pagination\LengthAwarePaginator;
|
||||
*
|
||||
* 활동 로그 목록을 페이지네이션과 함께 반환합니다.
|
||||
*/
|
||||
class ActivityLogCollection extends ResourceCollection
|
||||
class ActivityLogCollection extends BaseApiCollection
|
||||
{
|
||||
use HasRowNumber;
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function abilityMap(): array
|
||||
{
|
||||
return [
|
||||
'can_delete' => 'core.activities.delete',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 활동 로그 컬렉션을 배열로 변환합니다.
|
||||
@@ -24,6 +30,8 @@ class ActivityLogCollection extends ResourceCollection
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$abilities = $this->resolveCollectionAbilities($request);
|
||||
|
||||
return [
|
||||
'data' => $this->mapWithRowNumber(function ($activityLog) {
|
||||
return (new ActivityLogResource($activityLog))->toArray(request());
|
||||
@@ -37,6 +45,7 @@ class ActivityLogCollection extends ResourceCollection
|
||||
'to' => $this->resource->lastItem(),
|
||||
'has_more_pages' => $this->resource->hasMorePages(),
|
||||
] : null,
|
||||
...($abilities ? ['abilities' => $abilities] : []),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Helpers\PermissionHelper;
|
||||
use App\Http\Resources\Traits\HasRowNumber;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
/**
|
||||
* API 컬렉션 리소스 기본 클래스
|
||||
*
|
||||
* 모든 API Collection은 이 클래스를 상속해야 합니다.
|
||||
* - HasRowNumber: 순번 부여
|
||||
* - abilityMap(): 컬렉션 레벨 abilities (페이지 버튼 제어)
|
||||
*/
|
||||
abstract class BaseApiCollection extends ResourceCollection
|
||||
{
|
||||
use HasRowNumber;
|
||||
|
||||
/**
|
||||
* 컬렉션 레벨 능력(can_*) 매핑을 반환합니다.
|
||||
*
|
||||
* 페이지 레벨 버튼 제어용 (생성, 일괄삭제 등).
|
||||
* abilities가 불필요한 컬렉션은 오버라이드하지 않습니다.
|
||||
*
|
||||
* @return array<string, string> ['can_delete' => 'permission.identifier', ...]
|
||||
*/
|
||||
protected function abilityMap(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 컬렉션 레벨 abilities를 해석합니다.
|
||||
*
|
||||
* @param Request $request HTTP 요청 객체
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
public function resolveCollectionAbilities(Request $request): array
|
||||
{
|
||||
$map = $this->abilityMap();
|
||||
if (empty($map)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
$abilities = [];
|
||||
|
||||
foreach ($map as $key => $permission) {
|
||||
$abilities[$key] = PermissionHelper::check($permission, $user);
|
||||
}
|
||||
|
||||
return $abilities;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Http\Resources\Traits\HasRowNumber;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
/**
|
||||
* 메일 발송 이력 컬렉션 리소스
|
||||
*
|
||||
* 발송 이력 목록을 페이지네이션과 함께 반환합니다.
|
||||
*/
|
||||
class MailSendLogCollection extends ResourceCollection
|
||||
{
|
||||
use HasRowNumber;
|
||||
|
||||
/**
|
||||
* 메일 발송 이력 컬렉션을 배열로 변환합니다.
|
||||
*
|
||||
* @param Request $request HTTP 요청 객체
|
||||
* @return array<int|string, mixed> 변환된 컬렉션 배열
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'data' => $this->mapWithRowNumber(function ($mailSendLog) {
|
||||
return (new MailSendLogResource($mailSendLog))->toArray(request());
|
||||
}),
|
||||
'pagination' => $this->resource instanceof LengthAwarePaginator ? [
|
||||
'current_page' => $this->resource->currentPage(),
|
||||
'last_page' => $this->resource->lastPage(),
|
||||
'per_page' => $this->resource->perPage(),
|
||||
'total' => $this->resource->total(),
|
||||
'from' => $this->resource->firstItem(),
|
||||
'to' => $this->resource->lastItem(),
|
||||
'has_more_pages' => $this->resource->hasMorePages(),
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* 메일 발송 이력 API 리소스
|
||||
*/
|
||||
class MailSendLogResource extends BaseApiResource
|
||||
{
|
||||
/**
|
||||
* 권한 매핑을 반환합니다.
|
||||
*
|
||||
* @return array<string, string> 능력명 => 권한 식별자 매핑
|
||||
*/
|
||||
protected function abilityMap(): array
|
||||
{
|
||||
return [
|
||||
'can_delete' => 'core.mail-send-logs.delete',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 리소스를 배열로 변환합니다.
|
||||
*
|
||||
* @param Request $request HTTP 요청 객체
|
||||
* @return array<string, mixed> 변환된 배열 데이터
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'sender_email' => $this->sender_email,
|
||||
'sender_name' => $this->sender_name,
|
||||
'recipient_email' => $this->recipient_email,
|
||||
'recipient_name' => $this->recipient_name,
|
||||
'subject' => $this->subject,
|
||||
'body' => $this->body,
|
||||
'template_type' => $this->template_type,
|
||||
'extension_type' => $this->extension_type?->value,
|
||||
'extension_identifier' => $this->extension_identifier,
|
||||
'source' => $this->source,
|
||||
'status' => $this->status,
|
||||
'error_message' => $this->error_message,
|
||||
'sent_at' => $this->formatDateTimeStringForUser($this->sent_at),
|
||||
...$this->formatTimestamps(),
|
||||
...$this->resourceMeta($request),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Http\Resources\Traits\HasRowNumber;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
/**
|
||||
* 메일 템플릿 컬렉션 리소스
|
||||
*
|
||||
* 템플릿 목록을 페이지네이션과 함께 반환합니다.
|
||||
*/
|
||||
class MailTemplateCollection extends ResourceCollection
|
||||
{
|
||||
use HasRowNumber;
|
||||
|
||||
/**
|
||||
* 메일 템플릿 컬렉션을 배열로 변환합니다.
|
||||
*
|
||||
* @param Request $request HTTP 요청 객체
|
||||
* @return array<int|string, mixed> 변환된 컬렉션 배열
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'data' => $this->mapWithRowNumber(function ($mailTemplate) {
|
||||
return (new MailTemplateResource($mailTemplate))->toArray(request());
|
||||
}),
|
||||
'pagination' => $this->resource instanceof LengthAwarePaginator ? [
|
||||
'current_page' => $this->resource->currentPage(),
|
||||
'last_page' => $this->resource->lastPage(),
|
||||
'per_page' => $this->resource->perPage(),
|
||||
'total' => $this->resource->total(),
|
||||
'from' => $this->resource->firstItem(),
|
||||
'to' => $this->resource->lastItem(),
|
||||
'has_more_pages' => $this->resource->hasMorePages(),
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class MailTemplateResource extends BaseApiResource
|
||||
{
|
||||
/**
|
||||
* 리소스를 배열로 변환합니다.
|
||||
*
|
||||
* @param Request $request HTTP 요청
|
||||
* @return array<string, mixed> 리소스 배열
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->getValue('id'),
|
||||
'type' => $this->getValue('type'),
|
||||
'subject' => $this->getValue('subject'),
|
||||
'body' => $this->getValue('body'),
|
||||
'variables' => $this->getValue('variables'),
|
||||
'is_active' => (bool) $this->getValue('is_active'),
|
||||
'is_default' => (bool) $this->getValue('is_default'),
|
||||
'updated_by' => $this->updater?->uuid,
|
||||
...$this->formatTimestamps(),
|
||||
...$this->resourceMeta($request),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,8 @@ namespace App\Http\Resources;
|
||||
|
||||
use App\Http\Resources\Traits\HasAbilityCheck;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class MenuCollection extends ResourceCollection
|
||||
class MenuCollection extends BaseApiCollection
|
||||
{
|
||||
use HasAbilityCheck;
|
||||
|
||||
|
||||
@@ -3,10 +3,21 @@
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class ModuleCollection extends ResourceCollection
|
||||
class ModuleCollection extends BaseApiCollection
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function abilityMap(): array
|
||||
{
|
||||
return [
|
||||
'can_install' => 'core.modules.install',
|
||||
'can_activate' => 'core.modules.activate',
|
||||
'can_uninstall' => 'core.modules.uninstall',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 컬렉션을 배열로 변환합니다.
|
||||
*
|
||||
|
||||
@@ -67,6 +67,7 @@ class ModuleResource extends BaseApiResource
|
||||
'version' => $this->getValue('version'),
|
||||
'description' => $this->getLocalizedField('description'),
|
||||
'github_url' => $this->getValue('github_url'),
|
||||
'requires_core' => $this->getValue('requires_core'),
|
||||
'dependencies' => $this->getValue('dependencies', []),
|
||||
'status' => $this->getValue('status'),
|
||||
'is_installed' => $this->getValue('is_installed', false),
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NotificationDefinitionCollection extends BaseApiCollection
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function abilityMap(): array
|
||||
{
|
||||
return [
|
||||
'can_update' => 'core.settings.update',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 컬렉션을 배열로 변환합니다.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$sortOrder = $request->input('sort_order', 'asc');
|
||||
$abilities = $this->resolveCollectionAbilities($request);
|
||||
|
||||
return [
|
||||
'data' => $this->mapWithRowNumber(function ($definition) use ($request) {
|
||||
return (new NotificationDefinitionResource($definition))->toArray($request);
|
||||
}, $sortOrder),
|
||||
'pagination' => [
|
||||
'current_page' => $this->currentPage(),
|
||||
'last_page' => $this->lastPage(),
|
||||
'per_page' => $this->perPage(),
|
||||
'total' => $this->total(),
|
||||
'from' => $this->firstItem(),
|
||||
'to' => $this->lastItem(),
|
||||
'has_more_pages' => $this->hasMorePages(),
|
||||
],
|
||||
...($abilities ? ['abilities' => $abilities] : []),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NotificationDefinitionResource extends BaseApiResource
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function abilityMap(): array
|
||||
{
|
||||
return [
|
||||
'can_update' => 'core.settings.update',
|
||||
'can_delete' => 'core.settings.update',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 리소스를 배열로 변환합니다.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->getValue('id'),
|
||||
'type' => $this->getValue('type'),
|
||||
'hook_prefix' => $this->getValue('hook_prefix'),
|
||||
'extension_type' => $this->getValue('extension_type'),
|
||||
'extension_identifier' => $this->getValue('extension_identifier'),
|
||||
'name' => $this->getValue('name'),
|
||||
'description' => $this->getValue('description'),
|
||||
'variables' => $this->getValue('variables'),
|
||||
'channels' => $this->getValue('channels'),
|
||||
'hooks' => $this->getValue('hooks'),
|
||||
'is_active' => (bool) $this->getValue('is_active'),
|
||||
'is_default' => (bool) $this->getValue('is_default'),
|
||||
'templates' => $this->relationLoaded('templates')
|
||||
? NotificationTemplateResource::collection($this->templates)
|
||||
: null,
|
||||
...$this->formatTimestamps(),
|
||||
...$this->resourceMeta($request),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NotificationLogCollection extends BaseApiCollection
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function abilityMap(): array
|
||||
{
|
||||
return [
|
||||
'can_delete' => 'core.notification-logs.delete',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 컬렉션을 배열로 변환합니다.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$sortOrder = $request->input('sort_order', 'desc');
|
||||
$abilities = $this->resolveCollectionAbilities($request);
|
||||
|
||||
return [
|
||||
'data' => $this->mapWithRowNumber(function ($log) use ($request) {
|
||||
return (new NotificationLogResource($log))->toArray($request);
|
||||
}, $sortOrder),
|
||||
'pagination' => [
|
||||
'current_page' => $this->resource->currentPage(),
|
||||
'last_page' => $this->resource->lastPage(),
|
||||
'per_page' => $this->resource->perPage(),
|
||||
'total' => $this->resource->total(),
|
||||
'from' => $this->resource->firstItem(),
|
||||
'to' => $this->resource->lastItem(),
|
||||
'has_more_pages' => $this->resource->hasMorePages(),
|
||||
],
|
||||
...($abilities ? ['abilities' => $abilities] : []),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NotificationLogResource extends BaseApiResource
|
||||
{
|
||||
/**
|
||||
* 리소스를 배열로 변환합니다.
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->getValue('id'),
|
||||
'channel' => $this->getValue('channel'),
|
||||
'notification_type' => $this->getValue('notification_type'),
|
||||
'extension_type' => $this->getValue('extension_type'),
|
||||
'extension_identifier' => $this->getValue('extension_identifier'),
|
||||
'recipient_user_id' => $this->getValue('recipient_user_id'),
|
||||
'recipient_identifier' => $this->getValue('recipient_identifier'),
|
||||
'recipient_name' => $this->getValue('recipient_name'),
|
||||
'sender_user_id' => $this->getValue('sender_user_id'),
|
||||
'sender' => $this->whenLoaded('senderUser', function () {
|
||||
$user = $this->resource->senderUser;
|
||||
|
||||
return $user ? [
|
||||
'uuid' => $user->uuid,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
] : null;
|
||||
}),
|
||||
'recipient' => $this->whenLoaded('recipientUser', function () {
|
||||
$user = $this->resource->recipientUser;
|
||||
|
||||
return $user ? [
|
||||
'uuid' => $user->uuid,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
] : null;
|
||||
}),
|
||||
'subject' => $this->getValue('subject'),
|
||||
'body' => $this->getValue('body'),
|
||||
'status' => $this->getValue('status')?->value ?? $this->getValue('status'),
|
||||
'error_message' => $this->getValue('error_message'),
|
||||
'source' => $this->getValue('source'),
|
||||
'sent_at' => $this->formatDateTimeStringForUser($this->resource->sent_at),
|
||||
...$this->formatTimestamps(),
|
||||
...$this->resourceMeta($request),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 리소스별 능력 매핑을 반환합니다.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function abilityMap(): array
|
||||
{
|
||||
return [
|
||||
'can_delete' => 'core.notification-logs.delete',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NotificationTemplateResource extends BaseApiResource
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function abilityMap(): array
|
||||
{
|
||||
return [
|
||||
'can_update' => 'core.settings.update',
|
||||
'can_delete' => 'core.settings.update',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 리소스를 배열로 변환합니다.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->getValue('id'),
|
||||
'definition_id' => $this->getValue('definition_id'),
|
||||
'channel' => $this->getValue('channel'),
|
||||
'subject' => $this->getValue('subject'),
|
||||
'body' => $this->getValue('body'),
|
||||
'click_url' => $this->getValue('click_url'),
|
||||
'recipients' => $this->enrichRecipients($this->getValue('recipients') ?? []),
|
||||
'is_active' => (bool) $this->getValue('is_active'),
|
||||
'is_default' => (bool) $this->getValue('is_default'),
|
||||
'user_overrides' => $this->getValue('user_overrides'),
|
||||
'updated_by' => $this->resource->updater?->uuid ?? null,
|
||||
...$this->formatTimestamps(),
|
||||
...$this->resourceMeta($request),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 수신자 규칙에 표시용 이름을 추가합니다.
|
||||
*
|
||||
* @param array $recipients 수신자 규칙 배열
|
||||
* @return array
|
||||
*/
|
||||
private function enrichRecipients(array $recipients): array
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
|
||||
return array_map(function (array $rule) use ($locale) {
|
||||
if ($rule['type'] === 'role' && ! empty($rule['value'])) {
|
||||
$role = Role::where('identifier', $rule['value'])->first();
|
||||
$rule['display_name'] = $role
|
||||
? ($role->name[$locale] ?? $role->name['ko'] ?? $rule['value'])
|
||||
: $rule['value'];
|
||||
}
|
||||
|
||||
if ($rule['type'] === 'specific_users' && ! empty($rule['value'])) {
|
||||
$users = User::whereIn('uuid', $rule['value'])->pluck('name', 'uuid');
|
||||
$rule['display_names'] = $users->values()->all();
|
||||
}
|
||||
|
||||
return $rule;
|
||||
}, $recipients);
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,13 @@
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
/**
|
||||
* 플러그인 컬렉션 리소스
|
||||
*
|
||||
* 플러그인 목록을 다양한 형태로 변환하여 반환합니다.
|
||||
*/
|
||||
class PluginCollection extends ResourceCollection
|
||||
class PluginCollection extends BaseApiCollection
|
||||
{
|
||||
/**
|
||||
* 플러그인 컬렉션을 배열로 변환합니다.
|
||||
|
||||
@@ -94,6 +94,7 @@ class PluginResource extends BaseApiResource
|
||||
'version' => $this->getValue('version', '1.0.0'),
|
||||
'description' => $this->getLocalizedField('description'),
|
||||
'github_url' => $this->getValue('github_url'),
|
||||
'requires_core' => $this->getValue('requires_core'),
|
||||
'dependencies' => $this->getValue('dependencies', []),
|
||||
'status' => $this->getValue('status', 'uninstalled'),
|
||||
'is_installed' => $this->getValue('is_installed', false),
|
||||
|
||||
@@ -4,9 +4,8 @@ namespace App\Http\Resources;
|
||||
|
||||
use App\Http\Resources\Traits\HasAbilityCheck;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class RoleCollection extends ResourceCollection
|
||||
class RoleCollection extends BaseApiCollection
|
||||
{
|
||||
use HasAbilityCheck;
|
||||
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
class ScheduleCollection extends ResourceCollection
|
||||
class ScheduleCollection extends BaseApiCollection
|
||||
{
|
||||
/**
|
||||
* 스케줄 컬렉션을 배열로 변환합니다.
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
class ScheduleHistoryCollection extends ResourceCollection
|
||||
class ScheduleHistoryCollection extends BaseApiCollection
|
||||
{
|
||||
/**
|
||||
* 스케줄 실행 이력 컬렉션을 배열로 변환합니다.
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
/**
|
||||
* 템플릿 컬렉션 리소스
|
||||
*
|
||||
* 템플릿 목록을 다양한 형태로 변환하여 반환합니다.
|
||||
*/
|
||||
class TemplateCollection extends ResourceCollection
|
||||
class TemplateCollection extends BaseApiCollection
|
||||
{
|
||||
/**
|
||||
* 템플릿 컬렉션을 배열로 변환합니다.
|
||||
|
||||
@@ -83,6 +83,7 @@ class TemplateResource extends BaseApiResource
|
||||
'description' => $this->getLocalizedField('description'),
|
||||
'github_url' => $this->getValue('github_url'),
|
||||
'github_changelog_url' => $this->getValue('github_changelog_url'),
|
||||
'requires_core' => $this->getValue('requires_core'),
|
||||
// pending/bundled 상태
|
||||
'is_pending' => $this->getValue('is_pending', false),
|
||||
'is_bundled' => $this->getValue('is_bundled', false),
|
||||
|
||||
@@ -3,14 +3,11 @@
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Http\Resources\Traits\HasAbilityCheck;
|
||||
use App\Http\Resources\Traits\HasRowNumber;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class UserCollection extends ResourceCollection
|
||||
class UserCollection extends BaseApiCollection
|
||||
{
|
||||
use HasAbilityCheck;
|
||||
use HasRowNumber;
|
||||
|
||||
/**
|
||||
* 컬렉션 레벨 능력(can_*) 매핑을 반환합니다.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Contracts\Repositories\NotificationDefinitionRepositoryInterface;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UserNotificationCollection extends BaseApiCollection
|
||||
{
|
||||
|
||||
/**
|
||||
* 컬렉션을 배열로 변환합니다.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$sortOrder = $request->input('sort_order', 'desc');
|
||||
|
||||
// 알림 type 식별자 → 다국어 라벨 맵을 컬렉션 단위로 한 번만 로드 (N+1 회피)
|
||||
$user = $request->user();
|
||||
$typeLabelMap = app(NotificationDefinitionRepositoryInterface::class)
|
||||
->getLabelMap($user?->locale);
|
||||
|
||||
return [
|
||||
'data' => $this->mapWithRowNumber(function ($notification) use ($request, $typeLabelMap) {
|
||||
return (new UserNotificationResource($notification))
|
||||
->withTypeLabelMap($typeLabelMap)
|
||||
->toArray($request);
|
||||
}, $sortOrder),
|
||||
'pagination' => [
|
||||
'current_page' => $this->resource->currentPage(),
|
||||
'last_page' => $this->resource->lastPage(),
|
||||
'per_page' => $this->resource->perPage(),
|
||||
'total' => $this->resource->total(),
|
||||
'from' => $this->resource->firstItem(),
|
||||
'to' => $this->resource->lastItem(),
|
||||
'has_more_pages' => $this->resource->hasMorePages(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Contracts\Repositories\NotificationDefinitionRepositoryInterface;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UserNotificationResource extends BaseApiResource
|
||||
{
|
||||
/**
|
||||
* type 식별자 → 다국어 라벨 맵 (Collection에서 사전 주입).
|
||||
*
|
||||
* Collection에서 일괄 응답 시 N+1을 회피하기 위해 한 번만 로드한 맵을 주입받습니다.
|
||||
* 단일 Resource로 사용될 경우 null이며, toArray() 내부에서 즉시 조회합니다.
|
||||
*
|
||||
* @var array<string, string>|null
|
||||
*/
|
||||
protected ?array $typeLabelMap = null;
|
||||
|
||||
/**
|
||||
* 사전 주입된 type 라벨 맵을 설정합니다.
|
||||
*
|
||||
* @param array<string, string> $map
|
||||
* @return $this
|
||||
*/
|
||||
public function withTypeLabelMap(array $map): static
|
||||
{
|
||||
$this->typeLabelMap = $map;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 리소스를 배열로 변환합니다.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$data = $this->getValue('data') ?? [];
|
||||
$user = $request->user();
|
||||
$timezone = $user?->getTimezone() ?? config('app.default_user_timezone', 'Asia/Seoul');
|
||||
|
||||
$type = $data['type'] ?? $this->getValue('type');
|
||||
$typeLabel = $this->resolveTypeLabel($type, $user?->locale);
|
||||
|
||||
// 알림 클릭 시 이동 URL — click_url(템플릿 패턴 기반) 우선, 없으면 action_url(메일 CTA) fallback
|
||||
$innerData = $data['data'] ?? [];
|
||||
$url = $data['click_url'] ?? $innerData['action_url'] ?? null;
|
||||
|
||||
return [
|
||||
'id' => $this->getValue('id'),
|
||||
'type' => $type,
|
||||
'type_label' => $typeLabel,
|
||||
'subject' => $data['subject'] ?? null,
|
||||
'body' => $data['body'] ?? null,
|
||||
'url' => $url,
|
||||
'data' => $data,
|
||||
'read_at' => $this->resource->read_at?->setTimezone($timezone)->format('Y-m-d H:i:s'),
|
||||
'created_at' => $this->resource->created_at?->setTimezone($timezone)->format('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* type 식별자에 해당하는 다국어 라벨을 해석합니다.
|
||||
*
|
||||
* 우선순위:
|
||||
* 1. Collection에서 사전 주입한 typeLabelMap
|
||||
* 2. Repository에서 즉시 조회 (단일 Resource 사용 시)
|
||||
* 3. 정의 미존재 시 type 식별자 그대로 반환 (UI에 raw 키 노출 방지 fallback)
|
||||
*
|
||||
* @param string|null $type
|
||||
* @param string|null $locale
|
||||
* @return string
|
||||
*/
|
||||
protected function resolveTypeLabel(?string $type, ?string $locale): string
|
||||
{
|
||||
if (! $type) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($this->typeLabelMap !== null) {
|
||||
return $this->typeLabelMap[$type] ?? '';
|
||||
}
|
||||
|
||||
$repository = app(NotificationDefinitionRepositoryInterface::class);
|
||||
$definition = $repository->getActiveByType($type);
|
||||
|
||||
return $definition?->getLocalizedName($locale) ?: '';
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user