관리자 상품목록을 한 페이지 여는 것만으로 그 페이지 모든 상품의 옵션이 응답에 실렸다.
같은 패턴을 저장소 전역에서 찾아 14개 목록 엔드포인트를 함께 정리했다.
근본 원인은 둘이다. Resource 가 whenLoaded 로 방어하는데 Repository 가 목록 쿼리에서
관계를 무조건 로드해 가드가 항상 참이 되는 가짜 가드, 그리고 toListArray 경량 표현을
정의해 두고도 컬렉션이 toArray 를 부르는 목록/상세 미분리다. 둘 다 응답만 보면
정상이라 오류도 경고도 없이 페이로드만 불어난다.
목록은 화면이 실제로 그리는 것만 싣는다. 개수·합계는 PHP 컬렉션 연산이 아니라 DB
집계로, 대표 1건이 필요한 곳은 관계 자체를 oldestOfMany 로 좁힌다. eager load 의
limit(1) 은 부모별이 아니라 배치 쿼리 전체에 걸려 첫 행만 값을 갖게 되므로 쓸 수 없다.
뺀 값에는 대체 경로를 먼저 만들었다. 상품 옵션은 행을 펼칠 때 배치로 불러오고(상품 수와
무관하게 쿼리 상수), 종전 동작이 필요한 호출자를 위해 ?with_options=1 등 opt-in 을 남겼다.
배송정책 국가설정과 리뷰 첨부 이미지는 소비처를 실측한 결과 화면이 실제로 그리고 있어
제거하지 않았다 — 그 소비 사실을 회귀 테스트로 고정했다.
재발 방지로 정적 검사 룰 4종과 규정 문서 항목을 함께 넣었다.
KISA 신고포상제 접수 3건(KVE-2026-1677/1678/1679)의 공통 원인은 하나다 —
"위험한 것을 골라 막았고, 목록 밖으로 우회됐다". 세 지점 모두 판정을 뒤집는다.
예약 작업 Artisan (KVE-2026-1679)
차단 9개 / 허용 229개였던 구조를 허용목록으로 바꾸고 명령마다 허용 옵션을 선언한다.
검증 과정에서 보고서보다 짧은 경로를 추가로 발견했다 — 검증기는 preg_split 의 첫
토큰을 명령명으로 봤지만 실행부는 문자열째 넘겼고, parameters 가 비면 Laravel 이
StringInput 으로 재파싱한다. 따옴표 한 쌍이면 차단목록의 tinker 가 그대로 실행됐다
(마커 파일로 확인). 파서를 엄격 형태 하나로 고정하고 실행은 (명령명, 인자배열)로 넘겨
재해석 지점 자체를 없앴다.
언어팩 (KVE-2026-1678)
위험 함수 11개를 정규식으로 나열하던 검사를 토큰 상태기계로 교체해 "번역 배열만"
통과시킨다. 목록에 없는 함수도, 함수 이름을 아예 쓰지 않는 형태도 원리적으로 막힌다.
파일 형식·PHP 위치·심볼릭 링크도 화이트리스트로 뒤집었다. 활성 승격이 require 배선의
필수 조건이므로 설치와 활성화의 권한 경계를 네 설치 경로 전부에 같은 강도로 건다 —
경로별 면제는 곧 우회로가 되기 때문이다. 화면은 운영자의 활성화 권한을 보고
auto_activate 를 실을지 결정하므로 재설치 흐름은 그대로 동작한다.
인스톨러 (KVE-2026-1677)
실행 바이너리 자리에 인자가 들어가는 것을 형태 규칙으로 차단한다. escapeshellarg 는
"하나의 인자" 임만 보장할 뿐 그 인자가 실행 파일인지 스크립트인지는 보장하지 않는다.
판정을 의존성 0 공용 정책으로 모아 진단 API·설치 워커·저장 시점이 같은 규칙을 쓴다.
stat 은 호출하지 않는다 — 접근이 제한된 서버에서 정상 경로가 거부되던 회귀를
재발시키지 않기 위함이다.
검증: 백엔드 392건 + 프론트 7건 green(skip 1, symlink 미지원 환경). PoC 마커로 red 를
먼저 확보하고, 수정 후 stash 역회귀로 다시 red 가 되는 것까지 확인했다.
판정 통일(engine-v1.55.0)에서 리터럴 해석을 BindingShape 한 곳으로 모았으나
renderItemChildren 만 `hasPipes → isComplexExpression → 경로 탐색` 3분기를 직접
갈라 리터럴을 몰랐다. 그 결과 같은 `{{true}}` 가 조건 자리에서는 true,
목록 셀·카드 등 반복 렌더 prop 자리에서는 undefined 로 갈렸다 —
통일이 없애려던 비대칭이 이 지점에만 남아 있었다.
형태 판정을 resolveSingleBindingValue 에 위임해 다른 경로와 같은 규칙을 쓴다.
같은 지점이 빈 바인딩(`{{}}`)도 몰라 경로 탐색으로 보내던 것을 함께 봉인했다.
브라우저 실측(배포 번들의 G7Core.renderItemChildren 직접 호출)으로 발견했고,
저장소 레이아웃의 리터럴 단일 바인딩 60건은 전부 if/condition 자리라
화면 표시에는 변화가 없다.
SEO 패리티: 봇 측 ExpressionEvaluator 는 이미 리터럴을 해석하고 있었으므로
이 수정은 React 경로를 봇 경로에 맞춘 것이다 (파일 헤더에 면제 사유 기재).
표현식 해석 로직이 엔진 전역에 복제되면서 복사본마다 기능 집합과 판정 기준이
갈라졌고, 같은 작성이 어느 렌더 경로를 타느냐에 따라 다른 결과를 냈다. 증상
대부분이 예외가 아니라 조용한 오답 또는 값 소실이라 재현 위치를 특정하기 어려웠다.
판정만 BindingShape 단일 출처로 모으고 실행은 지점별로 유지했다. 전면 통합은
회귀 위험이 크고, 갈라진 것은 실행이 아니라 판정이었기 때문이다.
- 파이프 평가를 문자열 보간에 위임하면서 생긴 비대칭 3건 해소 (중괄호 노출 /
_computed stale 전파 / 원본 타입 소실)
- 파이프 분기 누락 8지점 보강 — 평가 지점 모집단을 스캔해 가드를 강제하는 회귀선 신설
- 단일 바인딩·복잡식 판정 14곳을 정본으로 치환, 빈 바인딩 봉인
- BINDING_PATTERN 정규식을 수동 스캐너로 교체, resolveObject 를 key 단위 예외 격리로 전환
- iteration + 항목 참조 if 가 목록 전체를 지우던 결함 해소, 반복 경로 기능 격차 정렬
- 템플릿 컴포넌트의 new Function 자체 평가기 제거 → G7Core.evaluateCondition 위임
- 컨텍스트에 식별자가 아닌 키가 하나라도 있으면 그 컨텍스트의 모든 식이 평가되지
못하던 결함 수정
- SEO 봇 렌더러도 같은 규칙으로 정렬 — React 만 고치면 봇 화면에서만 목록이 빈다
- audit walker 가 레이아웃의 74%(partial 루트 444/598)로 내려가지 못하던 사각 해소,
신규 룰 5종 등재
관리자 환경설정에서 미입금 자동취소 기한을 저장하면 HTML number 입력의
문자열이 그대로 영속되고, Carbon 3.13 의 strict 타입 경계에서 TypeError 가
발생해 무통장입금·가상계좌 주문이 전면 실패했다(공개 제보). 한 곳만 막으면
다른 저장 경로가 남으므로 저장·조회·소비·모델 네 계층에 각각 방어를 두고,
저장소 전역에서 같은 형태가 더 없음을 audit 룰로 확정했다.
같은 계획서 수행 중 드러난 인접 결함도 함께 해소했다.
- 쿠폰 수정 경로로 (발급일 기준, 일수 NULL) 조합이 저장되어 발급 즉시
만료된 쿠폰이 조용히 나가던 문제 — 저장·발급 양쪽에서 차단
- 리뷰 작성 기한을 화면과 저장 판정이 서로 다른 기준으로 계산하던 문제
- 결제 플러그인이 PaymentMethodEnum 과 다른 어휘로 지원 결제수단을 선언해
가상계좌·계좌이체·휴대폰결제에 PG사를 지정할 수 없던 문제
엔진에서는 `_localInit` 적용 여부를 전역 해시로만 판정해 인스턴스별 저장소가
리셋되지 않던 결함(engine-v1.54.5)과, 제거 대상이 없어도 상태 갱신·캐시
무효화가 상시 발생하던 판정 결함(engine-v1.54.6)을 고쳤다.
Playwright MCP 정밀 점검(T1~T11)에서 설정이 빈 문자열로 남은 사이트의
주문서 입금기한 안내가 미치환 플레이스홀더를 노출하는 것을 실측해, 화면
폴백을 서버와 같은 규칙으로 맞췄다.
목록 조회 성능 축(깊은 OFFSET·정렬·색인)과 그 검증 계층에서 계획서 전수검수와 3회에 걸친
감사가 지적한 항목을 처리했다. 세부 경위는 의 각 회차 문서에 있다.
검증 계층 — 컨트롤러가 base Request 를 직접 주입받던 확장 13곳을 전용 FormRequest 로 옮겼다.
옮기면서 기존 동작 계약은 그대로 두었다: 상한 초과 limit 을 거부하지 않고 상한까지 반환하던
공개 API, per_page 를 범위로 조정하던 관리자 목록, 미지원 period 를 year 로 해석하던 인기글
목록 모두 종전과 같은 응답을 낸다. 상한/폴백을 rules 로 승격시키면 200 이던 응답이 422 가
되어 기존 링크가 깨지므로, 규칙은 타입만 닫고 클램프·폴백은 접근자가 맡는다. period 는
접근자가 닫힌 집합만 반환해 캐시 키 공간도 함께 닫힌다.
ckeditor5 이미지 업로드만 ResponseHelper 봉투를 쓰지 않는다. 응답을 파싱하는 주체가 CDN 으로
로드되는 상위 CKEditor5 43.3.1 의 SimpleUploadAdapter 라 규약을 바꿀 수 없어, 각 응답 지점에
사유를 명시한 면제를 부착하고 근거를 API 문서에 남겼다.
하네스 — 룰 5개의 대상 경로 패턴이 매처와 맞지 않아 번들 확장 컨트롤러가 검사 대상에서
통째로 빠져 있었다. 패턴을 고치자 확장 위반 17건이 드러나 전건 처리했다. severity 오타가
요약 집계 양쪽에 안 잡혀 "0 error" 로 보고되던 문제도 런너 사전 검증으로 막았다.
정렬 게이트 대조 하네스는 관계 정렬 변형만 쓰는 저장소를 탐지하지 못한 채 통과시키고 있었다.
탐지·제외·인자 파싱을 함께 고치고 단위 테스트를 신설했다.
Laravel chunk/each 는 OFFSET 기반이라, 콜백이 행의 필터 소속을 바꾸면
(필터 컬럼을 채우거나 매칭 행을 삭제하면) 다음 조회가 그만큼 앞으로 밀려
미처리 행을 조용히 건너뛴다. 250건/청크 100 이면 정확히 100건이 남는다.
코어·번들 확장 전수 조사로 확정한 6곳을 chunkById(키셋 순회)로 통일했다.
브라우저 검수(28축)에서 계획에 없던 결함 12건이 드러나 같은 사슬에서
수정했다. 이 중 layout_strategy=keep 무효화와 수정 감지 실패 시 "수정 없음"
오보는 사용자 작업 소실에 직결된다.
재발 방지로 audit 룰을 추가했고, 수정 6곳을 원복해 red 6건 / 복원 후 green
0건으로 판별력을 검증했다.
관리자 환경설정이 값을 저장은 하지만 실제 동작에는 닿지 않던 결함군을 정리했다.
설정 화면(min/max)·저장 검증(FormRequest)·런타임 적용(Service) 세 계층이 각자
리터럴을 들고 있어 서로 조용히 갈라지던 것이 공통 원인이다.
경계값 SSoT 도입:
- config/core.php `settings_limits` 를 단일 출처로 두고, FormRequest 는 이 값으로
규칙을 만들고 화면은 설정 응답의 `_meta.limits` 로 같은 값을 받아 바인딩한다.
- 게시판·이커머스도 각 모듈 config 의 limits 를 동일 구조로 노출한다.
적용되지 않던 정책을 실제로 적용:
- 비밀번호 최소 길이·특수문자 필수(신규 지정 7개 경로. 로그인은 제외 — 정책을
올렸을 때 기존 회원이 자기 계정에 접근조차 못 하는 데드락을 막기 위함)
- 계정 잠금 시간 0 = 무한대(전용 컬럼 분리. timestamp 상한과 NULL 규약 때문에
기존 컬럼 재사용이 불가능하다) + 관리자 수동 해제 경로
- 업로드 최대 크기·허용 확장자, 게시판 첨부 개수·댓글 깊이, 페이지 첨부 정책,
장바구니 수량·구매 한도, 마일리지 사용 한도, 주문 부가세
파생 결함:
- 큐 Job(sync 드라이버)이 진행 중인 요청의 Request 바인딩을 덮어써, 뒤이어 실행되는
훅 리스너가 헤더를 잃고 조용히 건너뛰었다. Job 이 원 바인딩을 복원하도록 고치고,
전송 헤더가 필요한 리스너는 동기 실행으로 선언한다.
- 확장 업데이트 실패 시 상태만 되돌리고 상태 캐시를 비우지 않아, 그 확장의 관리자
화면만 캐시 TTL(기본 하루) 동안 404 로 남았다. 실패 경로에도 무효화를 대칭으로
두고, 빈 목록 자체를 캐시하지 않는다.
- 글 작성 시 첨부 파일이 Service 로 전달되지 않아 검증·권한은 통과하고 첨부만 사라졌다.
- 환경설정 저장 실패 안내가 내부 설정 키를 그대로 노출했다(105개 항목에 라벨 부여).
재발 방지:
- audit 룰 3종(레이아웃 경계 리터럴 / FormRequest 경계 불일치 / Service 재클램프)
- 테스트 실행 범위 강제 훅(check-test-scope)
- 트러블슈팅 사례 2건(큐 Job 요청 오염 / 확장 상태 캐시 고착)
공개 이슈: gnuboard/g7
세 갈래의 결함을 한 브랜치에서 정리한다.
## 목록 컨텍스트 왕복 시 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/활성 디렉토리 동기 완료.
설치 2단계 서버 요구사항 화면의 "에셋 파일 서빙 방식" 항목이 "확인 불가"
한 단어만 표시돼 어떤 상황인지 알 수 없었다. "확인 불가" 는 확장자 있는/없는
프로브 요청이 모두 실패한 상태로, 에셋 방식 문제가 아니라 앱 미응답·프록시·CSP
등 다른 원인일 수 있다.
- asset_url_mode_detected_unavailable(ko/en): 두 프로브 모두 실패라는 상황,
에셋 방식 문제가 아닐 가능성, 설치를 막지 않는다는 점, 설치 후 백지면
확장자 미사용으로 전환하라는 조치까지 담아 보강. 이 키는 Step 3 감지 버튼
상태(data-msg-unavailable)와 공유되어 양쪽 화면이 함께 개선된다.
- refreshAssetUrlModeCard: 경고 상태(확장자 미사용/확인 불가)에 설명 문구를
카드 아래 fix-guide-hint 로 렌더. 짧은 라벨만으로는 상황을 알 수 없던 문제 해소.
라우트 패턴은 끝 슬래시 없는 형태(*/admin 등)로만 정의되는데(routes.json
규약), Router.match 가 들어온 경로의 끝 슬래시를 정규화하지 않아 /admin/
(끝 슬래시)이 어떤 라우트에도 매칭되지 않고 404 로 떨어졌다. 특히 미인증
상태로 /admin/ 진입 시 대시보드 → 로그인 리다이렉트 흐름 대신 404 가 노출됐다.
match 진입에 normalizePathname 을 두어 루트(/)를 제외한 경로의 끝
슬래시(연속 포함)를 제거한 뒤 대조한다. match 는 라우팅 단일 진입점이라
모든 호출부가 함께 정상화되고, 다운스트림(auth back-URL, getAuthType)은
원본 pathname 을 그대로 써 의미 변화가 없다.
기존 릴리스부터 있던 라우터 견고성 결함이며 자산 URL 이중 모드 변경과는
무관하다(matchPattern 은 미변경, routes.json 은 81건 정상 로드 확인).
관리자 환경설정 > 일반의 에셋 파일 서빙 방식 필드를 브라우저 검수 지적대로 다듬었다.
- "자동 감지" 버튼이 btn 베이스 클래스 없이 btn-secondary 만 갖고 있어 다른 버튼과
달리 평문처럼 보였다. btn 을 더해 동일한 박스 스타일로 맞췄다.
- 감지 결과를 토스트로 띄우면 사라져 확인이 어려웠다. 핸들러가 결과를 로컬 폼
상태로 기록하고, 필드 하단에 상태별 인라인 안내로 상시 노출하도록 바꿨다.
- 라벨을 "에셋 파일 서빙 방식" 으로 바꾸고(전 화면·전 로케일), 확장자 가로채기
안내에 원인이 되는 nginx location ~* \.(js|css|...)$ 정적 최적화 블록을 담아
인프라 담당자가 바로 인지할 수 있게 했다. ko/en/ja 및 인스톨러까지 일관 반영.
레이아웃 test util 은 _local 조건부 렌더를 시드하지 못하므로(엔진은 로컬상태
provider 로 if 를 평가) 인라인 안내의 렌더는 레이아웃 구조 테스트 + 핸들러 단위 +
E2E 세 계층으로 커버했다.
develop 리베이스 중 빌드 산출물을 매 커밋 develop 베이스라인으로 넘겼으므로,
병합된 소스 기준으로 core:build + template:build 를 1회 재실행해 정합화했다.
iife 에 자산 URL 감지 핸들러가, main.css safelist 에 drift_alert amber 클래스가
develop 상위집합과 함께 담겼다.
코어 스위트에서 드러난 실패 4종을 처리했다(직전 세션이 admin 스위트만 돌려 누락):
- assetUrlRecovery(L6): 프로브 판정이 probeAssetUrlMode 로 추출됐는데 테스트가
옛 detectAssetUrlMode 앵커를 봤다. 앵커 재조준(코드 정상, 테스트 stale).
- TranslationEngine: suffixed 헬퍼 일원화로 쿼리 파라미터 순서가 바뀌었다(서버엔
무관). 순서 무관 단언으로 정정.
- sitemap sampleData 미커버: editor-spec 의 byDataSourceId 에 sitemap_status/
sitemap_progress_ws 샘플 추가.
- SectionHeader/ThemeToggle 캡: audit 룰 SSoT 엔 이미 비대상 등록됐으나 테스트
병행 allowlist 만 어긋나 있었다. allowlist 동기화.
앞 두 건은 훼손→red 로 가드 유효성을 확인했다. 순서상 테스트가 읽는 소스만
바뀌었고 런타임 동작 변화는 없다.
오토로드 캐시(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 가 없어, 이후
모든 테스트 클래스가 중복 키로 깨졌다(단독 실행 시 미재현).
계획서 §검증 후반부를 다시 대조해 미구현 3건을 확인하고 모두 구현했다. 앞선 커밋에서
"미구현 1건" 으로 보고한 것은 §12 불변식 표만 파일로 대조하고 §검증 섹션의 표 두 개는
확인하지 않은 결과였다.
- 설치 2단계 요구사항 화면에 자산 URL 방식 항목 추가. 판정은 서버가 아니라 브라우저가
한다 — 서버에서 자기 APP_URL 로 요청하면 loopback 이 vhost·프록시 체인을 우회해
실제 방문자와 다른 답을 낸다. 2단계와 3단계가 같은 판정 함수를 쓰도록 추출했다
- 대시보드가 스스로 프로브를 던져 **저장된** 방식과 대조하고 어긋나면 안내한다.
런타임 값이 아니라 저장값을 보는 이유는, 자가 복구가 이미 전환해 놓았을 때가 바로
알려야 할 상황이기 때문이다 — 봇은 자바스크립트를 실행하지 않아 자가 복구가 닿지 않는다.
알리기만 하고 저장하지 않으므로 클라이언트가 서버 설정을 뒤집지 않는다
- 레이아웃 렌더링 테스트를 두 모드로 추가. 핵심 단언은 두 모드의 DOM 이 완전히 동일하다는
것이다 — 모드가 화면 구조로 새어나가면 확장자 없는 환경에서만 깨지고 그 환경은 개발 중
거의 재현되지 않는다
함께 고친 테스트 격리 결함 4건 (이번 기능과 무관하나 같은 세션에서 발견):
- 인스톨러 공유 클래스 로드가 오토로드를 막고 있어, 앱 루트를 임시 디렉토리로 바꾸는
테스트에서 없는 파일을 읽으려다 죽었다 (8건 fatal)
- 테스트용 번역 스텁이 실제 구현과의 정의 경쟁에서 이기면 주입된 번역을 무시했다.
클래스 단독 실행은 통과하고 스위트 실행만 실패하던 원인
- 모듈 활성 목록은 "디스크 로드분 ∩ DB 활성분" 인데 부팅 시 테이블이 비어 디스크 스캔이
일어나지 않아, 파일 캐시 잔재가 있을 때만 통과하던 순서 의존 테스트
- 라우트 프로바이더 가드 테스트가 Git 미추적 파일(.env) 존재에 의존해 신규 클론·CI 에서
실패했다. 임시 앱 루트로 격리
또한 일부 테스트가 실제 개발 환경 파일을 삭제·재생성하고 자신의 정리 단계에서 되돌리는데,
그 테스트가 죽으면 되돌아오지 않아 복구 불가능한 유실이 발생한다. 실제로 이번 세션에서
겪었으므로 테스트 부트스트랩에 프로세스 수준 복원 안전망을 추가했다.
검증: 백엔드 344건 / 템플릿 2799건 / 레이아웃 7건 / 브라우저 9건 green, audit 위반 0.
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 님께서 제보해주셨습니다.
브라우저 실측에서 경고가 경고로 읽히지 않는 문제가 드러났다. 기능은 정상
동작했으나 문구가 밋밋하고, 인스톨러 카드가 통과 항목과 같은 색이며, 관리자
정보 탭에서는 경고가 카드 사이에 끼어 있었다.
문구는 영향의 크기를 구체화했다 — 페이지를 열 때마다 모든 PHP 코드를 다시
해석하며 응답이 몇 배까지 느려진다는 점을 인스톨러와 관리자 양쪽에 같은 기준으로
반영했다.
인스톨러는 상태를 아이콘에만 반영하고 있어 목록을 훑을 때 문제 항목이 눈에
들어오지 않았다. 카드 자체에 상태 수식자를 부여했다. 경고만 고치면 필수 항목
실패 카드도 통과 카드와 똑같이 보이는 같은 결함이 남으므로 fail 도 함께 다뤘다.
관리자 정보 탭에서는 경고가 2단 카드 그리드의 세 번째 셀로 들어가 있었다.
탭 최상단으로 옮기고, 여백은 호출처가 아니라 자산이 책임지는 기존 컨벤션을
따라 alert 인접 규칙에 카드 그리드를 추가했다. 배치는 눈으로만 보면 회귀를
알 수 없으므로 좌표와 직계 자식 여부를 E2E 로 고정했다.
설치 단계와 코어 부팅의 정책이 엇갈려 있었다. 인스톨러는 root 를 자유롭게
허용하면서, 설치 후 부팅은 같은 값을 보고 확장 로딩 전체를 조용히 스킵했다.
설치는 성공하지만 이후 깨진 화면만 뜨고 원인은 로그 한 줄에만 남았다.
판정을 프레임워크 의존성 0 인 공용 SSoT 로 모아 인스톨러와 코어가 같은 답을
내게 하고, 설치 단계에서 서버 2경로로 차단하며, 설치 후 변경은 전용 에러
페이지로 드러낸다. 가드를 boot 가 아닌 미들웨어에 두어 artisan 이 죽지 않게
해 복구 수단을 남겼다. admin 은 일반 권한 운영 계정 오차단을 막기 위해 차단
목록에서 제외했다.
OPcache 는 상태 조회 경로 자체가 없어 성능 저하를 인지할 방법이 없었다.
설치 단계 경고(비차단)와 관리자 정보 탭 표시를 같은 판정 SSoT 로 추가했다.
확장 로드 여부만으로는 부족해 opcache.enable 지시자까지 확인하며, ini_get 이
차단된 환경은 "확인 불가"로 폴백해 경고도 차단도 하지 않는다.
## 소스맵 노출 차단 (공개 이슈 )
배포본에 .map 이 포함돼 원본 TS/TSX 전문(sourcesContent, 한글 주석 포함)이
그대로 노출됐다. 생성·서빙·저장소 유입 3계층을 모두 막는다.
- 빌드 config 15곳(코어 3 + _bundled 12): sourcemap 하드코딩 제거.
G7_BUILD_SOURCEMAP 판정식으로 교체 — 미설정(로컬 npm run build)이면 생성해
개발 디버깅 경험을 보존하고, 0 이면 미생성.
- Build{Core,Module,Plugin,Template}Command: 죽어 있던 --production 플래그 복구.
$productionMode 를 콘솔 문자열에만 쓰고 npm 프로세스에 env 를 넘기지 않아
산출물이 개발 빌드와 동일했다. runNpmCommand 에 $env 를 추가해 주입
(--watch 에는 미주입). Symfony Process 는 부모 환경에 병합하므로 PATH 유실 없음.
- Allowed{Module,Plugin}FileType / {Module,Plugin}Service::getMimeType:
허용 확장자 map 제거 — 익명 200 서빙 차단(템플릿은 원래 미허용, 3종 정책 일치).
- ExtensionBundleService: CSS 번들의 /*# sourceMappingURL=... */ strip 추가.
CSS 는 블록 주석이라 JS 의 //# 패턴으로는 검출되지 않던 구멍.
- 추적 .map 13개 삭제 + .gitignore 전역 *.map 규칙(경로 한정 시 새 확장에서 누락).
실측: 산출 JS 의 sourceMappingURL 13 → 0, 추적 .map 13 → 0,
확장 디렉토리 npm run build 는 여전히 map 생성(개발 경험 회귀 없음).
## 관리자 환경설정 탭 지연 로딩
코어/이커머스/게시판 환경설정 화면이 진입 즉시 전 탭의 데이터소스를 한꺼번에
호출했다. 각 데이터소스에 활성 탭 조건(if)을 걸어 해당 탭에서만 fetch 하도록
변경하고, 결제 플러그인 3종의 테스트모드 상태 조회도 주문설정 탭으로 한정.
## 동반 결함 수정 (무관 결함 — 재발 방지 목적으로 같은 세션 처리)
- 나이스페이먼츠: 체크아웃 캐시 프리페치가 라우트를 가리지 않아 플러그인 번들이
로드되는 모든 페이지(홈/게시판/로그인)에서 주문서를 조회 → 비회원 매번 422.
형제 플러그인(kginicis)의 기존 가드 패턴을 적용해 체크아웃 경로로 한정.
- admin smoke 8건: 대부분 구현이 아니라 테스트 결함이었다 — URL 오타(단수 board),
존재하지 않는 권한키, 앱 로케일이 en 인데 한국어 라벨 로케이터, DataGrid 버튼을
먼저 잡던 휴리스틱 로케이터. 레이아웃에는 sticky className / 안정 id 를 부여.
- PHPDoc ↔ Pint 충돌 3건: 설명 없는 @param 은 no_superfluous_phpdoc_tags 가
삭제해 audit phpdoc-public-method 와 무한 충돌. 한국어 설명을 붙여 양쪽 충족
(리스너 3종의 설명 누락 메서드도 전수 보강).
- Log::spy 500 3건: spy 가 Log::channel('activity') 를 null 로 만들고
logActivity 가 null->info 로 \Error → 같은 줄 catch(\Exception) 은 못 잡아
통과. 운영에선 채널이 항상 로거이므로 제품 무결 → 테스트에 activity 채널
보존 헬퍼 추가.
- NhnKcpApiServiceTest 5건: 스텁이 Unix 바이너리만 만들어 Windows 분기
(pp_cli_exe.exe)에서만 실패. #!/bin/sh 는 Windows 실행 불가 → 파일 내 기존
컨벤션대로 skip 가드.
## 테스트
- 소스맵 범위 130 passed (신규: BuildCommandSourcemapEnv, NoSourcemapArtifacts,
ExtensionBundleService CSS strip, Allowed*FileType 반전, 자산 서빙 거부)
- 결제 3플러그인 505 passed / 0 failed (skipped 9 = Windows 전용 CLI, 사유 명시)
- 나이스페이먼츠 프론트 38 passed (가드 제거 시 red 실증 후 복원)
- E2E: production-sourcemap-exposure.spec.ts 6건 + 시나리오 매니페스트
버전: sirsoft-admin_basic 1.0.2 → 1.0.3, sirsoft-board 1.0.1 → 1.0.2
(각 manifest/package/lock/composer 동기화). 코어 7.0.4 는 미출시 누적.
직전 커밋이 install-process.php 에 installer-runtime.php require 를
functions.php 앞에 추가하면서 발생한 회귀.
installer-runtime.php 는 escapeEnvValue 를 function_exists 가드와 함께
polyfill 로 선언하는데, functions.php 는 같은 함수를 가드 없이 선언하고 있었다.
로드 순서가 뒤바뀌자 "Cannot redeclare escapeEnvValue" fatal 이 발생하고,
JSON 이 출력되기 전에 스크립트가 죽어 응답 본문이 비었다. 브라우저의
res.json 이 이를 "Unexpected end of JSON input" 으로 보고해 설치 시작
즉시 실패했다 (SSE / 폴링 양 모드 — 둘 다 install-process.php 를 경유).
수정:
- functions.php 의 escapeEnvValue 에 function_exists 가드 추가 (근본 방어 —
require 순서만 되돌리면 다음 사람이 순서를 바꿀 때 재발한다)
- install-process.php 의 require 순서를 다른 엔드포인트와 동일하게 정렬
인스톨러 전역 전수 스캔 결과 중복 선언 함수는 escapeEnvValue 하나뿐이며
이제 양쪽 모두 가드를 갖는다.
회귀 가드: InstallerIncludeOrderTest — 격리 프로세스로 로드 순서를 바꿔가며
fatal 부재 검증 + 중복 선언 함수의 가드 보유를 정적 검사 (수정 전 3/4 fail 확인).
비밀을 state.json(0664, 실패 시 무기한 잔존)에 기록하지 않고
runtime.php(0600, finalize 시 삭제)로 채널을 일원화한다.
정상 완료 경로에서는 finalize 가 state.json 을 삭제하지만, finalize
미호출 / unlink 실패 / 설치 중단 시에는 평문이 영구히 남았고, finalize
가드가 재호출을 410 차단해 자가 회복도 불가능했다. 삭제라는 단일 경로에
의존해 평문을 보호하던 구조 자체를 제거한다.
SSE 워커는 세션에 접근할 수 없어 파일 경유가 불가피한데, 그 파일이 0664 의
state.json 이었던 것이 구조적 원인이다. 세션을 보유한 유일한 공통 시작점인
install-process.php 를 비밀 이송 지점으로 삼고, db_seed 가 소비한 즉시
제거한다. 레거시 / 실패 경로는 완료·중단·초기화·finalize 시점 redact 로 방어.
동반 수정:
- 세션 config 재병합으로 무력화되던 기존 DB 비밀번호 마스킹 복구
- db_seed 후 putenv 미해제로 모든 exec 자식에 평문이 ENV 상속되던 누출 차단
- runtime read 비밀번호 폴백이 write 값으로 잘못 대체되던 문제 ( 인접)
이미 설치된 사이트는 7.0.4 업그레이드 스텝이 잔존 파일을 정리한다.
완료 증거가 있으면 삭제하고, 설치 진행 중일 수 있으면 비밀만 제거한다.
전수조사에서 지적된 5건을 코드로 재확인하고 조치했다.
차단성:
- ja 언어팩 1.0.3 → 1.0.4. 1.0.3 은 이미 배포된 버전이라, 같은 버전에 키를
추가하면 기설치 사이트가 업데이트를 받지 못해 번역이 영영 도달하지 않는다.
회귀 가드 실질화 (green ≠ 회귀를 잡는다):
- 미등록 핸들러 억제 테스트가 동어반복이었다. 값을 손으로 대입하고 그 값을
읽어, 억제를 삭제해도 통과했다. 실제 dispatch 후 표시 계층이 호출되지
않음을 감시하도록 교체 (억제 제거 시 red 확인).
- script 번들의 "1건 취소 → 복구" E2E 는 수정 전에도 통과한다. 브라우저가
abort 된 script 를 스스로 재요청하기 때문 (수정 전 blade 로 실측: 재시도
훅 0개인데 hits=2, 렌더 정상). 거짓 안심이므로 제거하고, blade 가 고유하게
보장하는 것 — 재시도/폴백 훅 설치 + 정상 경로 오발동 없음 — 을 단언하도록
교체 (수정 전 red 확인).
- 재시도 횟수 단언을 하한(>=2)에서 정확값(==2)으로. 과잉 재시도 회귀를
통과시키던 구멍을 막는다.
정확도:
- waitForHandlers 가 전역 실패 플래그로 판정해, 무관한 확장의 실패가 정상
로드된 확장의 핸들러 대기까지 포기시켰다. 실패 키 단위로 정밀화한다.
레이아웃 편집기는 라우트 트리(280px 고정) + 디바이스 미리보기 캔버스 +
라벨 있는 툴바 버튼 10여 개가 나란히 놓이는 대화면 전용 도구다. 그런데
셸에 너비 하한이 없어 창을 좁히면 이들이 창 너비에 맞춰 압착됐고, 툴바가
flex row 라 각 항목 폭이 깎이면서 라벨이 글자 단위로 줄바꿈됐다("요 소 추 가").
반응형으로 재배치하는 대신 셸이 최소 너비(EDITOR_MIN_WIDTH=1280)를 유지하고
모자란 폭은 브라우저 가로 스크롤이 흡수하도록 했다. 편집기 UI 는 축약 대상이
아니며, 압착을 허용하면 캔버스 실측 폭이 뷰포트에 종속돼 디바이스 미리보기의
의미 자체가 사라진다.
툴바 압착 차단은 직접 자식 전체를 덮는 CSS 규칙으로 뒀다. 버튼마다 인라인
style 로 붙이면 자체 style 을 가진 하위 컴포넌트(템플릿 전환기·언어 전환기)와
이후 추가될 항목이 빠져 같은 결함이 재발한다.
jsdom 은 레이아웃 엔진이 없어 단위 테스트로는 "style 선언 존재" 까지만 잠긴다.
실제로 압착이 멈췄는지는 렌더 박스를 재는 브라우저에서만 증명되므로 Playwright
spec 을 함께 추가했다(수정 되돌림 시 red 확인). 아울러 이 파일의 e2e:allow 면제
주석은 사유("브라우저 가시 UI 무변경")가 더 이상 참이 아니라 제거했다.
모바일에서 새로고침을 연타하면 진행 중이던 요청이 취소되는데, 화면 구성에
필요한 요청 하나만 그렇게 끊겨도 전면 "초기화 실패" 로 끝났다. 재시도가 전혀
없었고, 로컬은 왕복이 수 ms 라 재현되지 않아 오래 남아 있었다.
fetch 는 4xx/5xx 로 reject 하지 않고 Response.ok=false 로 resolve 한다.
따라서 TypeError 는 "응답 자체가 없었다"(취소·커넥션 유실)는 뜻이며, 그 경우만
재시도해야 한다. HTTP 응답은 그대로 호출부에 넘겨 기존 상태코드 분기와 이중으로
겹치지 않게 했다.
- routes.json / components.json / 레이아웃 JSON: 네트워크 실패에만 지수 백오프 재시도
- 실패한 promise 가 레이아웃 캐시에 남아 영구 실패가 되던 문제 수정
- 문서 이탈(pagehide) 중에는 에러 화면을 그리지 않는다 (버려질 문서)
- 확장 에셋 로더의 실패 계약을 resolve → reject 로 (실패를 발생 지점에서 표면화)
- 번들 로드가 실패로 확정되면 오지 않을 핸들러를 5초간 기다리지 않는다
- 미등록 핸들러의 raw 내부 식별자가 토스트로 노출되던 것을 차단
- 번들이 끝내 부재해도 백지 대신 새로고침 버튼이 있는 안내 화면
정상 경로 성능은 종전과 같다. script 태그는 정적으로 유지해 프리로드 스캐너
이점을 보존하고, 실패했을 때만 onerror 에서 동적 재시도를 건다.
_localInit 은 단일 슬롯이고 updateTemplateData 가 얕은 스프레드로 교체했다.
소비부(DynamicRenderer useEffect)는 React commit 이후에 실행되므로,
initLocal 을 가진 progressive 소스가 둘 이상이면 두 payload 가 같은 commit
사이에 도착할 때 먼저 온 것이 한 번도 관측되지 않고 사라졌다.
무조건 누적 병합은 refetchDataSource 시 소비가 끝난 payload 를 재적용해
사용자 폼 편집을 되돌린다. 그래서 아직 관측되지 않은 슬롯만 병합하고,
관측된 슬롯은 종전대로 교체한다. 소비 여부는 소비부가 기록한 슬롯 참조로
판정해 해시 계산식을 생산부에 복제하지 않는다.
레이아웃 전환으로 _local 을 리셋할 때 추적 레지스트리도 함께 비운다.
검증: CPU 6배 스로틀로 경합 창을 강제로 열어 수정 전 0/8 → 수정 후 8/8.
동반: template-engine.test.ts 의 vi.mock 이 공유 런타임 export 를 빠뜨려
스위트가 로드되지 않던 것과, 코어 spec 2개가 없는 fixture 를 import 해
playwright 수집이 실패하던 것을 함께 복구했다.
$localized 가 ActionDispatcher 경로에서 항상 ko 를 반환했다. 컨텍스트에
$locale 이 없다는 것은 "로케일이 ko" 라는 뜻이 아니라 "이 경로가 로케일을
넘기지 않는다" 는 뜻인데, context.$locale || 'ko' 로 하드코딩 폴백하고 있었다.
바로 아래 $t 는 engine-v1.38.2 에서 이미 앱 설정 회수 폴백을 갖췄으나
$localized 만 정의 순서상 그 폴백보다 위에 있어 혜택을 받지 못했다.
로케일/templateId 해석을 두 헬퍼 앞으로 끌어올려 공유한다 (engine-v1.52.1).
국가 추가 폼은 이름 입력칸이 ko/en 두 칸으로 고정돼 그 외 언어의 국가명을
넣을 수 없었다. 설치된 언어를 $locales 로 순회해 입력칸을 만들고, 추가 조건도
특정 로케일 대신 "어느 한 언어라도 채워졌는가" 로 바꾼다. 백엔드
StoreEcommerceSettingsRequest 도 name 을 array 로만 요구하므로 계약이 일치한다.
저장 시 입력하지 않은 로케일은 남기지 않는다. 기존 저장본에 박힌 빈 문자열은
업그레이드 스텝이 청소한다 — 부재 로케일은 서버 localize_catalog_field 가
읽기 시점에 언어팩으로 보강하므로, 빈 문자열은 그 보강을 막는 어중간한 상태였다.
동일 결함 재발을 막는 audit 룰(engine-context-locale-hardcoded-fallback)을
함께 추가한다. 수정 전 코드에서 1건 검출, 수정 후 0건을 확인했다.
설치 시 use_read_db=false 인데도 db_read_host 잔존값으로 read 커넥션을
조립해 5단계 마이그레이션이 실패하던 문제를 수정한다. "read 사용 여부"
판정이 세 지점에서 달랐던 것이 근본 원인으로, use_read_db 플래그를 단일
SSoT 로 통일한다.
- installer-runtime.php: read 블록을 use_read_db 게이트로. 미사용 시 runtime
에 read 키 미생성 → 하류가 write 로 자동 fallback. mergeRuntimeIntoEnv 도
read 부재 시 DB_READ_* 를 빈 값으로 기록(write 복사 제거).
- config/database.php: read env 를 Elvis(?:) fallback 으로 복원. 중첩
env(A, env(B)) 는 DB_READ_* 가 빈 문자열일 때 fallback 이 무력화되므로,
빈 값까지 흡수하는 ?: 를 사용(tinker 실측 확인). 설치 이후 운영 중
DB_READ_* 공란 시에도 SELECT 가 write DB 로 정확히 향하게 한다.
- functions.php / request-handler.php: read 미사용 시 .env DB_READ_* 빈 값
기록 + state.json 잔존 read 필드 제거(2단계 방어).
기존 설치 환경은 .env 에 DB_READ_* 가 이미 채워져 Elvis fallback 미발동 →
동작 불변이라 업그레이드 스텝 불필요(실측 확인).
무관 사전 실패 테스트(beta.7 stale-ENV 복구)는 코어 version 이 beta.7 이
아니면 skip 하도록 가드 헬퍼 추가(프로덕션 한시 가드 전제를 테스트에 반영).
자유형식 제보에서 진단 축(버전/활성 확장/커스텀 자산/환경)이 누락되기 쉬워
G7 전용 이슈 템플릿을 추가한다. bug_report / feature_request / upgrade_issue
+ config.yml(blank 비활성, Discussions·보안신고 링크).
인스톨러 2단계(설치 환경 확인)의 checkDirectoryPermissions 가 storage 를
재귀 스캔하는데, checkSubdirectoriesRecursive 가 깊이 제한 없이 무한 재귀하여
storage/framework/testing 하위에 누적된 수만 개 디렉토리를 전부 순회 → 응답이
8초 이상 지연되던 문제.
쓰기 권한 문제는 트리 상위 얕은 깊이에서 결정되므로(상위 쓰기 가능 시 하위 상속),
재귀에 maxDepth(기본 1) 제한을 추가. 기본값이 있어 호출부는 변경 불필요.
실측: checkDirectoryPermissions 8.4초 → 0.086초.
Composer 실행 모드로 설치할 때 일부 Windows 환경에서 composer 출력의
invalid UTF-8 바이트가 정규화 없이 로그에 저장되어, 진행 상태 응답의
json_encode 가 false 를 반환 → 빈 본문(HTTP 200) → 프론트 파싱 실패로
진행 상황이 표시되지 않던 문제.
- 로그 저장(addLog)에서 mb_scrub 로 항상 유효 UTF-8 보장 (근본)
- composer 스트림 라인 정규화 (SSE/폴링 공통 경계)
- 폴링(state-management)·SSE(progress-emitter) 응답 json_encode 에
JSON_INVALID_UTF8_SUBSTITUTE + false 가드 (양쪽 안전망)
무관 결함 동시 정리: 신규 격리 테스트가 트리거한 BASE_PATH 누수로
깨지던 DeleteDirectoryTest·InstallerWindowsCommandsTest 를 프로젝트 루트
절대경로 앵커로 격리.
공개 이슈 gnuboard/g7 (@kitrio 제보).
활성 모듈/플러그인마다 프론트엔드 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 판정을 강제(이번 초기 누락 재발 방지)
매 요청 균일하게 발생하던 부팅 오버헤드를 4개 축에서 제거한다.
- 설치 완료 상태에서 매 요청 반복되던 information_schema 조회(알림·본인인증
테이블 hasTable)를 installer_completed 가드로 스킵. 미설치 환경은 기존
hasTable 폴백 유지.
- 코어+모듈+플러그인 정적 훅 매핑을 bootstrap/cache/hooks.php 에 사전 계산해
매 요청 디렉토리 스캔·리플렉션·클래스 로딩을 제거(route:cache 동형).
등록↔발화 계약·매핑 바이트 동일. 확장/코어 변경 시 자동 재생성, 캐시
부재·손상은 스캔 폴백.
- 확장 소스(Modules\*/Plugins\*)를 autoload-extensions.php 의 classmap 에
편입해 findFile 파일시스템 스캔을 제거(느린 FS·cold OPcache 환경 직격).
클래스 로딩은 여전히 lazy, PSR-4 폴백 유지.
- config 캐시를 변경 지점(설정 저장/코어·확장 업데이트/APP_KEY 재생성/설치
완료)에서 clear 후 즉시 재생성하도록 ConfigCacheHelper 로 일원화. clear 만
하고 방치돼 캐시가 영구 비활성으로 남던 성능 손실 제거. HookCacheManager::read
요청당 1회 로드(memo)로 중복 파싱 제거.
부팅 매핑 fingerprint 스캔↔캐시 완전 동일(229 action + 35 filter). 신규/수정
8개 스위트 54 pass. 순수 내부 부팅 인프라 — 확장 공개 표면·훅 계약 불변.
레이아웃 편집기·개발자 진단도구를 코어 번들에서 분리해 각각
편집 화면 진입 시·디버그 모드 시에만 지연 로드하도록 변경.
초기 접속 gzip payload 458KB → 221KB (52% 감소).
lazy 번들은 window.G7Core.__runtime 으로 코어 런타임을 공유해
React/컨텍스트/싱글톤 단일 인스턴스를 보장(재번들 0중복).
코어 7.0.1 bump.