fix(extension): 확장 수명주기 캐시 무효화 순서 회귀 및 실패 사유 전달

route:cache 는 새 앱을 부팅해 라우트를 수집하고, 그 부팅의 확장 라우트 프로바이더는
DB 가 아니라 캐시된 활성 확장 목록을 읽는다. 그래서 rebuild 가
invalidate*StatusCache 보다 앞서면 방금 바뀐 상태가 빠진 채 라우트가 박제되고,
라우트 캐시에는 스캔 폴백이 없어 오류도 로그도 없이 404 가 된다. 무효화를 굽기 직전이
아니라 DB 상태 쓰기 직후로 올려, 같은 목록을 읽는 훅 매핑 캐시까지 함께 바로잡았다.
update 경로는 Updating 전이 직후에 비우면 그 창의 오토로드 갱신이 확장을 비활성으로
판정하므로, 상태 복원 직후에 비운 뒤 훅 캐시를 다시 굽는다.

플러그인 라우트 프로바이더에는 활성 게이트가 없어 비활성 플러그인의 API 가 계속
응답했다. 화면·메뉴만 사라지고 기능은 살아 있는 상태였다. 모듈과 같은 기준을 적용했다.

실패 사유가 하위 계층에서 버려져 관리자 화면에 :error 자리표시자가 그대로 노출되던
문제도 고쳤다. 반환 경로를 깨지 않도록 배열 키 reason 과 뒤에 붙인 선택적 out
파라미터로 사유를 실어 올리고, 확장이 수명주기 훅에서 사유를 남길 수 있는 통로를
추가했다. 설치 경로의 광역 RuntimeException catch 는 도메인 예외로 좁혀 원본 키와
파라미터를 응답에 싣는다 — 상태코드 422 는 유지해 사용자 계약을 함께 바꾸지 않는다.
언어팩 화면은 프로덕션에서 예외 원문을 싣지 않는 것이 확정된 계약이므로, 자리를
일반 문구로 채우는 대신 치환 자리 자체를 제거했다. 원문은 종전대로 errors 통로를
거쳐 디버그 모드에서 도달한다.
This commit is contained in:
HeuJung committed 2026-08-21 17:09:30 +09:00
1 parent 565f6c0117
commit 03cbb99196
35 files changed
+1362 -239

No files matched your search

@@ -38,40 +38,29 @@ class GenericCatchStatusCodeContractTest extends TestCase
/**
* 예외 원문을 메시지 키로 넘기는 것이 아직 남아 있는 지점.
*
* 코어 확장 설치 경로는 `\RuntimeException(__('key', [...]))` 형태로 서비스·헬퍼
* 40여 곳에서 던져진다. 키를 들고 다니는 예외로 승격하려면 설치/업데이트 경로 전체를
* 건드려야 하므로 별도 작업으로 분리한다 — 여기 남겨 두는 이유는 "판정기가 못 봐서
* 통과한 것" 과 "알고 남긴 것" 을 구분하기 위해서다. 목록이 늘어나면 실패한다.
* 확장 설치 경로 6곳(모듈·플러그인·템플릿 × from-file/from-github)이 여기 있었다.
* 도메인 실패가 이미 `*OperationException`(errorKey + params)으로 승격되어 있었는데
* catch 만 부모 `\RuntimeException` 으로 남아, 이미 번역된 문장을 키 자리로 넘기고
* 있었다. typed catch 로 좁히고 원본 키·파라미터를 넘기도록 바꿔 전부 해소했다.
*
* 비운 채로 둔다 — 새 위반이 생기면 그 자리에서 실패해야 한다.
*
* @var array<int, string>
*/
private const KNOWN_EXCEPTION_TEXT_AS_KEY = [
'app/Http/Controllers/Api/Admin/ModuleController.php:437',
'app/Http/Controllers/Api/Admin/ModuleController.php:461',
'app/Http/Controllers/Api/Admin/PluginController.php:448',
'app/Http/Controllers/Api/Admin/PluginController.php:472',
'app/Http/Controllers/Api/Admin/TemplateController.php:352',
'app/Http/Controllers/Api/Admin/TemplateController.php:376',
];
private const KNOWN_EXCEPTION_TEXT_AS_KEY = [];
/**
* 광역 `\RuntimeException` catch 가 4xx 를 반환하는 것이 아직 남아 있는 지점.
*
* `\RuntimeException` 은 도메인 예외의 부모가 되기 쉬워, 도메인 실패를 typed 로
* 승격한 뒤에도 이 catch 를 남겨 두면 남는 것은 인프라 예외뿐인데 그것까지 4xx 로
* 뭉갠다. 위 `KNOWN_EXCEPTION_TEXT_AS_KEY` 와 같은 줄이며, 같은 설치 경로 개편에서
* 함께 해소된다.
* 뭉갠다. 위 `KNOWN_EXCEPTION_TEXT_AS_KEY` 와 같은 줄이었고 함께 해소되었다.
*
* 비운 채로 둔다 — 새 위반이 생기면 그 자리에서 실패해야 한다.
*
* @var array<string, string>
*/
private const KNOWN_BROAD_RUNTIME_CATCH_4XX = [
'app/Http/Controllers/Api/Admin/ModuleController.php::installFromFile' => '설치 경로가 도메인 실패를 키 없는 RuntimeException 으로 던진다 — 예외 승격과 함께 해소',
'app/Http/Controllers/Api/Admin/ModuleController.php::installFromGithub' => '동일',
'app/Http/Controllers/Api/Admin/PluginController.php::installFromFile' => '동일',
'app/Http/Controllers/Api/Admin/PluginController.php::installFromGithub' => '동일',
'app/Http/Controllers/Api/Admin/TemplateController.php::installFromFile' => '동일',
'app/Http/Controllers/Api/Admin/TemplateController.php::installFromGithub' => '동일',
];
private const KNOWN_BROAD_RUNTIME_CATCH_4XX = [];
/**
* 스캔 대상 컨트롤러 루트 목록 (코어 + 번들 모듈/플러그인).