Files
Gnuboard7/app/Http/Controllers/Concerns/ServesExtensionBundles.php
T
HeuJung b4ed33aa44 feat(core): 정적 게시 수명주기 보완 — 빌드 파괴·권한·무결성
https://github.com/gnuboard/g7/issues/122 제보자의 추가 지적 2건(빌드가 게시본을
지움 / CLI 최초 게시 후 웹 재생성 불가)에 대응하고, 같은 근본 원인을 공유하는
결함을 전수조사로 함께 고친다.

- 빌드가 자기 산출물만 교체한다: 루트 vite `emptyOutDir: false`
 기본값 true 는 폴백 없는 코어 3번들과 배달된 immutable URL 의 게시본을 함께 지웠다
- 게시 트리 권한을 웹이 이어받는다: 병합 전 프리플라이트 + 부모 그룹 상속·g+w
 종전 소유권 정상화는 root 축만 덮어 비-root CLI 계정은 무방비였다
- 갱신 → 생성 → 완전성 확인을 한 묶음으로: 기록 바이트 대조, .old 원자 스왑,
 rename 일시 거부 재시도, 프론트 JSON 파싱 검증
- 실패를 억제하고 기록한다: 버전 한정 실패 마커 + 사유별 대시보드 알림
 + ext-static:status 점검 커맨드
- 파생: catch-all 제외 목록을 에셋 화이트리스트 합집합에서 파생(mjs·webp·otf 누락),
 번들 디스크 쓰기 fail-soft·빈 번들 503, 활성 디렉토리 prune 회피
2026-08-28 08:02:22 +09:00

84 lines
3.6 KiB
PHP

<?php
namespace App\Http\Controllers\Concerns;
use App\Services\ExtensionBundleService;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* 확장(모듈/플러그인) 병합 번들 서빙의 공통 판정 (#122 E1/E2).
*
* 모듈 컨트롤러와 플러그인 컨트롤러가 **같은 판정**을 해야 한다. 각자 구현하면 한쪽만
* 고쳐진 채 다른 쪽이 조용히 옛 동작으로 남는다 — 실제로 두 컨트롤러는 지금까지 동일
* 코드를 복제하고 있었다.
*
* 판정은 셋으로 갈린다:
*
* 1. 병합 결과가 있고 디스크 캐시도 성공 → 파일 응답 (ETag/304/immutable 재사용)
* 2. 병합 결과는 있는데 디스크 캐시가 실패 → **메모리 결과를 그대로 200** 으로 서빙.
* 디스크 캐시는 최적화이므로 쓰기 실패가 공개 엔드포인트의 500 이 되면 안 된다.
* 3. 병합 결과가 비었음 →
* - 에셋을 선언한 활성 확장이 **0개**면 정상 → 빈 200
* - 선언은 있는데 결과가 0 이면 장애(배포 중 `dist` 가 잠깐 빔, 경로 어긋남) →
* **503**. 빈 200 으로 내보내면 프론트는 404 도 오류도 받지 못한 채 한참 뒤
* "Unknown action handler" 로 죽어, 원인이 번들이라는 사실이 드러나지 않는다.
*/
trait ServesExtensionBundles
{
/**
* 확장 병합 번들을 서빙합니다.
*
* @param ExtensionBundleService $bundleService 번들 서비스
* @param string $type 'module' | 'plugin'
* @param string $kind 'js' | 'css'
* @param string $mimeType 응답 Content-Type
* @return BinaryFileResponse|Response 번들 응답
*/
protected function bundleResponse(
ExtensionBundleService $bundleService,
string $type,
string $kind,
string $mimeType
): BinaryFileResponse|Response {
$version = $bundleService->getCurrentVersion();
$path = $bundleService->getBundleFilePath($type, $kind, $version);
if ($path !== '') {
return $this->fileResponse($path, $mimeType, 31536000);
}
// 파일 경로가 비었다 — 병합 결과 자체가 비었거나, 디스크 캐시가 실패했다.
$content = $bundleService->buildBundleContent($type, $kind);
if ($content !== '') {
// 디스크 캐시 실패 (2) — 메모리 결과로 서빙한다. 캐시 헤더는 붙이지 않는다:
// 이 응답은 캐시에 실패한 상태의 산출물이라 다음 요청에서 정상 경로로
// 돌아갈 수 있어야 한다.
return response($content, 200)
->header('Content-Type', $mimeType)
->header('Cache-Control', 'no-cache, private');
}
$declared = $bundleService->countAssetDeclaringExtensions($type, $kind);
if ($declared > 0) {
// (3) 장애 — 선언은 있는데 결과가 0
Log::error('확장 번들 병합 결과가 비었습니다 — 에셋을 선언한 활성 확장이 있습니다', [
'type' => $type,
'kind' => $kind,
'version' => $version,
'declared_extensions' => $declared,
]);
return response('', 503)
->header('Content-Type', $mimeType)
->header('Cache-Control', 'no-cache, private');
}
// (3) 정상 — 선언이 0개면 빈 번들이 맞다
return response('', 200)->header('Content-Type', $mimeType);
}
}