공개 제보 https://github.com/gnuboard/g7/issues/122 대응 — 초기 부트스트랩 리소스(다국어 병합·컴포넌트 정의·라우트·확장 번들·템플릿 dist)를 캐시 버전 디렉토리(public/build/ext/{v}/)에 실파일로 게시해 웹서버가 rewrite 전에 직접 서빙한다. 부트 임계 경로의 PHP 왕복을 제거하고(실측 TTFB 131~144ms → 1~7ms), 미게시·부분게시·GC 직후에는 fetch·태그·번들 3계층이 종전 API 로 즉시 폴백한다. - 게시: 원자적 tmp→rename→manifest(존재=완료), 캐시 락 단일 실행, 인라인 GC (현재+직전 1개), incrementExtensionCacheVersion 단일 지점 terminating 트리거 + blade 자가 치유 + 설치기 태스크(best_effort) + 일일 cleanup 스케줄, sudo 업데이트 대비 소유권 정상화(normalizeOwnership)·prune/백업 제외 - 프론트(engine-v1.61.0): blade 주입 cache_version 1급 시드(이중 부트 로드 제거), fetchStaticFirst 즉시 폴백, ComponentRegistry 버전 키드 매니페스트, ModuleAssetLoader 번들 정적→레거시 폴백, asset-url-recovery staticToLegacy 역변환 - 폴백 API 품질: lang/components/routes ETag+304 + 환경 분기 Cache-Control, 열화 라우트 스냅샷 공개 캐시 금지(서버측 캐시 회피와 대칭), 게시 .htaccess mod_deflate + nginx gzip 스니펫(압축 전송량 회귀 방지) - SEO 정합: 봇 HTML 은 GC 대상 정적 URL 미사용(allowStatic:false), props $switch 봇측 해석 구현(engine-v1.56.0 패리티), 패리티 룰 expression-dialect 그룹 신설, 상주 allow 헤더 제거로 잠금 복원, _comment* 접두 주석 키 분류 - 검증: 전 수정 red→green 4단계, Playwright 라이브 21건, Chrome MCP 24축+M1~M3, 봇 curl 3축, 캐시 저장소(file/redis/database) 축 판정, 히스토리·공개이슈·커밋 이력 전수 재조사 반영 - 코어 7.0.10, engine-v1.61.0. kill-switch: G7_STATIC_CACHE=false
117 lines
7.2 KiB
PHP
117 lines
7.2 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
{!! g7_meta_generator_tag() !!}
|
|
|
|
<title>{{ config('app.name', '그누보드7') }} - Admin</title>
|
|
|
|
<!-- 템플릿 외부 리소스 (template.json의 externals) -->
|
|
@include('partials.template-externals-head')
|
|
|
|
<!-- Fallback UI 스타일 -->
|
|
@if(empty($activeAdminTemplate))
|
|
@include('partials.error-fallback-styles')
|
|
@endif
|
|
|
|
{{-- 자산 URL 자가 복구 헬퍼 — CSS <link> 의 onerror 보다 먼저 정의되어야 한다 --}}
|
|
@include('partials.asset-url-recovery')
|
|
|
|
<!-- 템플릿 컴포넌트 스타일 -->
|
|
@if(!empty($activeAdminTemplate))
|
|
{{-- onerror: 정적 최적화 서버에서 확장자 붙은 CSS 가 가로채였을 때
|
|
확장자 없는 형태로 1회 교체한다(무스타일 화면 방지). 링크당 1회. --}}
|
|
<link rel="stylesheet"
|
|
href="{{ \App\Support\AssetUrl::templateAsset($activeAdminTemplate, 'css/components.css', $extensionCacheVersion) }}"
|
|
onerror="window.__g7AssetUrl && window.__g7AssetUrl.recoverStylesheet(this);">
|
|
@endif
|
|
</head>
|
|
<body>
|
|
<!-- React 렌더링 루트 -->
|
|
<div id="app" data-template-id="{{ $activeAdminTemplate ?? '' }}">
|
|
<!-- Progressive Enhancement: 템플릿 없음 Fallback UI -->
|
|
@if(empty($activeAdminTemplate))
|
|
@include('partials.error-fallback-ui')
|
|
@endif
|
|
</div>
|
|
|
|
@if(!empty($activeAdminTemplate))
|
|
<!-- G7 설정 전역 변수 -->
|
|
<script>
|
|
window.G7Config = {
|
|
settings: @json($frontendSettings ?? []),
|
|
plugins: @json($pluginSettings ?? []),
|
|
modules: @json($moduleSettings ?? []),
|
|
moduleAssets: @json($moduleAssets ?? []),
|
|
pluginAssets: @json($pluginAssets ?? []),
|
|
bundleUrls: @json($bundleUrls ?? null),
|
|
activeModules: @json($activeModulesMeta ?? []),
|
|
activePlugins: @json($activePluginsMeta ?? []),
|
|
trustedScriptHosts: @json($trustedScriptHosts ?? []),
|
|
appConfig: @json($appConfig ?? []),
|
|
// 레이아웃 편집기 lazy 번들 URL — `/admin/layout-editor/*` 진입 시에만 런타임
|
|
// <script> 주입으로 로드된다(초기 접속 payload 에 미포함). filemtime 캐시버스팅,
|
|
// 미빌드 상태 대비 file_exists 가드.
|
|
coreEditorAsset: '{{ asset('build/core/layout-editor.min.js') }}?v={{ file_exists(public_path('build/core/layout-editor.min.js')) ? filemtime(public_path('build/core/layout-editor.min.js')) : 0 }}',
|
|
// DevTools lazy 번들 URL — 디버그 모드에서만 런타임 <script> 주입으로 로드.
|
|
coreDevToolsAsset: '{{ asset('build/core/devtools.min.js') }}?v={{ file_exists(public_path('build/core/devtools.min.js')) ? filemtime(public_path('build/core/devtools.min.js')) : 0 }}',
|
|
// 확장(코어/모듈/플러그인) 캐시 버전 SSoT — install/activate/deactivate/update 시 bump.
|
|
// 클라이언트 fetch (`?v=`) 가 이 값을 동반해야 백엔드 `template.routes.{id}.v{N}`
|
|
// 키가 새 버전으로 전환되어 routes.json/lang 변경이 즉시 가시화된다.
|
|
// 미주입 시 클라이언트가 항상 `v0` 으로 호출 → `template:cache-clear` 가 v 와일드카드를
|
|
// 처리하지 못해 캐시가 영구 stale 되는 결함이 발생.
|
|
cache_version: {{ (int) ($extensionCacheVersion ?? 0) }},
|
|
@if(($staticExtBase = \App\Support\AssetUrl::staticExtBase()) !== null)
|
|
// 정적 게시(bake) 베이스 — 게이트(프로덕션·kill-switch·게시 완료) 통과 시에만
|
|
// 주입된다. 프론트 로더가 routes/lang/components 를 이 경로에서 우선 수신 (#122).
|
|
staticBase: '{{ $staticExtBase }}',
|
|
@endif
|
|
// 자산 URL 모드 — 'extension'(기본) | 'extensionless'.
|
|
// 정적 최적화 블록이 동적 응답을 가로채는 서버에서 확장자 없는 형태로 전환.
|
|
// 부트스트랩 자가 복구가 런타임에 뒤집으므로 최상위 키로 노출한다.
|
|
// 자가 복구가 <head> 에서 이미 전환을 확정했다면 그 값을 잇는다.
|
|
// 이 대입은 G7Config 객체를 통째로 교체하므로, 독립 전역을 읽지 않으면
|
|
// 복원/전환 결과가 여기서 덮여 사라진다.
|
|
assetUrlMode: window.__g7AssetUrlMode || '{{ \App\Support\AssetUrl::mode() }}'
|
|
};
|
|
@if(isset($errorCode) && isset($errorLayout))
|
|
// 에러 상태 정보 (503 의존성 미충족 등)
|
|
window.G7Error = {
|
|
code: {{ $errorCode }},
|
|
layout: '{{ $errorLayout }}',
|
|
data: @json($unmetDependencies ?? [])
|
|
};
|
|
@endif
|
|
</script>
|
|
|
|
@include('partials.template-externals-scripts', ['position' => 'before-core'])
|
|
|
|
{{-- 코어 엔진 + 템플릿 컴포넌트 번들 로드 → 초기화 (재시도 + 폴백 UI) --}}
|
|
@include('partials.bootstrap-scripts', [
|
|
'templateType' => 'admin',
|
|
{{-- 코어 엔진 번들은 public/ 의 실물 정적 파일이라 자산 URL 이중 모드 대상이 아니다.
|
|
미빌드 상태에서 filemtime() 이 warning 을 내지 않도록 file_exists 가드
|
|
(coreEditorAsset/coreDevToolsAsset 와 동일 패턴). --}}
|
|
'coreEngineSrc' => asset('build/core/template-engine.min.js') . '?v=' . (file_exists(public_path('build/core/template-engine.min.js')) ? filemtime(public_path('build/core/template-engine.min.js')) : 0),
|
|
'componentsSrc' => \App\Support\AssetUrl::templateAsset($activeAdminTemplate, 'js/components.iife.js', $extensionCacheVersion),
|
|
'initConfig' => array_filter([
|
|
'templateId' => $activeAdminTemplate,
|
|
'templateType' => 'admin',
|
|
'locale' => app()->getLocale(),
|
|
'debug' => (bool) config('app.debug'),
|
|
'websocket' => config('broadcasting.connections.reverb.key') ? [
|
|
'appKey' => config('broadcasting.connections.reverb.key'),
|
|
'host' => config('g7.websocket.client.host', config('broadcasting.connections.reverb.options.host', 'localhost')),
|
|
'port' => (int) config('g7.websocket.client.port', config('broadcasting.connections.reverb.options.port', 80)),
|
|
'scheme' => config('g7.websocket.client.scheme', config('broadcasting.connections.reverb.options.scheme', 'https')),
|
|
] : null,
|
|
], fn ($value) => $value !== null),
|
|
])
|
|
|
|
@include('partials.template-externals-scripts', ['position' => 'body-end'])
|
|
@endif
|
|
</body>
|
|
</html>
|