fix(core,engine): OFFSET 순회 중 필터 컬럼 변경으로 인한 처리 누락 수정
Laravel chunk/each 는 OFFSET 기반이라, 콜백이 행의 필터 소속을 바꾸면 (필터 컬럼을 채우거나 매칭 행을 삭제하면) 다음 조회가 그만큼 앞으로 밀려 미처리 행을 조용히 건너뛴다. 250건/청크 100 이면 정확히 100건이 남는다. 코어·번들 확장 전수 조사로 확정한 6곳을 chunkById(키셋 순회)로 통일했다. 브라우저 검수(28축)에서 계획에 없던 결함 12건이 드러나 같은 사슬에서 수정했다. 이 중 layout_strategy=keep 무효화와 수정 감지 실패 시 "수정 없음" 오보는 사용자 작업 소실에 직결된다. 재발 방지로 audit 룰을 추가했고, 수정 6곳을 원복해 red 6건 / 복원 후 green 0건으로 판별력을 검증했다.
This commit is contained in:
1 parent
1866f0d566
commit
1eedbbdda1
55 files changed
+2764
-198
No files matched your search
@@ -3,9 +3,11 @@
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Enums\LanguagePackScope;
|
||||
use App\Extension\Vendor\VendorMode;
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Http\Controllers\Concerns\InjectsExtensionLanguagePacks;
|
||||
use App\Http\Controllers\Concerns\OrchestratesCascadeInstall;
|
||||
use App\Http\Requests\Extension\ChangelogRequest;
|
||||
use App\Http\Requests\Module\ActivateModuleRequest;
|
||||
use App\Http\Requests\Module\DeactivateModuleRequest;
|
||||
use App\Http\Requests\Module\IndexModuleRequest;
|
||||
@@ -16,10 +18,10 @@ use App\Http\Requests\Module\PerformModuleUpdateRequest;
|
||||
use App\Http\Requests\Module\PreviewModuleManifestRequest;
|
||||
use App\Http\Requests\Module\RefreshModuleLayoutsRequest;
|
||||
use App\Http\Requests\Module\UninstallModuleRequest;
|
||||
use App\Http\Requests\Extension\ChangelogRequest;
|
||||
use App\Http\Resources\ModuleCollection;
|
||||
use App\Http\Resources\ModuleResource;
|
||||
use App\Services\Extension\ExtensionInstallPreviewBuilder;
|
||||
use App\Services\LanguagePack\LanguagePackBundledRegistrar;
|
||||
use App\Services\LicenseService;
|
||||
use App\Services\ModuleService;
|
||||
use App\Services\TemplateService;
|
||||
@@ -169,7 +171,7 @@ class ModuleController extends AdminBaseController
|
||||
public function installPreview(string $moduleName, ExtensionInstallPreviewBuilder $builder): JsonResponse
|
||||
{
|
||||
try {
|
||||
$preview = $builder->build(\App\Enums\LanguagePackScope::Module, $moduleName);
|
||||
$preview = $builder->build(LanguagePackScope::Module, $moduleName);
|
||||
|
||||
return $this->success('module.fetch_success', $preview);
|
||||
} catch (\Exception $e) {
|
||||
@@ -188,7 +190,7 @@ class ModuleController extends AdminBaseController
|
||||
try {
|
||||
$validated = $request->validated();
|
||||
$moduleName = $validated['module_name'];
|
||||
$vendorMode = \App\Extension\Vendor\VendorMode::fromStringOrAuto(
|
||||
$vendorMode = VendorMode::fromStringOrAuto(
|
||||
$validated['vendor_mode'] ?? null
|
||||
);
|
||||
|
||||
@@ -253,7 +255,7 @@ class ModuleController extends AdminBaseController
|
||||
$moduleInfo = $result['module_info'] ?? null;
|
||||
|
||||
// 요구사항 #7: 재활성화 시 cascade 비활성화됐던 언어팩 목록 응답에 포함 (요구사항 #8: 빈 배열이면 모달 표시 안 함)
|
||||
$pendingLanguagePacks = app(\App\Services\LanguagePack\LanguagePackBundledRegistrar::class)
|
||||
$pendingLanguagePacks = app(LanguagePackBundledRegistrar::class)
|
||||
->getPendingForReactivation('module', $moduleName);
|
||||
|
||||
if ($moduleInfo) {
|
||||
@@ -490,6 +492,13 @@ class ModuleController extends AdminBaseController
|
||||
public function checkModifiedLayouts(string $moduleName): JsonResponse
|
||||
{
|
||||
try {
|
||||
// 미존재 식별자는 404 로 구분한다. 존재 확인 없이 조회하면 레이아웃 0건과
|
||||
// 모듈 부재가 똑같이 "수정된 레이아웃 없음" 으로 보고되어, 오타·제거된 모듈이
|
||||
// 조용히 "수정 없음" 으로 통과한다 (show/uninstall-info 와 동일 규약).
|
||||
if (! $this->moduleService->getModuleInfo($moduleName)) {
|
||||
return $this->error('module.not_found', 404, null, ['module' => $moduleName]);
|
||||
}
|
||||
|
||||
$result = $this->moduleService->checkModifiedLayouts($moduleName);
|
||||
|
||||
return $this->success('modules.check_modified_layouts_success', $result);
|
||||
@@ -515,7 +524,7 @@ class ModuleController extends AdminBaseController
|
||||
{
|
||||
try {
|
||||
$validated = $request->validated();
|
||||
$vendorMode = \App\Extension\Vendor\VendorMode::fromStringOrAuto(
|
||||
$vendorMode = VendorMode::fromStringOrAuto(
|
||||
$validated['vendor_mode'] ?? null
|
||||
);
|
||||
$layoutStrategy = $validated['layout_strategy'] ?? 'overwrite';
|
||||
@@ -524,14 +533,23 @@ class ModuleController extends AdminBaseController
|
||||
|
||||
$moduleInfo = $result['module_info'] ?? null;
|
||||
|
||||
// 메시지 치환 파라미터를 반드시 전달한다 — 누락 시 ":module"/":version"
|
||||
// 플레이스홀더가 그대로 사용자에게 노출된다.
|
||||
$messageParams = [
|
||||
'module' => $moduleName,
|
||||
'version' => $result['to_version'] ?? ($moduleInfo['version'] ?? ''),
|
||||
];
|
||||
|
||||
if ($moduleInfo) {
|
||||
return $this->successWithResource(
|
||||
'modules.update_success',
|
||||
new ModuleResource($moduleInfo)
|
||||
new ModuleResource($moduleInfo),
|
||||
200,
|
||||
$messageParams
|
||||
);
|
||||
}
|
||||
|
||||
return $this->success('modules.update_success', $result);
|
||||
return $this->success('modules.update_success', $result, 200, $messageParams);
|
||||
} catch (ValidationException $e) {
|
||||
// Service/Manager에서 이미 번역된 메시지를 errors에 포함하므로
|
||||
// 첫 번째 에러를 top-level message로 직접 사용 (이중 래핑 방지)
|
||||
@@ -601,7 +619,7 @@ class ModuleController extends AdminBaseController
|
||||
/**
|
||||
* 모듈의 라이선스 파일 내용을 반환합니다.
|
||||
*
|
||||
* @param string $identifier 모듈 식별자
|
||||
* @param string $identifier 모듈 식별자
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function license(string $identifier): JsonResponse
|
||||
|
||||
@@ -3,24 +3,26 @@
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Enums\LanguagePackScope;
|
||||
use App\Extension\Vendor\VendorMode;
|
||||
use App\Helpers\PermissionHelper;
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Http\Controllers\Concerns\InjectsExtensionLanguagePacks;
|
||||
use App\Http\Controllers\Concerns\OrchestratesCascadeInstall;
|
||||
use App\Http\Requests\Extension\ChangelogRequest;
|
||||
use App\Http\Requests\Plugin\ActivatePluginRequest;
|
||||
use App\Http\Requests\Plugin\DeactivatePluginRequest;
|
||||
use App\Http\Requests\Plugin\IndexPluginRequest;
|
||||
use App\Http\Requests\Plugin\InstallPluginFromFileRequest;
|
||||
use App\Http\Requests\Plugin\PreviewPluginManifestRequest;
|
||||
use App\Http\Requests\Plugin\InstallPluginFromGithubRequest;
|
||||
use App\Http\Requests\Plugin\InstallPluginRequest;
|
||||
use App\Http\Requests\Plugin\PerformPluginUpdateRequest;
|
||||
use App\Http\Requests\Plugin\PreviewPluginManifestRequest;
|
||||
use App\Http\Requests\Plugin\RefreshPluginLayoutsRequest;
|
||||
use App\Http\Requests\Plugin\UninstallPluginRequest;
|
||||
use App\Http\Requests\Extension\ChangelogRequest;
|
||||
use App\Http\Resources\PluginCollection;
|
||||
use App\Http\Resources\PluginResource;
|
||||
use App\Services\Extension\ExtensionInstallPreviewBuilder;
|
||||
use App\Services\LanguagePack\LanguagePackBundledRegistrar;
|
||||
use App\Services\LicenseService;
|
||||
use App\Services\PluginService;
|
||||
use App\Services\TemplateService;
|
||||
@@ -177,7 +179,7 @@ class PluginController extends AdminBaseController
|
||||
try {
|
||||
$validated = $request->validated();
|
||||
$pluginName = $validated['plugin_name'];
|
||||
$vendorMode = \App\Extension\Vendor\VendorMode::fromStringOrAuto(
|
||||
$vendorMode = VendorMode::fromStringOrAuto(
|
||||
$validated['vendor_mode'] ?? null
|
||||
);
|
||||
|
||||
@@ -242,7 +244,7 @@ class PluginController extends AdminBaseController
|
||||
$pluginInfo = $result['plugin_info'] ?? null;
|
||||
|
||||
// 요구사항 #7: 재활성화 시 cascade 비활성화됐던 언어팩 목록 응답에 포함
|
||||
$pendingLanguagePacks = app(\App\Services\LanguagePack\LanguagePackBundledRegistrar::class)
|
||||
$pendingLanguagePacks = app(LanguagePackBundledRegistrar::class)
|
||||
->getPendingForReactivation('plugin', $pluginName);
|
||||
|
||||
if ($pluginInfo) {
|
||||
@@ -498,6 +500,12 @@ class PluginController extends AdminBaseController
|
||||
public function checkModifiedLayouts(string $pluginName): JsonResponse
|
||||
{
|
||||
try {
|
||||
// 미존재 식별자는 404 로 구분한다. 존재 확인 없이 조회하면 레이아웃 0건과
|
||||
// 플러그인 부재가 똑같이 "수정된 레이아웃 없음" 으로 보고된다.
|
||||
if (! $this->pluginService->getPluginInfo($pluginName)) {
|
||||
return $this->error('plugins.not_found', 404, null, ['plugin' => $pluginName]);
|
||||
}
|
||||
|
||||
$result = $this->pluginService->checkModifiedLayouts($pluginName);
|
||||
|
||||
return $this->success('plugins.check_modified_layouts_success', $result);
|
||||
@@ -523,7 +531,7 @@ class PluginController extends AdminBaseController
|
||||
{
|
||||
try {
|
||||
$validated = $request->validated();
|
||||
$vendorMode = \App\Extension\Vendor\VendorMode::fromStringOrAuto(
|
||||
$vendorMode = VendorMode::fromStringOrAuto(
|
||||
$validated['vendor_mode'] ?? null
|
||||
);
|
||||
$layoutStrategy = $validated['layout_strategy'] ?? 'overwrite';
|
||||
@@ -532,14 +540,23 @@ class PluginController extends AdminBaseController
|
||||
|
||||
$pluginInfo = $result['plugin_info'] ?? null;
|
||||
|
||||
// 메시지 치환 파라미터를 반드시 전달한다 — 누락 시 ":plugin"/":version"
|
||||
// 플레이스홀더가 그대로 사용자에게 노출된다.
|
||||
$messageParams = [
|
||||
'plugin' => $pluginName,
|
||||
'version' => $result['to_version'] ?? ($pluginInfo['version'] ?? ''),
|
||||
];
|
||||
|
||||
if ($pluginInfo) {
|
||||
return $this->successWithResource(
|
||||
'plugins.update_success',
|
||||
new PluginResource($pluginInfo)
|
||||
new PluginResource($pluginInfo),
|
||||
200,
|
||||
$messageParams
|
||||
);
|
||||
}
|
||||
|
||||
return $this->success('plugins.update_success', $result);
|
||||
return $this->success('plugins.update_success', $result, 200, $messageParams);
|
||||
} catch (ValidationException $e) {
|
||||
// Service/Manager에서 이미 번역된 메시지를 errors에 포함하므로
|
||||
// 첫 번째 에러를 top-level message로 직접 사용 (이중 래핑 방지)
|
||||
@@ -623,7 +640,7 @@ class PluginController extends AdminBaseController
|
||||
/**
|
||||
* 플러그인의 라이선스 파일 내용을 반환합니다.
|
||||
*
|
||||
* @param string $identifier 플러그인 식별자
|
||||
* @param string $identifier 플러그인 식별자
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function license(string $identifier): JsonResponse
|
||||
|
||||
@@ -7,20 +7,21 @@ use App\Helpers\PermissionHelper;
|
||||
use App\Http\Controllers\Api\Base\AdminBaseController;
|
||||
use App\Http\Controllers\Concerns\InjectsExtensionLanguagePacks;
|
||||
use App\Http\Controllers\Concerns\OrchestratesCascadeInstall;
|
||||
use App\Http\Requests\Extension\ChangelogRequest;
|
||||
use App\Http\Requests\Template\ActivateTemplateRequest;
|
||||
use App\Http\Requests\Template\DeactivateTemplateRequest;
|
||||
use App\Http\Requests\Template\IndexTemplateRequest;
|
||||
use App\Http\Requests\Template\InstallTemplateFromFileRequest;
|
||||
use App\Http\Requests\Template\PreviewTemplateManifestRequest;
|
||||
use App\Http\Requests\Template\InstallTemplateFromGithubRequest;
|
||||
use App\Http\Requests\Template\InstallTemplateRequest;
|
||||
use App\Http\Requests\Template\PerformTemplateUpdateRequest;
|
||||
use App\Http\Requests\Template\PreviewTemplateManifestRequest;
|
||||
use App\Http\Requests\Template\RefreshTemplateLayoutsRequest;
|
||||
use App\Http\Requests\Template\UninstallTemplateRequest;
|
||||
use App\Http\Requests\Extension\ChangelogRequest;
|
||||
use App\Http\Resources\TemplateCollection;
|
||||
use App\Http\Resources\TemplateResource;
|
||||
use App\Services\Extension\ExtensionInstallPreviewBuilder;
|
||||
use App\Services\LanguagePack\LanguagePackBundledRegistrar;
|
||||
use App\Services\LicenseService;
|
||||
use App\Services\TemplateService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -215,7 +216,7 @@ class TemplateController extends AdminBaseController
|
||||
$templateInfo = $result['template_info'] ?? null;
|
||||
|
||||
// 요구사항 #7: 재활성화 시 cascade 비활성화됐던 언어팩 목록 응답에 포함
|
||||
$pendingLanguagePacks = app(\App\Services\LanguagePack\LanguagePackBundledRegistrar::class)
|
||||
$pendingLanguagePacks = app(LanguagePackBundledRegistrar::class)
|
||||
->getPendingForReactivation('template', $templateName);
|
||||
|
||||
if ($templateInfo) {
|
||||
@@ -435,6 +436,12 @@ class TemplateController extends AdminBaseController
|
||||
public function checkModifiedLayouts(string $templateName): JsonResponse
|
||||
{
|
||||
try {
|
||||
// 미존재 식별자는 404 로 구분한다. 존재 확인 없이 조회하면 레이아웃 0건과
|
||||
// 템플릿 부재가 똑같이 "수정된 레이아웃 없음" 으로 보고된다.
|
||||
if (! $this->templateService->getTemplateInfo($templateName)) {
|
||||
return $this->error('templates.not_found', 404, null, ['template' => $templateName]);
|
||||
}
|
||||
|
||||
$result = $this->templateService->checkModifiedLayouts($templateName);
|
||||
|
||||
return $this->success('templates.check_modified_layouts_success', $result);
|
||||
@@ -466,14 +473,23 @@ class TemplateController extends AdminBaseController
|
||||
|
||||
$templateInfo = $result['template_info'] ?? null;
|
||||
|
||||
// 메시지 치환 파라미터를 반드시 전달한다 — 누락 시 ":template"/":version"
|
||||
// 플레이스홀더가 그대로 사용자에게 노출된다.
|
||||
$messageParams = [
|
||||
'template' => $templateName,
|
||||
'version' => $result['to_version'] ?? ($templateInfo['version'] ?? ''),
|
||||
];
|
||||
|
||||
if ($templateInfo) {
|
||||
return $this->successWithResource(
|
||||
'templates.update_success',
|
||||
new TemplateResource($templateInfo)
|
||||
new TemplateResource($templateInfo),
|
||||
200,
|
||||
$messageParams
|
||||
);
|
||||
}
|
||||
|
||||
return $this->success('templates.update_success', $result);
|
||||
return $this->success('templates.update_success', $result, 200, $messageParams);
|
||||
} catch (ValidationException $e) {
|
||||
// Service/Manager에서 이미 번역된 메시지를 errors에 포함하므로
|
||||
// 첫 번째 에러를 top-level message로 직접 사용 (이중 래핑 방지)
|
||||
@@ -516,7 +532,7 @@ class TemplateController extends AdminBaseController
|
||||
/**
|
||||
* 템플릿의 라이선스 파일 내용을 반환합니다.
|
||||
*
|
||||
* @param string $identifier 템플릿 식별자
|
||||
* @param string $identifier 템플릿 식별자
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function license(string $identifier): JsonResponse
|
||||
|
||||
Reference in new issue
Block a user