feat(core,admin_basic): 번들 비-ko 언어팩 자동 프로비저닝 + 미설치·드리프트 발견성

신규 설치/복구 시 비-ko/en 번들 언어팩(ja 등)이 자동으로 채워지지 않아
런타임에 오류 없이 조용히 ko 로 폴백하고, 운영자가 이를 알아채기 어려웠다.
두 결손을 함께 해소한다.

프로비저닝(SSoT):
- language-pack:provision 멱등 커맨드 신설. supported_locales ∖ {ko,en} 의
 미설치 번들 팩을 설치하고, 설치본 파일이 사라진 드리프트 팩(슬롯 점유로
 getUninstalledBundledPacks 가 못 잡는)도 복구 경로로 겸한다. 재실행 시 0건 수렴.

발견성:
- CLI language-pack:list 를 Service::list 경유로 전환해 미설치(uninstalled)와
 드리프트(active·파일 없음)를 함께 표면화.
- files_missing / bundled_source_available 파생 플래그를 Resource 에 노출.
- 관리자 언어팩 목록에 "파일 없음" 배지 + 원클릭 재설치(기존 install-bundled 모달
 재사용). 재설치 경로만 auto_activate 로 바인딩해 복구 후 active 를 유지한다.

회귀 방지:
- 정적 룰 lang-pack-supported-locale-has-bundle(선언 로케일 ↔ 번들 소스 부재) 신설,
 lang-pack-installed-version-drift 를 프로비저닝된 환경 한정으로 확장.
- 백엔드/레이아웃/E2E/시나리오 테스트 + 룰 fixture 추가.

부수 수정:
- 프로비저닝 작업 중 표면화된 ecommerce 언어팩 시더 테스트 격리결함 수정
 (modules 등록 행을 매 테스트가 직접 보장 — 프로세스당 1회 시딩 전제 붕괴 대응).
This commit is contained in:
HeuJung
2026-07-27 14:58:48 +09:00
parent 6b791679a6
commit 3d8890b118
23 changed files with 1574 additions and 30 deletions
+26
View File
@@ -224,6 +224,32 @@ UI 에서 라디오로 즉시 전환 가능. 활성 팩이 제거되면 slot 의
- 설치 실행 시 5단계의 모든 확장 install/activate 가 완료된 뒤 `php artisan language-pack:install {identifier} --source=bundled` 가 선택된 각 언어팩에 대해 호출됩니다(자동 활성화 default).
- 언어팩 1건 설치 실패는 best-effort 처리 — 전체 설치를 중단하지 않고 경고 로그만 남긴 뒤 다음 언어팩으로 진행합니다. 코어/모듈/플러그인 install 실패와 달리 rollback 을 발생시키지 않습니다.
## 프로비저닝과 드리프트 발견 (supported_locales)
코어의 `ko`/`en` 은 가상 보호 행으로 DB 설치 없이 항상 서빙되지만, 그 외 로케일(예: `ja`)의 번들 언어팩은 `lang-packs/_bundled/` 에 소스가 있어도 설치본 디렉토리로 복사·등록되어야 서빙됩니다. 이 비대칭 때문에 "설치본이 없거나 어긋난" 상태가 오류 없이 조용히 `ko` 로 폴백할 수 있어, 다음 도구로 프로비저닝·발견을 지원합니다.
### `language-pack:provision` (멱등 프로비저닝)
```bash
php artisan language-pack:provision # supported_locales 의 비-base 로케일을 대상으로 미설치 번들 팩 설치
php artisan language-pack:provision --locale=ja # 특정 로케일만
php artisan language-pack:provision --scope=core # 스코프 한정
```
- 대상 로케일 기본값은 `config('app.supported_locales')` 에서 base locale(ko/en)을 뺀 집합입니다. 즉 "사이트가 쓰겠다고 선언한 로케일" 만 채웁니다(불필요한 로케일 대량 설치 없음).
- 미설치 번들 팩(신규 설치)과 설치본 파일이 사라진 드리프트 팩(복구)을 함께 대상으로 삼습니다. 정상 설치된 팩은 어느 쪽에도 해당하지 않아 재실행해도 신규 설치가 0 건으로 수렴합니다(완전 멱등). fresh install · 복구 · 시더가 공유하는 단일 프로비저닝 경로입니다.
- 대상 확장 미설치/미활성 등 설치 차단 사유가 있는 팩은 건너뛰고 경고만 남깁니다(best-effort).
### 미설치·드리프트 발견
- `language-pack:list` 는 설치된 DB 행뿐 아니라 "번들에 있으나 미설치"(`uninstalled`) 및 "active 로 기록됐으나 설치본 파일 부재(드리프트)" 를 함께 표시합니다. 드리프트 행의 Status 는 `active (파일 없음)` 처럼 표기됩니다.
- 관리자 언어팩 목록 화면도 동일하게 드리프트 행에 "파일 없음" 배지와 원클릭 재설치 버튼을 노출합니다(번들 소스 재설치).
- 재설치 버튼은 **번들 소스가 실재하는 팩**에만 뜹니다. 설치 경로(zip/GitHub/URL)와 무관하게 동일 식별자의 `lang-packs/_bundled/` 소스가 있으면 복구할 수 있고, 소스가 없는 서드파티 팩은 배지로 발견만 되고 복구 버튼은 뜨지 않습니다(복구할 원본이 없기 때문).
### supported_locales ↔ 번들 소스 정합
`config/app.php` 의 `supported_locales` 에 비-base 로케일을 선언했다면, 대응하는 코어 번들 소스(`lang-packs/_bundled/g7-core-{locale}/`)가 존재해야 프로비저닝으로 채울 수 있습니다. 대응 소스가 없는 로케일을 선언하면 설치할 팩 자체가 없어 그 로케일이 조용히 `ko` 로 폴백합니다(복구 경로 없음). 이 정합은 정적 검사로 확인됩니다 — 소스를 추가하거나 미지원 로케일을 선언에서 제거하세요.
## 시더 통합 (HookManager 필터)
기존 시더에 `applyFilters` 1줄만 추가하면 자동으로 다국어 키가 병합됩니다.