Files
Gnuboard7/app/Http/View/Composers/TemplateComposer.php
T
HeuJung 15cd4452bf feat(core): 정적 최적화 서버 대응 자산 URL 이중 모드
nginx 의 정규식 location 은 프리픽스 location 보다 먼저 매칭되므로,
`location ~* \.(js|css|json)$` 블록이 있는 서버에서는 확장자 붙은 동적
엔드포인트가 `try_files ... /index.php` 폴백이 실행될 기회 없이 404 가 된다.
aaPanel/CyberPanel/Plesk 기본 템플릿에 들어있어 드물지 않으며, 관리자 화면조차
뜨지 않아 "서버 설정을 고치세요" 안내가 순환 참조가 된다.

관례를 깬 쪽이 G7 이므로 해소 책임도 G7 에 두고, 두 형태를 모두 서빙한 뒤
환경에 따라 택일한다. 확장자 형태는 영구 유지한다 — 제거하면 URL 을 하드코딩한
서드파티 확장이 깨진다.

- 라우트: dualSuffix / dualSuffixSegment / dualAsset 매크로로 23개 엔드포인트와
 프로브를 이중 등록. 확장자 형태를 먼저 등록한다 — 확장자 없는 쪽이 더 느슨한
 패턴이라 순서가 뒤집히면 `.json` 요청까지 삼킨다
- URL 생성: 서버 App\Support\AssetUrl, 프론트 core/support/assetUrl.ts 로 집약.
 기본 모드에서 생성 결과는 치환 이전과 문자열까지 동일하다 (쿼리 순서 포함 —
 순서가 바뀌면 의미는 같아도 HTTP 캐시 키가 갈린다)
- 자가 복구: 부트스트랩 자산 로드 실패 시 확장자 없는 형태로 1회 단방향 전환.
 역방향 금지 + 기존 재시도 예산 공유로 무한 왕복을 막는다
- 감지·전환: 인스톨러 프로브(설치 시 확정) / 관리자 환경설정 일반 탭 /
 g7:asset-url-mode. 판정은 상태코드가 아니라 매직 토큰 + Content-Type 으로 한다.
 상태코드만 보면 "404 대신 200 + 에러 HTML" 환경에서 영원히 오판한다
- 봇은 JavaScript 를 실행하지 않아 자가 복구가 닿지 않으므로, 모드 변경 시
 SEO 프리렌더 캐시를 비워 재생성시킨다

가드: audit 룰 2종(dynamic-route-static-extension / asset-url-builder-required),
Playwright 6건(정적 블록 가로채기 시뮬레이션), 루프 방지 불변식 L1~L9 전수 red 증명.

부수 정리: 양 Composer 에 중복돼 있던 확장 에셋 수집 123줄을 트레이트로 통합하고,
자산 확장자 화이트리스트 누락(.map)과 경로 정규화 우회 가능성을 함께 교정.

sir.kr 커뮤니티의 hang 님께서 제보해주셨습니다.
2026-07-23 11:34:32 +09:00

120 lines
4.7 KiB
PHP

<?php
namespace App\Http\View\Composers;
use App\Exceptions\TemplateNotFoundException;
use App\Extension\ModuleManager;
use App\Extension\PluginManager;
use App\Extension\TemplateManager;
use App\Extension\Traits\ClearsTemplateCaches;
use App\Http\View\Composers\Traits\CollectsActiveExtensionMeta;
use App\Http\View\Composers\Traits\CollectsExtensionAssets;
use App\Http\View\Composers\Traits\CollectsTemplateExternals;
use App\Services\ModuleSettingsService;
use App\Services\PluginSettingsService;
use App\Services\SettingsService;
use App\Services\TemplateService;
use Illuminate\View\View;
class TemplateComposer
{
use CollectsActiveExtensionMeta;
use CollectsExtensionAssets;
use CollectsTemplateExternals;
/**
* 서비스 주입
*
* @param TemplateService $templateService 템플릿 서비스
* @param SettingsService $settingsService 코어 설정 서비스
* @param ModuleSettingsService $moduleSettingsService 모듈 설정 서비스
* @param PluginSettingsService $pluginSettingsService 플러그인 설정 서비스
* @param ModuleManager $moduleManager 모듈 매니저
* @param PluginManager $pluginManager 플러그인 매니저
* @param TemplateManager $templateManager 템플릿 매니저
*/
public function __construct(
private TemplateService $templateService,
private SettingsService $settingsService,
private ModuleSettingsService $moduleSettingsService,
private PluginSettingsService $pluginSettingsService,
private ModuleManager $moduleManager,
private PluginManager $pluginManager,
private TemplateManager $templateManager
) {}
/**
* 뷰에 데이터 바인딩
*/
public function compose(View $view): void
{
try {
$activeTemplate = $this->templateService->getActiveTemplateIdentifier('admin');
} catch (TemplateNotFoundException $e) {
$activeTemplate = null;
}
// 프론트엔드 전역 변수로 사용할 설정 조회
// SettingsService의 공통 메서드 사용
try {
$frontendSettings = $this->settingsService->getFrontendSettings();
} catch (\Exception $e) {
$frontendSettings = [];
}
// 활성화된 플러그인 설정 조회
try {
$pluginSettings = $this->pluginSettingsService->getAllActiveSettings();
} catch (\Exception $e) {
$pluginSettings = [];
}
// 활성화된 모듈 설정 조회 (frontend_schema 기반 필터링 적용)
try {
$moduleSettings = $this->moduleSettingsService->getAllActiveSettings();
} catch (\Exception $e) {
$moduleSettings = [];
}
// 활성화된 모듈의 프론트엔드 에셋 정보 수집
$moduleAssets = $this->collectModuleAssets();
// 활성화된 플러그인의 프론트엔드 에셋 정보 수집
$pluginAssets = $this->collectPluginAssets();
// 활성 확장(모듈/플러그인) 메타 — 레이아웃 편집기 SSoT
// 기존 modules/plugins 키는 hasSettings() 필터로 활성 전수가 아님
$activeModulesMeta = $this->collectActiveModulesMeta();
$activePluginsMeta = $this->collectActivePluginsMeta();
// 프론트엔드에 노출할 앱 config 값 조회
try {
$appConfig = $this->settingsService->getAppConfigForFrontend();
} catch (\Exception $e) {
$appConfig = [];
}
// 템플릿의 외부 리소스 정보 수집
$templateExternals = $this->collectTemplateExternals($activeTemplate);
// 확장 기능 캐시 버전 (브라우저 캐시 무효화용)
$extensionCacheVersion = ClearsTemplateCaches::getExtensionCacheVersion();
// 확장 프론트엔드 병합 번들 URL (상시 ON — 활성 에셋이 없으면 null)
$bundleUrls = $this->buildExtensionBundleUrls($moduleAssets, $pluginAssets, $extensionCacheVersion);
$view->with('activeAdminTemplate', $activeTemplate);
$view->with('extensionCacheVersion', $extensionCacheVersion);
$view->with('frontendSettings', $frontendSettings);
$view->with('pluginSettings', $pluginSettings);
$view->with('moduleSettings', $moduleSettings);
$view->with('moduleAssets', $moduleAssets);
$view->with('pluginAssets', $pluginAssets);
$view->with('bundleUrls', $bundleUrls);
$view->with('activeModulesMeta', $activeModulesMeta);
$view->with('activePluginsMeta', $activePluginsMeta);
$view->with('appConfig', $appConfig);
$view->with('templateExternals', $templateExternals);
}
}