중첩 라우트의 상위 리소스 ID 를 조회 스코프에 반영하지 않아 A 의 경로로 B 의
하위 리소스를 읽거나 변경할 수 있었다. 방어 위치를 Repository where 절(SSoT)로
통일하고 컨트롤러 사후 비교에 의존하지 않게 했다. 같은 리소스의 형제 엔드포인트가
서로 다른 강도로 검증하던 부분도 맞췄다.
계층 리소스는 자기 자신·자손을 부모로 지정하는 순환을 검증 계층에서 차단하고,
검증을 우회하는 경로(시더/훅/오염 데이터)를 위해 path 재귀에 방문 ID 가드를 뒀다.
설정값이 정하는 깊이 상한을 Service 가 리터럴로 재클램프하던 부분을 제거해
게시판 설정이 실제로 적용되게 했다.
쓰기 경로는 validated 기준으로 좁혀 FormRequest 미정의 필드가 fillable 로
새지 않게 했다. 기존 오염 데이터는 업그레이드 스텝이 정리한다(삭제 없이 최상위로 이동).
재발 방지로 audit 룰 3종을 추가하고, 규정 문서와 트러블슈팅 사례를 함께 갱신했다.
공개 이슈: gnuboard/g7
설치 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건 정상 로드 확인).
에셋 파일 서빙 방식은 언어·시간대와 무관한 인프라(서버 라우팅) 설정이라
지역화 카드에 묶이는 것이 의미상 어긋난다. tab_content_general 하위에
전용 카드(card_asset_serving)를 신설해 필드·자동 감지 버튼·인라인 안내를
이동하고, 라벨/설명을 카드 헤더로 승격해 다른 카드와 형태를 맞췄다.
배치 회귀 가드: 레이아웃 구조 테스트 + E2E 로 전용 카드에 항목이 있고
지역화 카드에는 없음을 단언.
관리자 환경설정 > 일반의 에셋 파일 서빙 방식 필드를 브라우저 검수 지적대로 다듬었다.
- "자동 감지" 버튼이 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 가 없어, 이후
모든 테스트 클래스가 중복 키로 깨졌다(단독 실행 시 미재현).
이번 이슈에서 손대지 않은 파일인데 Pint 실행 시 워킹트리에 떠올라,
HEAD 사본을 꺼내 pint --test 로 대조했다. 5개 파일 모두 fail 이며
fixer 목록이 워킹트리 diff 와 일치 — 이전부터 누적된 미적용분이다.
기능 커밋에 섞으면 diff 의 의미가 흐려지므로 분리했다.
FakerShim 의 16진 표기 변경은 UUID v4 비트 마스킹 결과에 영향이 없고,
bootstrap.php 의 use 임포트 추출은 ::class 가 완전한 FQN 으로 해석되므로
class_alias 대상이 달라지지 않는다. 이 파일은 autoload.files 진입점이라
전역 fake 에 닿는 자리여서 따로 확인했다.
`_bundled`(또는 `_pending`) 확장의 상세 정보를 읽는 두 메서드가 파일을 가드 없이
require 했다. 같은 확장의 활성 디렉토리 사본이 이미 로드돼 있으면 두 파일은 경로가 달라
중복 제거가 통하지 않으므로 같은 클래스명을 두 번 선언하게 되고, 그 자리에서 죽는다.
Cannot declare class Modules\Sirsoft\Board\Module, because the name is already in use
특히 위험한 것은 클래스 재선언이 Exception 이 아니라 Error 라는 점이다. 두 메서드 모두
try/catch 로 감싸여 있어 안전해 보이지만 실제로는 잡히지 않아 프로세스가 통째로 종료된다.
운영에서는 해당 요청이 죽고, 테스트에서는 요약 줄도 없이 스위트가 중단된다.
같은 파일의 다른 로드 지점들은 이미 "이미 선언돼 있으면 임시 클래스명으로 로드" 패턴을
갖고 있었고 이 두 곳만 빠져 있었다. 기존 패턴을 그대로 적용하고, 놓치던 Error 까지 잡도록
catch 범위를 넓혔다.
회귀 테스트는 처음에 실제 번들 확장으로 짰다가 폐기했다 — 테스트 부팅이 번들 쪽을 먼저
로드하므로 같은 경로를 다시 읽는 셈이라 위험한 조합이 만들어지지 않았고, 가드를 지워도
통과했다. 활성/번들 두 사본을 갖는 임시 확장 루트를 만들어 조합을 직접 재현하고, 어느 쪽
파일을 읽었는지까지 단언하도록 다시 작성했다. 가드를 제거하면 예측한 메시지 그대로 죽는다.
이 경로가 앞서 관측된 스위트 중단과 같은 사건인지는 단정하지 않는다. 메시지와 메커니즘은
일치하지만 그 중단은 이후 재현되지 않아 수정 전후 비교가 불가능했다.
검증: 신규 2건 + 확장 매니저 회귀 120건 green, 인스톨러·업그레이드 통합 581건 green.
테스트가 개발자의 `.env` 를 삭제한 사고 이후 부트스트랩에 복원 안전망을 넣었지만,
그것은 프로세스 종료 시점 복원이라 같은 실행 안에서 뒤 테스트가 훼손된 파일을 읽는 구간이
남아 있었다. 복원이 아니라 애초에 건드리지 않는 구조로 바꾼다.
앱 루트를 임시 디렉토리로 돌리면 `base_path` 를 쓰는 제품 코드는 한 줄도 바뀌지 않은 채
격리된 파일만 다룬다. 공용 트레이트로 묶어 세 클래스가 같은 방식을 쓰게 했다.
격리 자체를 증명하기 위해 안전망을 임시로 끄고 돌렸다. 안전망 없이도 `.env` 가 바이트
단위로 그대로였다 — "green 이니 됐다" 로는 안전망이 뒤에서 복원해 준 것과 구분되지 않는다.
이 과정에서 격리로는 막을 수 없는 유형을 하나 더 찾았다. 별도 프로세스를 띄워 업그레이드
커맨드를 실행하는 테스트가 자식 쪽에서 `.env` 의 버전 값을 덮어쓰고 있었다. 부모의 앱 루트
전환도, 테스트 부트스트랩의 안전망도 자식에는 닿지 않는다. 테스트 환경 지정은 읽는 파일만
바꿀 뿐 쓰기 대상은 언제나 프로젝트 루트의 `.env` 다. 그 테스트가 검증하는 것은 자식이 방금
쓴 클래스를 읽어들이는지 뿐이므로, 커맨드가 이미 제공하는 부수효과 차단 옵션을 붙였다.
같은 구조의 문제가 업그레이드 스텝 임시 파일에도 있었다 — 실제 `upgrades/` 에 쓰고 정리를
tearDown 에 의존하므로 중단되면 개발 체크아웃에 정체불명의 스텝이 남는다. 실제로 두 번
발견해 지웠고, 부트스트랩 셧다운 훅이 그 패턴을 쓸어내도록 했다.
검증: 세 클래스 개별 green, 안전망을 끈 통합 실행 28건 green + `.env` 불변,
자식 프로세스 테스트 green, 전 구간 `.env` 해시 불변, audit 위반 0.
알려진 잔여 문제: 인스톨러와 업그레이드 스위트를 한 프로세스로 합쳐 돌리면 모듈 클래스
중복 선언 fatal 로 중단된다. 각각 따로 돌리면 297 / 301 건 모두 통과하며, 이번 변경 이전에도
같은 증상이었다. 별도 조사가 필요해 이번 범위에서 다루지 않았다.
계획서 §검증 후반부를 다시 대조해 미구현 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 님께서 제보해주셨습니다.
여러 화면에서 조건부(FormRequest) 검증이 우회되거나 과도하게 제한되던 결함을 한 계열로 묶어 수정한다.
- 검증 규칙 확장 필터 이름 10개를 나머지 92개와 동일 규칙으로 통일
- 설명 길이를 바이트가 아닌 글자 수로 계산(한글 오검출 해소)
- 비활성 언어팩 번역 보존한 채 수정 허용
- 알림 템플릿 제목 nullable 존중, 미지원 발송 수단 저장 차단
- 일정/실행 이력 목록 조회 검증 정합
- 게시판 첨부 미사용 시 빈 확장자 통과, new_display_hours/최소 길이 0 허용, 일괄 적용 override_values 범위 검증(SSoT)
- 이커머스 클레임 사유 조회 검증, 용어 정정
- 원문 i18n 키 노출 다수 해소, 저장/삭제 실패 시 사유 화면 노출
- 아이콘 크기/다크모드 대비 회귀 수정
번들 확장(모듈3·플러그인6·템플릿2·언어팩5) 버전 new-bump 및 CHANGELOG 반영, ja 언어팩 동기.
브라우저 실측에서 경고가 경고로 읽히지 않는 문제가 드러났다. 기능은 정상
동작했으나 문구가 밋밋하고, 인스톨러 카드가 통과 항목과 같은 색이며, 관리자
정보 탭에서는 경고가 카드 사이에 끼어 있었다.
문구는 영향의 크기를 구체화했다 — 페이지를 열 때마다 모든 PHP 코드를 다시
해석하며 응답이 몇 배까지 느려진다는 점을 인스톨러와 관리자 양쪽에 같은 기준으로
반영했다.
인스톨러는 상태를 아이콘에만 반영하고 있어 목록을 훑을 때 문제 항목이 눈에
들어오지 않았다. 카드 자체에 상태 수식자를 부여했다. 경고만 고치면 필수 항목
실패 카드도 통과 카드와 똑같이 보이는 같은 결함이 남으므로 fail 도 함께 다뤘다.
관리자 정보 탭에서는 경고가 2단 카드 그리드의 세 번째 셀로 들어가 있었다.
탭 최상단으로 옮기고, 여백은 호출처가 아니라 자산이 책임지는 기존 컨벤션을
따라 alert 인접 규칙에 카드 그리드를 추가했다. 배치는 눈으로만 보면 회귀를
알 수 없으므로 좌표와 직계 자식 여부를 E2E 로 고정했다.
작업 중 전체 Vitest 스위트에서 사전 결함 6건을 발견해 같은 세션에서 종결한다.
SEO 탭의 sitemap_status 가 transition_overlay.wait_for 에 빠져 있어, 오버레이가
먼저 걷히며 진행상황 블록이 빈 상태로 잠깐 노출될 수 있었다. 구조가 동일한
policies 는 등록돼 있어 설계 의도가 아닌 누락으로 판정했다.
나머지 5건은 로직이 아니라 테스트가 낡은 것이었다. 사이드바 테스트는 프로필 UI
부재라는 의도 대신 컨테이너 id 부재로 단언해 이후 재도입된 크로스 이동 링크에
걸렸고, 대시보드 테스트는 iteration id 보간 도입 후에도 정확 일치로만 노드를
찾고 있었다. 각각 의도에 맞게 교정하고 반대 방향 회귀 가드를 보강했다.
설치 단계와 코어 부팅의 정책이 엇갈려 있었다. 인스톨러는 root 를 자유롭게
허용하면서, 설치 후 부팅은 같은 값을 보고 확장 로딩 전체를 조용히 스킵했다.
설치는 성공하지만 이후 깨진 화면만 뜨고 원인은 로그 한 줄에만 남았다.
판정을 프레임워크 의존성 0 인 공용 SSoT 로 모아 인스톨러와 코어가 같은 답을
내게 하고, 설치 단계에서 서버 2경로로 차단하며, 설치 후 변경은 전용 에러
페이지로 드러낸다. 가드를 boot 가 아닌 미들웨어에 두어 artisan 이 죽지 않게
해 복구 수단을 남겼다. admin 은 일반 권한 운영 계정 오차단을 막기 위해 차단
목록에서 제외했다.
OPcache 는 상태 조회 경로 자체가 없어 성능 저하를 인지할 방법이 없었다.
설치 단계 경고(비차단)와 관리자 정보 탭 표시를 같은 판정 SSoT 로 추가했다.
확장 로드 여부만으로는 부족해 opcache.enable 지시자까지 확인하며, ini_get 이
차단된 환경은 "확인 불가"로 폴백해 경고도 차단도 하지 않는다.
담기/바로구매가 메인 옵션을 로케일 값 조합(option_values)으로 전송하고 서버가
문자열 동등비교로 역매칭하던 방식은, 클라·서버 로케일 불일치나 관리자의 옵션 값
텍스트 수정 시 매칭 실패를 유발했다. 클라이언트가 이미 보유한 옵션 ID(optionId)를
전송하고, 서버는 요청 상품의 옵션 집합 내에서 ID로만 조회하도록 전환한다.
추가옵션(value_id/custom_text)은 이미 ID 기반이라 변경 없음.
관리자 카테고리 이미지 다운로드가 streamDownload 콜백 안에서
Storage::get 으로 파일 전체를 메모리에 적재한 뒤 echo 하던 안티패턴을,
공개 서빙과 동일한 CategoryImageService::download
(StorageInterface::response 스트리밍) 위임으로 교체한다.
서빙 계층은 이미 종단간 스트리밍이며 이 지점이 유일한 전체 적재 경로였다.
규정(storage-driver.md)에 서빙 안티패턴을 명시하고 audit coverage 에 등재한다.
결제 플러그인 3종(pay_kginicis/nhnkcp/nicepayments)과 sirsoft-basic 템플릿의
배포 JS 에 //# sourceMappingURL 주석과 .map 파일이 남아 있어, 공개 배포판에
원본 TS/TSX 전문(sourcesContent)이 노출될 수 있었다(NoSourcemapArtifactsTest 2건 실패).
각 확장을 --production(G7_BUILD_SOURCEMAP=0) 으로 재빌드해 소스맵 주석·.map 을
제거하고 {plugin|template}:update 로 활성 디렉토리에 반영. 기능 코드는 바이트 동일,
소스맵 주석만 제거된다. nhnkcp/nicepayments 의 components.json 파생 버전도 매니페스트
SSoT(1.0.0)로 재동기화. 매니페스트 버전은 무변경(동작 변화 없는 배포본 위생 수정).
NoSourcemapArtifactsTest 4/4 green.
계획서 위임 항목(Q6·Q7·Q8) 마감.
Q8: sitemaps.org 폐쇄 어휘를 App\Enums\SitemapChangeFreq 로 도입해
저장(SitemapIndexer)·렌더(SitemapXmlRenderer) 두 경계에서 normalize 하고,
생산 지점(생성기 정적 URL·3 contributor·4 리스너) 리터럴을 enum case 로 치환.
규격에 없는 changefreq 가 사이트맵에 그대로 출력되던 것을 차단(대소문자·공백 흡수).
Q6: Upgrade_7_0_0_beta_3Test 의 "동결 TARGETS === 살아있는 config" 단언은
동결 규정과 충돌하는 무효 불변식(config 는 에서 정당하게 진화)이므로
의도적 divergence 를 검증하도록 재작성.
Q7: 코어 20건 사전 실패를 격리 실측으로 재분류 — 6클러스터는 전 스위트
OOM/싱글톤 누수 오염(격리 시 통과), GenericNotificationViaTest 는 readiness
게이트를 mock 하지 않아 빈 settings 로 mail 이 항상 제외되던 stale test 를 정정.
관리자 "전체 재생성"을 큐(Full 모드)로 던지고 진행상황을 SEO 탭에서 확인한다.
Reverb ON 이면 방송 채널 구독으로 실시간 갱신, OFF 면 상태 API 폴링으로 폴백한다.
- SitemapProgress 진행상황 스토어(queued→running→writing→completed/failed,
캐시 TTL 3600, 방송 스로틀) + SitemapManager.getStatus 확장(progress + realtime_enabled)
- GET /api/admin/seo/sitemap/status(core.settings.read) 신규
- 관리자 재생성을 항상 Full 모드로 큐잉 + 진행률 기록, Job 최종 실패 시 progress=failed 고정
- core.admin.seo.sitemap 방송 채널 인증(core.settings.read)
- SEO 탭 진행상황 UI(상태 배지/단계/누적 URL/실시간·폴링 안내) + 폴링 분기
- 재생성 중 누적 URL 표기(replaceAllForContributor 삽입 건수 반환 — count 쿼리 없이 스트림 누적)
- ja 언어팩 진행상황 UI 번역 동기화
사이트맵을 리소스 단위로 영속화(sitemap_urls)해 진정한 증분 갱신과
도메인 재쿼리 없는 파일 재작성을 구현했다. 리소스 공개/비공개/삭제 시
해당 항목만 색인이 갱신되고, 관리자=full·스케줄러=auto·리소스 변경=증분으로 동작한다.
- sitemap_urls 마이그레이션 + SitemapUrl 모델. loc(2048) 를 unique 에 직접 넣으면
InnoDB 키 길이를 초과하므로 loc_hash(sha256) 컬럼을 identity 로 사용
- SitemapUrlRepository(멱등 upsert / remove / 스트리밍) + SitemapIndexer
(+ 제3자용 sitemap.index.collect_for_resource 필터 훅)
- 4 리스너 재배선: 공개→색인 / 비공개·삭제→색인 해제 + 재생성 잡 디바운스 디스패치.
3 기여자가 resource_type/resource_id 를 함께 emit 해 전체·증분 identity 를 일치
- 재생성 모드를 SitemapGenerationMode Backed Enum 으로 통일(Command→Job→Manager)
- seo:generate-sitemap 에 --rebuild / --mode 옵션 추가
AbstractSitemapContributor 브리지 base 로 인터페이스를 바꾸지 않고
board/ecommerce/page 기여자를 getUrlsLazy 지연 스트리밍으로 전환해,
게시글이 수백만 건인 사이트에서도 URL 배열을 통째로 적재하지 않는다.
SitemapGenerator 는 capability 를 감지해 지연 경로를 쓰고, base 를
상속하지 않은 제3자 구현체는 기존 getUrls 경로로 그대로 동작한다.
hreflang alternate 집합을 base URL 당 한 번만 계산해 로케일별 <url>
에 재사용하도록 바꿔 계산 비용을 로케일 수의 제곱에서 선형으로 줄였다
(정상 구성 출력은 불변). 다국어 대체 링크 사용 여부 토글과 로케일 수
상한을 추가하고 설정/검증/관리자 UI/다국어(ko·en·ja) 표면을 동반했다.
봇 요청 스레드의 동기 생성을 제거하고 디스크 스트리밍 서빙으로 전환했다.
신선도가 만료되면 생성 잡만 예약하고 기존 세트(stale) 또는 503+Retry-After 로
응답해, 대용량에서 봇 요청이 OOM/타임아웃을 유발하던 경로를 끊었다.
- 잡: ShouldBeUnique(고정 uniqueId)+uniqueFor+retryUntil, timeout 300→1800, backoff
- 컨트롤러: 인덱스 stale/503 분기 + 자식 라우트(.xml/.xml.gz 동일 액션)
- 스케줄 withoutOverlapping, 관리자 재생성 큐 이관
- 설정 4키(분할 기준·압축·stale 서빙·수집기 상한) + 고급탭 Sitemap 캐시 짝
- SitemapManager 응답 메시지 다국어화
설정 저장 테스트가 실제 설정 파일(사이트명/언어 포함)을 덮어쓰던 사전결함을
TestCase 전역 격리로 차단하고, 두 탭이 같은 다국어 키를 공유해 한쪽에 틀린
한도를 안내하던 사전결함을 키 분리로 해소했다.
대용량 사이트에서 사이트맵 생성이 메모리 부족으로 반복 실패하던 문제(공개 )의
1단계로, 전체 URL을 메모리에 적재하던 구조를 스트리밍 분할 생성으로 교체한다.
사이트맵 스트리밍 코어:
- SitemapWriter — 자식 파일 1개 분량만 버퍼링하고 임계 도달 시 flush.
StorageInterface가 append를 제공하지 않아(put이 전체 덮어쓰기) 택한 구조이며,
최대 메모리가 "자식 파일 1개 크기"로 유계가 된다. URL 수(50,000)와
바이트(45MB) 두 임계로 분할해 sitemaps.org 프로토콜을 지킨다.
- 커밋은 _tmp 전량 기록 → live 재배치 → manifest 마지막 기록 순. manifest 존재가
커밋 완료 신호다. 기존 manifest를 먼저 지우지 않아 스왑 중 봇에게 503이 나가지 않는다.
- SitemapFileStore — 읽기측 + 파일 경로 SSoT. SitemapXmlRenderer — escape 단일 출처.
데이터 접근 계층 정리:
- contributor 3종이 쿼리를 직접 조립하던 것을 Repository 위임으로 전환.
MySQL 버퍼드 쿼리에서 cursor는 결과셋 전체를 적재하므로 lazyById를 쓴다.
- SeoCacheStatsService → SeoCacheStatRepository 신설 (집계 4쿼리 → 1쿼리).
- audit 룰 service-direct-data-access의 사각 2건 수정 — 확장 디렉토리 정규식이
상대경로를 매칭하지 못해 *Contributor처럼 접미사 없는 클래스가 전부 미검출이었다.
SEO 캐시 설정 정합화:
- SEO 설정의 캐시 3키가 cache 카테고리 형제 키에 항상 져서 운영자 입력이 조용히
무시되고 있었다(설정을 cache 카테고리로 이관하며 SEO 탭 옛 칸을 미제거).
고급 탭을 기준값으로 두되 SEO 탭에 지정이 있으면 오버라이드하도록 SeoCacheSettings
단일 출처로 정리. "지정 여부"는 null로 판정하므로 기본값을 비웠다.
- 기존 설치는 Upgrade_7_1_0이 이행 — 옛 기본값과 같으면 미설정으로 비우고, 다르면
운영자 의도로 보아 보존한다.
코어 버전 bump와 루트 CHANGELOG는 계획서 결정(D20)에 따라 마감 단계에서 일괄 수행한다.
코어 7.0.5 릴리즈와 함께 버전이 오른 확장·언어팩의 CHANGELOG 출시 날짜를 오늘(2026-07-16)로 맞춤.
- 코어 7.0.5, ecommerce 1.0.4, gdpr 1.0.1, pay_kginicis 1.0.1, basic 1.0.3 출시일 통일
- core-ja 1.0.4, ecommerce-ja 1.0.4 출시일 통일
- pay_kginicis-ja: [Unreleased] 항목을 1.0.1 로 릴리즈 (manifest version 1.0.0 → 1.0.1 bump)
- pay_nhnkcp-ja / pay_nicepayments-ja 1.0.1 누락 날짜 명시
두 결제 플러그인을 1.0.0-beta.4 에서 정식 1.0.0 으로 올린다.
공개 release latest(beta.3) 대비 프리릴리즈 → 정식 승격으로
버전 bump 규정에 정합(new-bump). manifest/package/composer/lock
4개 파일 버전 동기화 + CHANGELOG [Unreleased] 를 1.0.0 으로 확정
(nicepayments 는 중복 Fixed 챕터 병합). g7_version(>=7.0.5) 은
코드가 7.0.5 도입 표면(미들웨어 self-gate·애플페이 iOS 컨텍스트)에
의존하므로 그대로 유지.
sudo 로 실행된 종료 시점 복구가 symlink 로 public/storage 를 재생성하면
소유권 인자가 없어 링크가 root:root 로 남아, 원래 앱 실행 유저 소유였던
링크의 소유권이 바뀌었다. 소유권 복원(restoreOwnership) 이후 재생성되어
복원 대상에서도 빠졌다.
StorageLinkHelper::createLink 가 symlink 성공 직후 부모 public/ 의
owner/group 을 lchown/lchgrp(링크 자체 대상 — chown 은 target 을 따라감)로
상속시켜, 이 프로젝트의 "신규 항목은 부모 소유권 상속" 컨벤션과 일치시킨다.
비-POSIX/함수 부재/권한 부족 시 무해하게 skip.
--prune 은 릴리즈 소스에 없는 public/storage 런타임 symlink 를 orphan 으로
삭제해 업로드 파일이 404 되었다. 심층 방어 2층으로 차단한다.
- 층1(예방): FilePermissionHelper 에 preserveLinkPaths 화이트리스트 추가.
applyUpdate 가 public 타깃에 ['storage'] 전달 → 매칭 orphan 링크/junction 만
삭제 제외 (정밀 보호)
- 층2(복구): StorageLinkHelper::ensurePublicStorageLink 신규 정적 헬퍼.
CoreUpdateCommand 종료 시점(정상+핸드오프) 호출로 부재/손상 링크 멱등 재생성
(Windows junction 폴백). beta.5 migration 05 는 이 헬퍼로 위임(상위호환)
SeoRenderer.getCssPath가 구 Vite manifest 구조만 파싱해 존재하지 않는 고정 경로 /build/assets/app.css 를 반환, 봇 렌더링 HTML(seo.blade)에서 CSS 404가 발생했다. 해당 파일은 실제 사용자 화면에서 미참조하는 Laravel 기본 빌드 산출물이므로, 담당자 답변 방향대로 참조 자체와 getCssPath 메서드를 제거했다. 봇 스타일은 템플릿 CSS($stylesheets)가 그대로 담당한다.
확장이 SP Kernel 미들웨어 그룹을 직접 조작하거나 라우트 파일에 미들웨어
FQCN 을 직접 부착하던 임시 방식을, 확장이 부착 대상(targets)을 명시 선언하고
코어가 요청 시점에 라우트명·URI 로 매칭해 실행하는 self-gate 로 정규화.
번들 7건(ecommerce/gdpr/pay 3종) 이전, audit 룰 3건·문서·버전 동기화 동반.
부수적으로 generate-skills.cjs 가 재생성 시 신의성실(부수의무) 섹션을 소실
시키던 회귀를 수정 — 스킬별 불릿을 스크립트 SSoT 로 편입해 재생성에도 보존.
3사 PG 플러그인의 체크아웃 DOM 인젝터가 렌더 후 브랜드 마크를 주입하고
injector끼리 같은 버튼을 두고 경합하던 구조를 제거한다. 시각정보(색 배지/
SVG 로고)를 결제수단 카탈로그(_cached_brand_mark)로 편입해 코어 체크아웃
레이아웃의 신규 BrandMark 컴포넌트가 데이터 기반으로 직접 렌더한다.
reconcileKginicisPatchedDuplicates(중복 방어)도 함께 제거했다.
애플페이 iOS 게이팅도 injector 밖으로 이관: 이커머스 모듈 DetectDevice
미들웨어가 UA로 기기를 판정해 코어 core.frontend.filter_app_config 훅으로
appConfig.isIos를 주입하고(1차), 템플릿 부트스트랩이 iPadOS 데스크탑 UA를
클라 신호로 보정한다(2차). 코어는 훅만 제공하고 감지 로직은 모듈이 소유한다.
미들웨어는 Router::pushMiddlewareToGroup 이 아닌 HTTP Kernel::appendMiddlewareToGroup
으로 web 그룹에 등록한다(전자는 Router 레지스트리만 갱신해 SSR 셸 요청 파이프라인에
미반영 → isIos 항상 false). sirsoft-gdpr 선례와 동일.
코어 최소 요구 버전 7.0.5 상향(appConfig 필터 훅 도입).
PG 플러그인이 등록하는 간편결제(네이버페이 등)를 코어가 정식 결제수단으로
인식하도록 결제수단 카탈로그를 SSoT 로 전환했다. 확장 결제수단이
pg_provider=null 로 등록돼 서버가 "PG 결제가 아닌 주문"으로 오인, 결제 실패
주문에 관리자 신규주문 알림이 발송되고 TempOrder 가 즉시 삭제돼 재결제가
막히던 두 결함을 하나의 원인(능력 해석을 enum 이 답하지 못함)으로 근본 해결.
- PaymentMethodResolver 신규: 능력(PG 필요/라벨/환불수단/PG 고정) 해석을
카탈로그 SSoT 로 통일, enum case 없는 확장 ID 도 동일 판정
- 프론트 결제수단 위장(card) 제거, provider-agnostic 결제 진입(pg_payment_handler)
- 관리자 주문설정 결제수단 목록 PG 고정 3분기 UI (pg_locked/needs_pg)
- 저장된 pg_provider=null 자가치유(read-time merge) + 백필 업그레이드 스텝
- 사용자 주문완료 화면 즉시 결제완료 헤더를 입금대기 여집합으로 판정(basic 1.0.3)
- 전 계층 회귀 테스트: 훅 발화 스파이(관리자 알림 미발화/PG 환불 훅 진입),
주문 응답 계약(requires_pg_payment/pg_payment_handler)+TempOrder 보존,
관리자 3분기 렌더, 확장 E2E 인프라