KVE-2026-2191 대응에서 계획 안 세부를 자율 결정한 지점을 회귀 전수조사해 정합 결함
5건을 고쳤다. 넷은 같은 미출시 버전이 만든 자기교정이고, 마지막 하나는 7.0.10 및 그
이전부터 있던 결함이라 공개 CHANGELOG 에 항목을 넣었다.
- 적중 통계의 레이아웃명이 항상 비어 화면별 표가 성립하지 않던 것 → 페이지 캐시 값에
레이아웃명을 함께 담고 적중 경로가 그것으로 귀속한다. 이전 버전이 문자열로만 저장한
항목은 그대로 읽어 배포 직후 살아 있는 캐시를 버리지 않는다. 인터페이스는 불변이고
코어 구현일 때만 항목 전체를 읽는다.
- 통계 url 컬럼(255)이 캐시 키 URL(정규화 쿼리 최대 512바이트)보다 짧아 긴 주소의 기록이
엄격 모드에서 실패하고 그 예외를 서비스가 삼키던 것 → 768 로 확장(utf8mb4 인덱스 키
상한 3072바이트). down 은 잘리는 행을 먼저 지운다.
- 경로당 변종 상한이 언어를 합산해 다국어 사이트의 실효 상한이 언어 수만큼 줄던 것 →
인덱스 항목이 url|locale 별이므로 경로·언어 조합 단위로 판정한다.
- 그릴 게 없는 요청(미라우트 404·SEO 비활성)이 렌더 예산을 소모해, 캐시에 남지 않는 죽은
주소 재크롤이 정상 페이지의 예산을 태우던 것 → null 렌더는 차감을 환급한다. 렌더 도중
예외는 비용을 이미 치른 것이라 환급하지 않는다.
- 병합 단계에서 건너뛴 확장이 있는 결과가 캐시로 굳어 버전 bump 전까지 그 확장 자산이
사라진 채 고정되던 것 → 병합 루프를 mergeJs/mergeCss 로 분리해 건너뛴 확장을 함께
돌려주고, 하나라도 있으면 캐시하지 않고 Log::error 로 남긴다. 캐시 없이 돌면 호출측이
매 요청 다시 병합하므로 원인이 사라지는 즉시 회복한다. 기본 로그 수준이 error 라
종전 per-extension warning 은 기록되지 않아 흔적이 없었다.
회귀 테스트 8건은 red 확인 후 green 으로 전환했고, 변경 심볼의 소비자 모집단(테스트
클래스 9종)을 프로세스 분할로 재실행해 189건 통과를 확인했다.
공개 번들 엔드포인트가 캐시 파일이 있어도 요청마다 다시 병합했다. 캐시 키는
(type, kind, version) 인자만으로 계산되는데 "병합 결과가 비면 파일을 만들지
않는다" 는 규칙을 먼저 두느라 빌드를 앞세운 것이 원인이다. 그래서 캐시 적중
경로에도 활성 확장 열거와 산출물 전량 읽기가 붙었고, 원본이 소실되면 멀쩡한
캐시를 두고 빈 경로가 반환되어 503 이 됐다. 응답은 정상 200 이라 타이밍 말고는
드러나는 증상이 없다.
프로덕션에서 캐시 존재를 병합보다 먼저 확인하고, 캐시 미스는 같은 키의 잠금으로
1회 빌드에 수렴시킨 뒤 잠금 뒤 캐시를 재확인한다. 잠금 대기 초과·저장소 장애는
실패로 바꾸지 않고 각자 빌드로 폴백한다.
병합 결과가 비어도 선언 산출물이 전부 존재하거나 선언이 0이면 0바이트 캐시를
만들어 정적 게시까지 보낸다. 만들지 않으면 그 구성의 자산 URL 이 API 로 폴백해
방문자의 모든 페이지 로드가 PHP 를 거치고, 그 요청마다 컨트롤러가 열거를 세 번
반복한다. 캐시하지 않는 것은 산출물 소실(503 판정 보존)과 디스크 쓰기 실패뿐이라
응답 계약은 바뀌지 않는다 — 컨트롤러와 트레이트는 손대지 않았다.
같은 결의 결함이 검색봇 캐시에도 있었다. 봇 판정은 User-Agent 문자열뿐인데 캐시
키가 경로 + 전체 쿼리여서, 물음표 뒤 값만 바꾼 반복 요청이 매번 미스가 되고 그
미스마다 레이아웃 병합·표현식 평가·자기 API 루프백 호출이 일어나며 결과가 무제한
저장됐다. 키를 정규화하고(시스템 파라미터 제외·개수/길이 상한), IP 당 분당 미스
렌더 예산과 저장 규모 상한을 뒀다. 초과분은 차단이 아니라 일반 SPA 응답을 받는다
— 봇에게 오류를 주면 그 URL 이 색인에서 빠지기 때문이다.
저장 상한은 만료 항목을 걷어낸 뒤 판정한다. 인덱스는 페이지보다 오래 살아
(30일 vs 기본 2시간) 정리 없이 세면 상한이 "지금 저장된 양"이 아니라 "과거에
저장한 적이 있는 양"을 재게 되어 일방향 래치가 된다. 정리는 인덱스 전체를
훑으므로 최소 60초 간격으로만 수행한다. put 과 putWithLayout 은 같은 자원을
쓰므로 단일 저장 경로로 합쳤다 — 한쪽만 상한 밖이면 그쪽이 우회로가 되고,
인터페이스는 확장에 열려 있어 "지금 호출부가 없다" 는 방어가 되지 않는다.
캐시 적중·미적중을 기록하는 호출처가 없어 관리자 SEO 통계와 seo:stats 가 항상
0 이었던 것도 함께 고쳤다. 기록 자체에도 IP 당 상한을 둬 통계 테이블이 새 증식
축이 되지 않게 했다.
(KISA 측에서 제보해주셨습니다 — KVE-2026-2191)
- 템플릿 update 는 레이아웃 변경 여부와 무관하게 캐시 버전을 올리고(실패 복원 뒤에도), 자산 주소 방식 전환 3경로도 bump 한다
- config:cache 가 컨테이너 인스턴스를 덮어 이후 terminating 재게시가 사라지던 결함을 복원 헬퍼로 차단
- custom/ 변경 감지와 게시가 같은 열거자(재귀·크기 포함)를 쓰고, 서명은 호스트별로 저장
- 캐시 버전·서명 키를 만료시키지 않는다(기본 TTL 24h 로 매일 전체 재생성되던 문제)
- 관리자 > 환경설정 > 일반 「초기 화면 정적 파일」 카드: 상태 조회 + 지금 다시 만들기(확인 모달), 대시보드 알림 버튼 연결, CLI 와 같은 statusReport 소비
- sudo core:update 경로의 root 소유 잔존(설정 디렉토리·임시 폴더·로그·업그레이드 마이그레이션 산출물) 상속·정합화, ext-static 명령 root 경고와 sudo -u 힌트
- 안내·문서·트러블슈팅 4건·audit 룰 ext-cache-version-raw-read·ja 언어팩 동기
빈 번들 503 을 "에셋을 선언한 활성 확장이 있는데 결과가 비었다" 로 판정해,
스타일 규칙이 아직 없는 0바이트 CSS 만 선언된 기본 구성이 통째로 503 이 됐다.
file_get_contents 는 0바이트에서 false 가 아니라 '' 를 돌려주므로 읽기 실패
분기도 타지 않는다. 선언 축 게터는 전부 file_exists 게이트라 부재를 셀 수 없어,
존재 게이트가 없는 getDeclaredAssetAbsolutePaths 를 통로로 두고 503 의 근거를
소실 축(findMissingDeclaredAssets)으로 옮긴다. 존재하되 비었으면 빈 200 이다.
곁들여: 실패 배너 항목명을 내부 구분 키에서 사용자 어휘로(engine-v1.64.7),
봇 화면이 없는 템플릿 CSS 를 링크하지 않도록 실재 게이트, 매니페스트 자산
선언 드리프트 4건 정정.
https://github.com/gnuboard/g7/issues/128 (제보자 glitter-gim) 과 그 부수의무
전수조사에서 확정한 결함군을 닫는다.
## 디버그 라우트 게이트 (F1~F5)
게이트가 핸들러 안에 흩어져 있어 8개 라우트 중 3개에만 붙어 있었다. 빠진 쪽에
`File::cleanDirectory(storage/debug-dump)` 를 수행하는 `DELETE clear` 가 있었고,
production·APP_DEBUG=false 에서도 미인증 200 으로 덤프 전체가 지워졌다. GET 4종은
User SPA catch-all 이 등록 순서상 앞서 가려 주고 있었을 뿐이라, 보호를 우연에 맡긴
구조였다.
게이트를 `bootstrap/app.php` 의 devtools 래퍼 한 곳(`debug.gate` 그룹 미들웨어)으로
올린다. 새 라우트는 자동으로 덮이고, 부착 지점이 하나라 훼손이 곧바로 드러난다.
`withRouting(channels:)` 인자가 함께 등록하던 게이트 없는 `/broadcasting/auth` 는
제거하고, 채널 정의 로드만 `BroadcastServiceProvider` 로 옮겨 웹소켓 킬스위치
우회로를 없앤다. catch-all 제외 패턴에는 예약 프리픽스를 전수 추가했다.
## 확장 자산 CSS 상대 참조 (범위 추가 — 결정)
`asset_url_mode=extensionless` 에서 자산 URL 이 쿼리 형태가 되면 브라우저의 기준
디렉토리가 `/api/{타입}/assets/` 로 잡혀, CSS 안의 `url('./woff2/f.woff2')` 가
존재하지 않는 주소가 된다. 사용자 템플릿 웹폰트 1건과 관리자 템플릿 국기 아이콘
약 500건이 이 상태였다. 병합 CSS 번들은 더 나빴다 — 상대 참조를 가진 확장을 번들에서
통째로 제외하고 있었는데, 번들 URL 이 내려오면 프론트는 개별 로딩을 아예 타지 않으므로
제외는 곧 그 확장 스타일이 하나도 적용되지 않음을 뜻했다.
둘 다 서빙 시점에 절대 자산 URL 로 치환한다. 최종 URL 은 런타임 모드와 캐시 버전이
정하므로 빌드 시점에는 알 수 없고, 동봉 자산은 제3자 산출물이라 원본을 손대면 상류
갱신마다 재작업이 된다. 개별 서빙 3경로와 병합 번들이 같은 해석 규칙을 공유한다 —
갈라지면 한쪽만 고쳐진 채 남는다.
이 결함군은 공통적으로 예외도 서버 로그 흔적도 남기지 않는다. 열린 엔드포인트가
정상 응답하는 것, 404 로 글꼴이 기본 서체가 되는 것이 유일한 증상이다.
## 회귀 잠금
되돌림 red 를 모집단 전원에 대해 실측했다 — 그룹 미들웨어 제거, 개별 게이트 재주입,
`channels:` 복원, catch-all 예외 제거, 컨트롤러 3원의 `fileResponse` 회귀, 번들
치환 제거가 각각 어느 테스트를 red 로 만드는지 확인하고 원상 복구했다.
게이트 부착 축은 행위 테스트로 대체할 수 없다 — 가려진 라우트는 행위상 "막힌 것" 과
구분되지 않으므로, `gatherMiddleware` 를 직접 보는 등록 계약 테스트를 둔다. 치환
배선도 같은 이유로 라우트 이름 규약에서 모집단을 도출하는 계약 검사를 둔다(손으로 적은
목록은 네 번째 경로가 생기는 순간 조용히 낡는다).
레이아웃 scripts[] 에만 있던 원격 스크립트 차단 게이트가 다른 주입 경로에는
없어, 저장측(SafeLayoutExpressions·NoExternalUrls)이 외부 URL 저장을 422 로
막아도 런타임 디스패치 한 번으로 임의 원격 코드가 로드됐다. 저장 검증이
통째로 무의미해지는 상태였고 오류도 경고도 남지 않았다.
판정식을 support/scriptSrcPolicy.ts 로 분리해 런타임 SSoT 를 하나로 두고,
loadScript 액션 · 확장 핸들러 재로드 · 편집기 프리뷰 · 편집기 자산 매니페스트 ·
G7Core.asset.loadScript · PG 플러그인 SDK 주입이 모두 그것을 경유하게 했다.
사본을 두지 않은 이유는 차집합이 그대로 우회로가 되기 때문이다.
- 게이트는 캐시 검사보다 앞에 둔다 (뒤에 두면 이미 로드된 미신뢰 스크립트가
캐시 히트로 통과한다)
- NoExternalUrls 순회를 9키로 확대 — 번들 레이아웃 601개 사전 스캔으로
정당한 외부 URL 0건을 확인한 뒤 넓혔다 (저장 회귀 없음)
- callExternal 은 생성자를 참조 동일성으로 거부하고(별칭 전역 포함),
프로토타입 경로 세그먼트를 읽기·매핑 키 양쪽에서 차단한다
- PG SDK 는 서비스 SDK 라 자체 호스팅이 불가능하므로 manifest 선언 +
주입 직전 호스트 확인이 게이트다. tosspayments SDK URL 은 확장자가 없어
정적 검사에 걸리지 않아 그 런타임 확인이 유일한 게이트다
- 함께 드러난 결함: 동시 loadScript 가 로드 전에 완료 처리되던 문제,
확장 재로드에서 script 기존재가 CSS 까지 건너뛰던 문제, loadCSS 의
in-flight 미공유
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 회피
공개 저장소 이슈 gnuboard/g7 (@bigmsg) 제보에서 출발한 작업이다.
브라우저가 화면을 그리려고 제3자 CDN 에 도달해야 하면, 그 도달 실패는 예외도 로그도
남기지 않고 화면 기능만 조용히 사라진다. 폐쇄망·방화벽·광고차단기에서 재현되는데
자체 서버 로그에는 흔적이 없어 운영자가 원인을 특정할 수 없다. 제보된 것은 편집기
하나였지만 같은 구조가 아이콘·글꼴·코드편집기·압축 라이브러리·설치 마법사·개발
대시보드에 똑같이 있었으므로, 번들 확장과 템플릿 전체를 자체 제공으로 옮겼다.
런타임에 외부로 나가는 것은 주소 검색 서비스 하나만 남았다.
자체 제공만으로는 부족하다 — 자기 서버에서 받는 파일도 실패할 수 있고, 종전에는 그
실패가 무음이었다. CSS 경로에 재시도 계층을 세우고(스크립트 경로와 동형), 서버가 HTML
에 직접 심는 externals 까지 실패를 붙잡아 안내 배너와 [다시 시도]로 표면화했다.
편집기·코드편집기는 확보 실패 시 평문 입력으로 내려앉되 저장 계약을 유지한다.
두 번째 축은 운영자가 CSS 를 덧붙일 자리가 없던 문제다(sir.kr 문의). 확장 디렉토리의
custom/ 을 운영자 소유로 정해, 확장 교체가 그 디렉토리만은 보존하게 했다. 출처에
의존하지 않는 서술자로 해석하므로 나중에 다른 출처가 붙어도 소비자는 바뀌지 않는다.
확장 자산과 같은 메커니즘으로 정적 게시되어 CSS 내부 상대 url 도 해석되고, 파일을
고치면 그 변경을 감지해 재게시까지 예약된다.
FTP 접근이 없는 운영자에게는 그 자리도 없는 것과 같으므로 레이아웃 편집기에서 직접
넣고 고칠 수 있게 했다. 모듈·플러그인·템플릿이 한 엔드포인트를 공유한다 — 타입별로
나누면 같은 검증이 세 벌로 갈리고 그중 약한 하나가 우회로가 된다. 여기서 올린
스크립트는 그 레이아웃 한 장이 아니라 사이트 전 화면에서 실행되므로 레이아웃 편집과
분리된 전용 권한으로 연다. 운영자 CSS 가 화면을 조작 불능으로 만들면 그것을 고칠
화면에도 같은 CSS 가 실려 스스로 갇히므로, 서버가 목록을 비우는 탈출구(?custom=off)를
함께 뒀다.
동봉 자산은 재생성 경로에 버전 대조 가드를 붙였다. 선언과 다른 버전을 버전 디렉토리에
써 넣는 조용한 거짓말은 배포된 뒤에는 드러나지 않는다.
위임 관리자(부관리자)가 권한·역할·표현식·비밀 콘텐츠 경계를 우회하던
결함군을 계층 대칭성 원칙으로 전건 차단한다. 약한 경로가 정상 응답을
내보내는 것이 유일한 증상이라, 게이트를 생산 지점 한 곳(SSoT)에 두고
같은 데이터를 내보내는 소비 경로 전부가 그 게이트를 경유하도록 맞췄다.
- 등급 상한(rank ceiling): 슈퍼관리자 보호·역할/사용자 역할 배정(추가·제거
대칭)·일괄 상태변경·순서변경을 상세 경로와 동일 강도로 재적용. 가드는
DB 쓰기에 선행하여 거부 시 상태 불변.
- 레이아웃 표현식: new Function/with 실행을 AST 화이트리스트 평가기로 교체.
비-문자열 computed 키 정규화(normalizeKey)·Object facade(리플렉션 static
제거)·legacy 접근자 차단. 저장측 검증·정적 검사와 3계층 동형.
- secret 게이트: 비밀글의 댓글·첨부·문의 독립 경로 재적용, hash 파일서빙
소유권·비밀·발행 상태 검사 통일.
- 신뢰 스크립트 호스트: 확장 선언 기반 + same-origin 브라우저 정규화를
런타임·저장측·정적검사 3층 동형화.
- 회귀 감지: 단위·Feature·E2E·시나리오 매니페스트 전축 + audit 룰 4종 신설.
메일 인증 대신 휴대폰 본인확인을 쓸 수 있게 하는 플러그인을 추가한다.
가입·비밀번호 재설정·성인인증 등 코어 IDV 가 강제하는 모든 지점에서 동작하며,
테스트 모드로 계약 없이 전 흐름을 확인할 수 있다.
구현·검수 과정에서 드러난 저장소 전역 결함을 함께 처리했다.
- 외래키 컬럼의 한국어 comment 가 방출되지 않던 문제 (소스 38건 교정 +
기설치본 백필 업그레이드 스텝 7종). ->comment 를 ->constrained 뒤에
체인하면 예외 없이 통과하면서 comment 가 조용히 사라진다
- 로케일 전환 후 확장 액션 핸들러가 소실되던 문제 (플러그인 3종에
재등록 진입점 노출)
- 본인인증 상태 폴링 응답이 누적 시도 횟수를 함께 내보내던 문제
- 인증 안내 문구와 실제 진행 방식의 폭 경계 불일치 (768~1023px).
엔진과 같은 값을 같은 방법으로 읽도록 교정
- transition_overlay_target 이 replace 없이 무효이던 관리자 목록 40곳.
로딩 표시가 나오지 않아도 경고가 남지 않아 화면을 봐야만 알 수 있었다
두 인증 플러그인의 코어 최소 요구 버전을 7.0.6 으로 올린다. 운영 모드
자격증명 필수 검사가 7.0.6 의 설정 저장 검증 표면에 의존하며, 그보다 낮은
코어에서는 그 검사가 조용히 통과해 빈 자격증명으로 저장할 수 있었다.
재발 차단으로 정적 검사 룰 4종을 신설하고, 검사 도구가 미커밋 신규 확장을
"검사 파일 0" 으로 통과시키던 사각을 함께 막았다.
세 갈래의 결함을 한 브랜치에서 정리한다.
## 목록 컨텍스트 왕복 시 URL 상태 소실 ( @jiwonpapa 님께서 제보해주셨습니다.)
목록에서 상세·형제 상세(이전/다음)·작성/수정 폼에 다녀오면 보고 있던
page/search/category/filters 가 사라지던 문제를 전 도메인에서 수정했다.
- 엔진(engine-v1.54.2): `mergeQuery: true` 만 적고 `query` 를 생략하면 병합이
통째로 건너뛰어지던 함정을 교정 — `ActionDispatcher.handleNavigate`/`handleReplaceUrl`.
- 게시판·이커머스·페이지·회원·마이페이지·gdpr 등 9개 확장 레이아웃의 왕복 leg 전수
적용(mergeQuery: true). 의도적 리셋(검색/필터 초기화·탭 전환·프리셋)은 면제 주석으로 구분.
- 무한스크롤 목록(브랜드·상품 공통정보·고시정보)의 새로고침이 URL 검색·정렬을 떨구던
결함 수정.
- 재발 차단: audit 룰 `layout-list-context-navigate-merge-query`(목록 클러스터 자동 도출,
page/필터 URL 신호 4종) + `layout-navigate-path-absolute`(navigate path 동작 키워드 금지).
## cellChildren 등 반복 렌더에서 단일 바인딩 파이프 미적용 ( @glitter-gim 님께서 제보해주셨습니다.)
목록 표의 각 칸에 넣은 날짜·숫자 서식(`{{row.x | datetime(...)}}`)이 빈 값이 되거나
서식 없는 원본으로 나오던 문제를, 표현식 판정 로직이 엔진 전역에 복제되며 갈라진
구조적 결함으로 진단하고 판정 경로를 단일화했다(engine-v1.54.3).
- `RenderHelpers`(renderItemChildren·evaluateIfCondition)·`ConditionEvaluator`·
`DataBindingEngine.resolveObject`·`DynamicRenderer` props 5곳에 단일 바인딩 파이프 분기 추가.
- 계획: `g7-scalable-lobster.md`(렌더 경로 비대칭 결함 일괄 수정).
## 공개 문서 내부 도구 귀속 제거
release 에 포함되는 공개 문서(`docs/**`)에서 내부 audit 룰 ID 귀속 서술을
도구 비귀속 표현("정적 검사")으로 정리. 재발 차단 룰 `public-no-internal-audit-reference` 신설.
전 계층 테스트(PHPUnit·Vitest·Playwright)·회귀 테스트 동반, 버전/CHANGELOG/활성 디렉토리 동기 완료.
오토로드 캐시(psr4)가 공집합으로 덮어써져 모든 모듈/플러그인 컨트롤러가
해석 불가능해지고 확장 화면 전체가 500 이 됐다. 이 매핑은 "DB 설치 목록 ×
디스크 활성 디렉토리" 교집합이라 DB 쪽만 일시적으로 비어도(테이블 부재·
설치 중간 실패·testing DB) 정상 매핑이 통째로 소실된다. 코어 라우트는 200
인데 확장 라우트만 전부 500 인 비대칭이 이 결함의 진단 단서다.
- 디스크에 확장 디렉토리가 남아 있는데 매핑이 0건이면 기록하지 않고 보존.
정상적인 0건(신규 설치·마지막 확장 삭제)은 디스크도 함께 비어 통과한다
- generateAutoloadFile 에 testing 실 캐시 경로 쓰기 차단. updateComposerAutoload
에만 있던 가드를 생성기 자체로 옮겨 어느 경로로 들어와도 막히게 했다
- extension:update-autoload 가 훅 캐시 재생성 전에 런타임 오토로드를 재등록.
미재등록 시 깨진 매핑으로 훅 캐시가 modules/plugins 0건으로 생성되어
확장 훅이 조용히 사라지고, 복구에 커맨드 2회 실행이 필요했다
소스맵 서빙은 에서 차단했으나 이번 브랜치에서 화이트리스트에 재추가되어
운영에서 원본 코드 전문(sourcesContent)이 노출될 수 있었다. 이 화이트리스트가
확장 에셋 서빙의 유일한 방어선이다. dev 빌드의 sourceMappingURL 404 를 피하면서
운영을 막도록 local 환경에서만 허용하도록 분기하고, 산출물을 production 으로
재빌드해 docroot 에 남아 있던 소스맵을 제거했다. --production 빌드는 소스맵을
생성하지 않을 뿐 기존 파일을 지우지 않는다.
CoreUpdateCommandSpawnFailureTest 는 in-process fallback 이 실제 업그레이드
스텝을 돌려 코어 역할·권한·메뉴를 커밋하는데 RefreshDatabase 가 없어, 이후
모든 테스트 클래스가 중복 키로 깨졌다(단독 실행 시 미재현).
실측(g7.dev) 결과 줄바꿈 제거·gzip 은 이미 적용된 상태였고,
남은 실질 개선 3종을 구현:
- 모든 API 응답에 JSON_UNESCAPED_UNICODE 적용 — 한글이 \uXXXX(6B)로
팽창하던 것을 raw UTF-8(3B)로 직렬화 (lang pack gzip -9.1%)
- 공개 레이아웃 서빙에서 개발자용 comment/_comment 재귀 제거,
편집 모드(with_source_meta)는 보존 (layout gzip -34.9%)
- 번들 JS/CSS(BinaryFileResponse) gzip 압축 사각지대 해소 —
기존 미들웨어가 getContent==false 로 압축을 건너뛰던 문제 (-73~77%)
활성 모듈/플러그인마다 프론트엔드 IIFE JS·CSS 가 개별 HTTP 요청으로 로드되어
확장 수에 비례해 요청이 늘어나는 오버헤드를, 종류별 1개 번들로 서버 병합해 줄인다.
각 확장 IIFE 자가등록 계약(핸들러/리스너/preblocker)은 priority 순 물리 병합으로
실행 순서가 보존되어 그대로 동작한다.
## 백엔드
- 신규 ExtensionBundleService — 정렬(uasort priority)·필터(global)·concat(\n;\n
구분자로 ASI 경계 보호)·소스맵(prod strip / dev rewrite)·version-in-path 디스크
캐시·확장별 fine-grained try/catch(한 확장 실패가 번들 전체를 붕괴시키지 않음)
- AbstractModule/AbstractPlugin::getBuiltAssetAbsolutePaths — _bundled 경로
정확 해석용 절대경로 게터(getAssetFilePath 하드코딩 복제 회피)
- PublicModule/PluginController::serveBundle{Js,Css} + routes/api.php 4개
(/api/{modules,plugins}/bundle.{js,css}?v={version}, fileResponse ETag/304 재사용)
- config/filesystems.php ext-bundles 디스크 + storage/.gitignore + 루트 .gitignore
- 신규 ext-bundles:cleanup 커맨드 + {module,plugin,template}:cache-clear 번들
정리 통합 + dev-dashboard 버튼
## 프론트엔드 (engine-v1.52.0)
- TemplateApp.loadExtensionAssets — 개별 로딩 → bundleUrls 기반 모듈→플러그인
순 번들 로딩. bundleUrls 부재 시 개별 로딩 폴백(회귀 안전)
- ModuleAssetLoader.loadBundle + parseBundleUrlsFromConfig + ExtensionBundleUrls
- TemplateComposer/UserTemplateComposer bundleUrls 뷰변수 + app/admin.blade.php
- core:build 재빌드 산출물(public/build/core/*)
## 버전
- 코어 7.0.1 → 7.0.2 bump. 7.0.1 이 이미 2026-07-03 공개 릴리스이므로 그 위 신규
기능은 +1 bump(new-bump). config/app.php(SSoT) + .env{.example,.testing.example,
.testing} + README 뱃지 + INSTALL 예시 + 루트 CHANGELOG 신규 [7.0.2] 섹션 동기화.
- 확장 g7_version 상향 안 함(>=7.0.0 유지) — 6개 프론트 에셋 확장이 신규 코어
표면을 자기 코드에서 호출하지 않고 개별 서빙 라우트 존치+프론트 폴백으로 코어
7.0.0 에서도 동작 → 실제 의존 없음( 결정).
- 부수 결함 수정: config/app.php 편집으로 활성화된 core-update-targets-coverage
룰이 v1.51.0 에서 추가된 vite.config.{editor,devtools}.js 가 app.update.targets
에 미등재됨을 검출 → 추가. 코어 업데이트 시 편집기/devtools 빌드 설정 갱신 누락
방지(CHANGELOG [7.0.2] Fixed).
## 규정/audit/문서/테스트
- ("확장 번들 병합" 절+파일유형표+E2E경로) / AGENTS.md /
docs/extension/module-assets.md / docs/cheatsheet.md
- coverage.json 4항목 + codified 룰 4종(url-same-origin, concat-separator,
asset-path-getter, clears-template-caches-no-file-delete)
- 루트 CHANGELOG [7.0.2] Added/Fixed / 엔진 CHANGELOG engine-v1.52.0 Added
- Vitest 11(loadBundle + concat 자가등록) + PHPUnit 17(Service 12 + Feature 5)
+ Playwright spec + 시나리오 매니페스트
- 브라우저 실측(g7.dev): 홈/관리자 번들 각 1건·개별 iife 0건, gdpr 인터셉터
정상, 활성화 토글 반영, 장바구니→체크아웃·게시판 글쓰기 정상. g7_2.dev 미적용
대비 확장 요청 11~12→4건, DOMContentLoaded ~843~957→605~741ms(페이로드 무손실)
## 도구 보완
- pre-commit-review 스킬 에 엔진 경로 감지 게이트(5-pre + 보고 5e행) 신설 —
매핑 표에 엔진 파일이 있으면 엔진 CHANGELOG 판정을 강제(이번 초기 누락 재발 방지)